Compare commits

...

20 Commits

Author SHA1 Message Date
CaIon 41d20484f9 Merge branch 'main' into alpha 2025-07-01 13:15:47 +08:00
CaIon 0d336793bb Merge remote-tracking branch 'origin/alpha' into alpha 2025-07-01 13:14:40 +08:00
CaIon cfc8dcf7af feat: enhance JWT exchange process with proxy support. (close #1087) 2025-07-01 13:14:24 +08:00
CaIon 2f04ab0daf feat: enhance environment configuration and resource initialization 2025-07-01 13:13:30 +08:00
Calcium-Ion ffaf60df1d Merge pull request #1322 from feitianbubu/pr/jimeng-key-delimiter
feat: jimeng apiKey format to use `|` delimiter
2025-07-01 10:44:19 +08:00
skynono c26029895d feat: jimeng apiKey format to use | delimiter 2025-07-01 10:35:29 +08:00
同語 8e0f1bf451 🐛 fix: ratelimit style error
Merge pull request #1301 from tbphp/fix_ratelimit_style
2025-06-29 02:34:06 +08:00
t0ng7u 16cb2a4a14 style: change the border radius of most components from full to lg size 2025-06-29 02:32:09 +08:00
t0ng7u 8e7d3b3d86 🤯style: remove useless card headerStyle 2025-06-29 00:11:15 +08:00
CaIon e2c34bdeb9 🐛 fix: refactor JSON unmarshalling across multiple handlers to use UnmarshalJson and UnmarshalJsonStr for consistency
This update replaces instances of DecodeJson and DecodeJsonStr with UnmarshalJson and UnmarshalJsonStr in various relay handlers, enhancing code consistency and clarity in JSON processing. The changes improve maintainability and align with recent refactoring efforts in the codebase.
2025-06-28 00:02:07 +08:00
CaIon aae9e81c20 🐛 fix: refactor response body handling in multiple relay handlers to utilize IOCopyBytesGracefully 2025-06-27 23:35:56 +08:00
CaIon defc18dea8 🐛 fix: refactor JSON encoding and decoding in OpenAI handlers for improved consistency 2025-06-27 22:45:36 +08:00
CaIon 452cf0dcc4 🐛 fix: refactor OaiStreamHandler to improve last response handling and streamline response body closure 2025-06-27 22:44:20 +08:00
CaIon b7776bec62 🐛 fix: update hardcoded completion model ratio for gemini-2.5-flash-lite 2025-06-27 22:36:23 +08:00
CaIon 5178e563be feat: introduce IOCopyBytesGracefully function for streamlined response body handling
This update adds the IOCopyBytesGracefully function to the common package, which simplifies the process of copying response bodies in the OpenAI handlers. It enhances error handling and ensures proper resource management by encapsulating the logic for setting headers and writing response data. The OpenAI handlers have been refactored to utilize this new function, improving code clarity and maintainability.
2025-06-27 22:36:12 +08:00
CaIon 6db7a84787 🐛 fix: replace direct response body closure with common.CloseResponseBodyGracefully for improved error handling
This update standardizes the closure of HTTP response bodies across multiple stream handlers, enhancing error management and resource cleanup. The new method ensures that any errors during closure are handled gracefully, preventing potential request termination issues.
2025-06-27 21:40:36 +08:00
CaIon ae586e1be9 feat: add CloseResponseBodyGracefully function to handle HTTP response body closure 2025-06-27 21:37:13 +08:00
Xyfacai d76c3ebeca Merge pull request #1298 from xiangyuanliu/feat/page-format
feat: 优化分页组件
2025-06-27 15:55:30 +08:00
tbphp 8d34c57239 fix: ratelimit style error 2025-06-26 21:32:05 +08:00
Xiangyuan-liu b206a7c683 feat: 优化分页组件 2025-06-25 18:42:19 +08:00
68 changed files with 516 additions and 832 deletions
+10 -2
View File
@@ -7,6 +7,8 @@
# 调试相关配置
# 启用pprof
# ENABLE_PPROF=true
# 启用调试模式
# DEBUG=true
# 数据库相关配置
# 数据库连接字符串
@@ -41,6 +43,14 @@
# 更新任务启用
# UPDATE_TASK=true
# 对话超时设置
# 所有请求超时时间,单位秒,默认为0,表示不限制
# RELAY_TIMEOUT=0
# 流模式无响应超时时间,单位秒,如果出现空补全可以尝试改为更大值
# STREAMING_TIMEOUT=120
# Gemini 识别图片 最大图片数量
# GEMINI_VISION_MAX_IMAGE_NUM=16
# 会话密钥
# SESSION_SECRET=random_string
@@ -58,8 +68,6 @@
# GET_MEDIA_TOKEN_NOT_STREAM=true
# 设置 Dify 渠道是否输出工作流和节点信息到客户端
# DIFY_DEBUG=true
# 设置流式一次回复的超时时间
# STREAMING_TIMEOUT=120
# 节点类型
+1 -2
View File
@@ -2,7 +2,6 @@ package common
import (
"bytes"
"encoding/json"
"github.com/gin-gonic/gin"
"io"
"strings"
@@ -31,7 +30,7 @@ func UnmarshalBodyReusable(c *gin.Context, v any) error {
}
contentType := c.Request.Header.Get("Content-Type")
if strings.HasPrefix(contentType, "application/json") {
err = json.Unmarshal(requestBody, &v)
err = UnmarshalJson(requestBody, &v)
} else {
// skip for now
// TODO: someday non json request have variant model, we will need to implementation this
+57
View File
@@ -0,0 +1,57 @@
package common
import (
"bytes"
"fmt"
"io"
"net/http"
"github.com/gin-gonic/gin"
)
func CloseResponseBodyGracefully(httpResponse *http.Response) {
if httpResponse == nil || httpResponse.Body == nil {
return
}
err := httpResponse.Body.Close()
if err != nil {
SysError("failed to close response body: " + err.Error())
}
}
func IOCopyBytesGracefully(c *gin.Context, src *http.Response, data []byte) {
if c.Writer == nil {
return
}
body := io.NopCloser(bytes.NewBuffer(data))
// We shouldn't set the header before we parse the response body, because the parse part may fail.
// And then we will have to send an error response, but in this case, the header has already been set.
// So the httpClient will be confused by the response.
// For example, Postman will report error, and we cannot check the response at all.
if src != nil {
for k, v := range src.Header {
// avoid setting Content-Length
if k == "Content-Length" {
continue
}
c.Writer.Header().Set(k, v[0])
}
}
// set Content-Length header manually BEFORE calling WriteHeader
c.Writer.Header().Set("Content-Length", fmt.Sprintf("%d", len(data)))
// Write header with status code (this sends the headers)
if src != nil {
c.Writer.WriteHeader(src.StatusCode)
} else {
c.Writer.WriteHeader(http.StatusOK)
}
_, err := io.Copy(c.Writer, body)
if err != nil {
LogError(c, fmt.Sprintf("failed to copy response body: %s", err.Error()))
}
}
+1 -1
View File
@@ -24,7 +24,7 @@ func printHelp() {
fmt.Println("Usage: one-api [--port <port>] [--log-dir <log directory>] [--version] [--help]")
}
func LoadEnv() {
func InitCommonEnv() {
flag.Parse()
if *PrintVersion {
+8 -4
View File
@@ -5,12 +5,16 @@ import (
"encoding/json"
)
func DecodeJson(data []byte, v any) error {
return json.NewDecoder(bytes.NewReader(data)).Decode(v)
func UnmarshalJson(data []byte, v any) error {
return json.Unmarshal(data, v)
}
func DecodeJsonStr(data string, v any) error {
return DecodeJson(StringToByteSlice(data), v)
func UnmarshalJsonStr(data string, v any) error {
return json.Unmarshal(StringToByteSlice(data), v)
}
func DecodeJson(reader *bytes.Reader, v any) error {
return json.NewDecoder(reader).Decode(v)
}
func EncodeJson(v any) ([]byte, error) {
+62
View File
@@ -0,0 +1,62 @@
package common
import (
"github.com/gin-gonic/gin"
"strconv"
)
type PageInfo struct {
Page int `json:"page"` // page num 页码
PageSize int `json:"page_size"` // page size 页大小
StartTimestamp int64 `json:"start_timestamp"` // 秒级
EndTimestamp int64 `json:"end_timestamp"` // 秒级
Total int `json:"total"` // 总条数,后设置
Items any `json:"items"` // 数据,后设置
}
func (p *PageInfo) GetStartIdx() int {
return (p.Page - 1) * p.PageSize
}
func (p *PageInfo) GetEndIdx() int {
return p.Page * p.PageSize
}
func (p *PageInfo) GetPageSize() int {
return p.PageSize
}
func (p *PageInfo) GetPage() int {
return p.Page
}
func (p *PageInfo) SetTotal(total int) {
p.Total = total
}
func (p *PageInfo) SetItems(items any) {
p.Items = items
}
func GetPageQuery(c *gin.Context) (*PageInfo, error) {
pageInfo := &PageInfo{}
err := c.BindQuery(pageInfo)
if err != nil {
return nil, err
}
if pageInfo.Page < 1 {
// 兼容
page, _ := strconv.Atoi(c.Query("p"))
if page != 0 {
pageInfo.Page = page
} else {
pageInfo.Page = 1
}
}
if pageInfo.PageSize == 0 {
pageInfo.PageSize = ItemsPerPage
}
return pageInfo, nil
}
+12 -14
View File
@@ -246,15 +246,15 @@ func Register(c *gin.Context) {
}
func GetAllUsers(c *gin.Context) {
p, _ := strconv.Atoi(c.Query("p"))
pageSize, _ := strconv.Atoi(c.Query("page_size"))
if p < 1 {
p = 1
pageInfo, err := common.GetPageQuery(c)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "parse page query failed",
})
return
}
if pageSize < 0 {
pageSize = common.ItemsPerPage
}
users, total, err := model.GetAllUsers((p-1)*pageSize, pageSize)
users, total, err := model.GetAllUsers(pageInfo)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
@@ -262,15 +262,13 @@ func GetAllUsers(c *gin.Context) {
})
return
}
pageInfo.SetTotal(int(total))
pageInfo.SetItems(users)
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
"data": gin.H{
"items": users,
"total": total,
"page": p,
"page_size": pageSize,
},
"data": pageInfo,
})
return
}
+1 -1
View File
@@ -16,7 +16,7 @@ services:
- REDIS_CONN_STRING=redis://redis
- TZ=Asia/Shanghai
- ERROR_LOG_ENABLED=true # 是否启用错误日志记录
# - TIKTOKEN_CACHE_DIR=./tiktoken_cache # 如果需要使用tiktoken_cache,请取消注释
# - STREAMING_TIMEOUT=120 # 流模式无响应超时时间,单位秒,默认120秒,如果出现空补全可以尝试改为更大值
# - SESSION_SECRET=random_string # 多机部署时设置,必须修改这个随机字符串!!!!!!!
# - NODE_TYPE=slave # Uncomment for slave node in multi-node deployment
# - SYNC_FREQUENCY=60 # Uncomment if regular database syncing is needed
+1 -1
View File
@@ -66,7 +66,7 @@ type GeneralOpenAIRequest struct {
func (r *GeneralOpenAIRequest) ToMap() map[string]any {
result := make(map[string]any)
data, _ := common.EncodeJson(r)
_ = common.DecodeJson(data, &result)
_ = common.UnmarshalJson(data, &result)
return result
}
+52 -32
View File
@@ -32,12 +32,12 @@ var buildFS embed.FS
var indexPage []byte
func main() {
err := godotenv.Load(".env")
if err != nil {
common.SysLog("Support for .env file is disabled: " + err.Error())
}
common.LoadEnv()
err := InitResources()
if err != nil {
common.FatalLog("failed to initialize resources: " + err.Error())
return
}
common.SetupLogger()
common.SysLog("New API " + common.Version + " started")
@@ -47,19 +47,7 @@ func main() {
if common.DebugEnabled {
common.SysLog("running in debug mode")
}
// Initialize SQL Database
err = model.InitDB()
if err != nil {
common.FatalLog("failed to initialize database: " + err.Error())
}
model.CheckSetup()
// Initialize SQL Database
err = model.InitLogDB()
if err != nil {
common.FatalLog("failed to initialize database: " + err.Error())
}
defer func() {
err := model.CloseDB()
if err != nil {
@@ -67,21 +55,6 @@ func main() {
}
}()
// Initialize Redis
err = common.InitRedisClient()
if err != nil {
common.FatalLog("failed to initialize Redis: " + err.Error())
}
// Initialize model settings
ratio_setting.InitRatioSettings()
// Initialize constants
constant.InitEnv()
// Initialize options
model.InitOptionMap()
service.InitTokenEncoders()
if common.RedisEnabled {
// for compatibility with old versions
common.MemoryCacheEnabled = true
@@ -186,3 +159,50 @@ func main() {
common.FatalLog("failed to start HTTP server: " + err.Error())
}
}
func InitResources() error {
// Initialize resources here if needed
// This is a placeholder function for future resource initialization
err := godotenv.Load(".env")
if err != nil {
common.SysLog("未找到 .env 文件,使用默认环境变量,如果需要,请创建 .env 文件并设置相关变量")
common.SysLog("No .env file found, using default environment variables. If needed, please create a .env file and set the relevant variables.")
}
// 加载旧的(common)环境变量
common.InitCommonEnv()
// 加载constants的环境变量
constant.InitEnv()
// Initialize model settings
ratio_setting.InitRatioSettings()
service.InitHttpClient()
service.InitTokenEncoders()
// Initialize SQL Database
err = model.InitDB()
if err != nil {
common.FatalLog("failed to initialize database: " + err.Error())
return err
}
model.CheckSetup()
// Initialize options, should after model.InitDB()
model.InitOptionMap()
// Initialize SQL Database
err = model.InitLogDB()
if err != nil {
return err
}
// Initialize Redis
err = common.InitRedisClient()
if err != nil {
return err
}
return nil
}
+2 -2
View File
@@ -114,7 +114,7 @@ func GetMaxUserId() int {
return user.Id
}
func GetAllUsers(startIdx int, num int) (users []*User, total int64, err error) {
func GetAllUsers(pageInfo *common.PageInfo) (users []*User, total int64, err error) {
// Start transaction
tx := DB.Begin()
if tx.Error != nil {
@@ -134,7 +134,7 @@ func GetAllUsers(startIdx int, num int) (users []*User, total int64, err error)
}
// Get paginated users within same transaction
err = tx.Unscoped().Order("id desc").Limit(num).Offset(startIdx).Omit("password").Find(&users).Error
err = tx.Unscoped().Order("id desc").Limit(pageInfo.GetPageSize()).Offset(pageInfo.GetStartIdx()).Omit("password").Find(&users).Error
if err != nil {
tx.Rollback()
return nil, 0, err
+1 -4
View File
@@ -132,10 +132,7 @@ func aliImageHandler(c *gin.Context, resp *http.Response, info *relaycommon.Rela
if err != nil {
return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
}
err = resp.Body.Close()
if err != nil {
return service.OpenAIErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
}
common.CloseResponseBodyGracefully(resp)
err = json.Unmarshal(responseBody, &aliTaskResponse)
if err != nil {
return service.OpenAIErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
+2 -4
View File
@@ -4,6 +4,7 @@ import (
"encoding/json"
"io"
"net/http"
"one-api/common"
"one-api/dto"
relaycommon "one-api/relay/common"
"one-api/service"
@@ -35,10 +36,7 @@ func RerankHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayI
if err != nil {
return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
}
err = resp.Body.Close()
if err != nil {
return service.OpenAIErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
}
common.CloseResponseBodyGracefully(resp)
var aliResponse AliRerankResponse
err = json.Unmarshal(responseBody, &aliResponse)
+3 -12
View File
@@ -45,10 +45,7 @@ func aliEmbeddingHandler(c *gin.Context, resp *http.Response) (*dto.OpenAIErrorW
return service.OpenAIErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
}
err = resp.Body.Close()
if err != nil {
return service.OpenAIErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
}
common.CloseResponseBodyGracefully(resp)
if aliResponse.Code != "" {
return &dto.OpenAIErrorWithStatusCode{
@@ -186,10 +183,7 @@ func aliStreamHandler(c *gin.Context, resp *http.Response) (*dto.OpenAIErrorWith
return false
}
})
err := resp.Body.Close()
if err != nil {
return service.OpenAIErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
}
common.CloseResponseBodyGracefully(resp)
return nil, &usage
}
@@ -199,10 +193,7 @@ func aliHandler(c *gin.Context, resp *http.Response) (*dto.OpenAIErrorWithStatus
if err != nil {
return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
}
err = resp.Body.Close()
if err != nil {
return service.OpenAIErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
}
common.CloseResponseBodyGracefully(resp)
err = json.Unmarshal(responseBody, &aliResponse)
if err != nil {
return service.OpenAIErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
+4 -13
View File
@@ -166,10 +166,7 @@ func baiduStreamHandler(c *gin.Context, resp *http.Response) (*dto.OpenAIErrorWi
return false
}
})
err := resp.Body.Close()
if err != nil {
return service.OpenAIErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
}
common.CloseResponseBodyGracefully(resp)
return nil, &usage
}
@@ -179,10 +176,7 @@ func baiduHandler(c *gin.Context, resp *http.Response) (*dto.OpenAIErrorWithStat
if err != nil {
return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
}
err = resp.Body.Close()
if err != nil {
return service.OpenAIErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
}
common.CloseResponseBodyGracefully(resp)
err = json.Unmarshal(responseBody, &baiduResponse)
if err != nil {
return service.OpenAIErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
@@ -215,10 +209,7 @@ func baiduEmbeddingHandler(c *gin.Context, resp *http.Response) (*dto.OpenAIErro
if err != nil {
return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
}
err = resp.Body.Close()
if err != nil {
return service.OpenAIErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
}
common.CloseResponseBodyGracefully(resp)
err = json.Unmarshal(responseBody, &baiduResponse)
if err != nil {
return service.OpenAIErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
@@ -280,7 +271,7 @@ func getBaiduAccessTokenHelper(apiKey string) (*BaiduAccessToken, error) {
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Accept", "application/json")
res, err := service.GetImpatientHttpClient().Do(req)
res, err := service.GetHttpClient().Do(req)
if err != nil {
return nil, err
}
+7 -7
View File
@@ -125,7 +125,7 @@ func RequestOpenAI2ClaudeMessage(textRequest dto.GeneralOpenAIRequest) (*dto.Cla
if textRequest.Reasoning != nil {
var reasoning openrouter.RequestReasoning
if err := common.DecodeJson(textRequest.Reasoning, &reasoning); err != nil {
if err := common.UnmarshalJson(textRequest.Reasoning, &reasoning); err != nil {
return nil, err
}
@@ -519,7 +519,7 @@ func FormatClaudeResponseInfo(requestMode int, claudeResponse *dto.ClaudeRespons
func HandleStreamResponseData(c *gin.Context, info *relaycommon.RelayInfo, claudeInfo *ClaudeResponseInfo, data string, requestMode int) *dto.OpenAIErrorWithStatusCode {
var claudeResponse dto.ClaudeResponse
err := common.DecodeJsonStr(data, &claudeResponse)
err := common.UnmarshalJsonStr(data, &claudeResponse)
if err != nil {
common.SysError("error unmarshalling stream response: " + err.Error())
return service.OpenAIErrorWrapper(err, "stream_response_error", http.StatusInternalServerError)
@@ -619,7 +619,7 @@ func ClaudeStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.
func HandleClaudeResponseData(c *gin.Context, info *relaycommon.RelayInfo, claudeInfo *ClaudeResponseInfo, data []byte, requestMode int) *dto.OpenAIErrorWithStatusCode {
var claudeResponse dto.ClaudeResponse
err := common.DecodeJson(data, &claudeResponse)
err := common.UnmarshalJson(data, &claudeResponse)
if err != nil {
return service.OpenAIErrorWrapper(err, "unmarshal_claude_response_failed", http.StatusInternalServerError)
}
@@ -657,13 +657,14 @@ func HandleClaudeResponseData(c *gin.Context, info *relaycommon.RelayInfo, claud
case relaycommon.RelayFormatClaude:
responseData = data
}
c.Writer.Header().Set("Content-Type", "application/json")
c.Writer.WriteHeader(http.StatusOK)
_, err = c.Writer.Write(responseData)
common.IOCopyBytesGracefully(c, nil, responseData)
return nil
}
func ClaudeHandler(c *gin.Context, resp *http.Response, requestMode int, info *relaycommon.RelayInfo) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
defer common.CloseResponseBodyGracefully(resp)
claudeInfo := &ClaudeResponseInfo{
ResponseId: fmt.Sprintf("chatcmpl-%s", common.GetUUID()),
Created: common.GetTimestamp(),
@@ -675,7 +676,6 @@ func ClaudeHandler(c *gin.Context, resp *http.Response, requestMode int, info *r
if err != nil {
return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
}
resp.Body.Close()
if common.DebugEnabled {
println("responseBody: ", string(responseBody))
}
+3 -12
View File
@@ -81,10 +81,7 @@ func cfStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.Rela
}
helper.Done(c)
err := resp.Body.Close()
if err != nil {
common.LogError(c, "close_response_body_failed: "+err.Error())
}
common.CloseResponseBodyGracefully(resp)
return nil, usage
}
@@ -94,10 +91,7 @@ func cfHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo)
if err != nil {
return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
}
err = resp.Body.Close()
if err != nil {
return service.OpenAIErrorWrapperLocal(err, "close_response_body_failed", http.StatusInternalServerError), nil
}
common.CloseResponseBodyGracefully(resp)
var response dto.TextResponse
err = json.Unmarshal(responseBody, &response)
if err != nil {
@@ -127,10 +121,7 @@ func cfSTTHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayIn
if err != nil {
return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
}
err = resp.Body.Close()
if err != nil {
return service.OpenAIErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
}
common.CloseResponseBodyGracefully(resp)
err = json.Unmarshal(responseBody, &cfResp)
if err != nil {
return service.OpenAIErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
+2 -8
View File
@@ -173,10 +173,7 @@ func cohereHandler(c *gin.Context, resp *http.Response, modelName string, prompt
if err != nil {
return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
}
err = resp.Body.Close()
if err != nil {
return service.OpenAIErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
}
common.CloseResponseBodyGracefully(resp)
var cohereResp CohereResponseResult
err = json.Unmarshal(responseBody, &cohereResp)
if err != nil {
@@ -217,10 +214,7 @@ func cohereRerankHandler(c *gin.Context, resp *http.Response, info *relaycommon.
if err != nil {
return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
}
err = resp.Body.Close()
if err != nil {
return service.OpenAIErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
}
common.CloseResponseBodyGracefully(resp)
var cohereResp CohereRerankResponseResult
err = json.Unmarshal(responseBody, &cohereResp)
if err != nil {
+1 -4
View File
@@ -48,10 +48,7 @@ func cozeChatHandler(c *gin.Context, resp *http.Response, info *relaycommon.Rela
if err != nil {
return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
}
err = resp.Body.Close()
if err != nil {
return service.OpenAIErrorWrapperLocal(err, "close_response_body_failed", http.StatusInternalServerError), nil
}
common.CloseResponseBodyGracefully(resp)
// convert coze response to openai response
var response dto.TextResponse
var cozeResponse CozeChatDetailResponse
+2 -5
View File
@@ -95,7 +95,7 @@ func uploadDifyFile(c *gin.Context, info *relaycommon.RelayInfo, user string, me
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", info.ApiKey))
// Send request
client := service.GetImpatientHttpClient()
client := service.GetHttpClient()
resp, err := client.Do(req)
if err != nil {
common.SysError("failed to send request: " + err.Error())
@@ -257,10 +257,7 @@ func difyHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInf
if err != nil {
return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
}
err = resp.Body.Close()
if err != nil {
return service.OpenAIErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
}
common.CloseResponseBodyGracefully(resp)
err = json.Unmarshal(responseBody, &difyResponse)
if err != nil {
return service.OpenAIErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
+6 -15
View File
@@ -1,7 +1,6 @@
package gemini
import (
"encoding/json"
"io"
"net/http"
"one-api/common"
@@ -15,15 +14,13 @@ import (
)
func GeminiTextGenerationHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (*dto.Usage, *dto.OpenAIErrorWithStatusCode) {
defer common.CloseResponseBodyGracefully(resp)
// 读取响应体
responseBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError)
}
err = resp.Body.Close()
if err != nil {
return nil, service.OpenAIErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError)
}
if common.DebugEnabled {
println(string(responseBody))
@@ -31,7 +28,7 @@ func GeminiTextGenerationHandler(c *gin.Context, resp *http.Response, info *rela
// 解析为 Gemini 原生响应格式
var geminiResponse GeminiChatResponse
err = common.DecodeJson(responseBody, &geminiResponse)
err = common.UnmarshalJson(responseBody, &geminiResponse)
if err != nil {
return nil, service.OpenAIErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError)
}
@@ -54,18 +51,12 @@ func GeminiTextGenerationHandler(c *gin.Context, resp *http.Response, info *rela
}
// 直接返回 Gemini 原生格式的 JSON 响应
jsonResponse, err := json.Marshal(geminiResponse)
jsonResponse, err := common.EncodeJson(geminiResponse)
if err != nil {
return nil, service.OpenAIErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError)
}
// 设置响应头并写入响应
c.Writer.Header().Set("Content-Type", "application/json")
c.Writer.WriteHeader(resp.StatusCode)
_, err = c.Writer.Write(jsonResponse)
if err != nil {
return nil, service.OpenAIErrorWrapper(err, "write_response_failed", http.StatusInternalServerError)
}
common.IOCopyBytesGracefully(c, resp, jsonResponse)
return &usage, nil
}
@@ -80,7 +71,7 @@ func GeminiTextGenerationStreamHandler(c *gin.Context, resp *http.Response, info
helper.StreamScannerHandler(c, resp, info, func(data string) bool {
var geminiResponse GeminiChatResponse
err := common.DecodeJsonStr(data, &geminiResponse)
err := common.UnmarshalJsonStr(data, &geminiResponse)
if err != nil {
common.LogError(c, "error unmarshalling stream response: "+err.Error())
return false
+7 -12
View File
@@ -801,7 +801,7 @@ func GeminiChatStreamHandler(c *gin.Context, resp *http.Response, info *relaycom
helper.StreamScannerHandler(c, resp, info, func(data string) bool {
var geminiResponse GeminiChatResponse
err := common.DecodeJsonStr(data, &geminiResponse)
err := common.UnmarshalJsonStr(data, &geminiResponse)
if err != nil {
common.LogError(c, "error unmarshalling stream response: "+err.Error())
return false
@@ -866,15 +866,12 @@ func GeminiChatHandler(c *gin.Context, resp *http.Response, info *relaycommon.Re
if err != nil {
return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
}
err = resp.Body.Close()
if err != nil {
return service.OpenAIErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
}
common.CloseResponseBodyGracefully(resp)
if common.DebugEnabled {
println(string(responseBody))
}
var geminiResponse GeminiChatResponse
err = common.DecodeJson(responseBody, &geminiResponse)
err = common.UnmarshalJson(responseBody, &geminiResponse)
if err != nil {
return service.OpenAIErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
}
@@ -920,11 +917,12 @@ func GeminiChatHandler(c *gin.Context, resp *http.Response, info *relaycommon.Re
}
func GeminiEmbeddingHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *dto.OpenAIErrorWithStatusCode) {
defer common.CloseResponseBodyGracefully(resp)
responseBody, readErr := io.ReadAll(resp.Body)
if readErr != nil {
return nil, service.OpenAIErrorWrapper(readErr, "read_response_body_failed", http.StatusInternalServerError)
}
_ = resp.Body.Close()
var geminiResponse GeminiEmbeddingResponse
if jsonErr := json.Unmarshal(responseBody, &geminiResponse); jsonErr != nil {
@@ -956,14 +954,11 @@ func GeminiEmbeddingHandler(c *gin.Context, resp *http.Response, info *relaycomm
}
openAIResponse.Usage = *usage.(*dto.Usage)
jsonResponse, jsonErr := json.Marshal(openAIResponse)
jsonResponse, jsonErr := common.EncodeJson(openAIResponse)
if jsonErr != nil {
return nil, service.OpenAIErrorWrapper(jsonErr, "marshal_response_failed", http.StatusInternalServerError)
}
c.Writer.Header().Set("Content-Type", "application/json")
c.Writer.WriteHeader(resp.StatusCode)
_, _ = c.Writer.Write(jsonResponse)
common.IOCopyBytesGracefully(c, resp, jsonResponse)
return usage, nil
}
+3 -6
View File
@@ -5,6 +5,7 @@ import (
"github.com/gin-gonic/gin"
"io"
"net/http"
"one-api/common"
"one-api/dto"
"one-api/service"
)
@@ -26,7 +27,7 @@ func embeddingRequestOpenAI2Moka(request dto.GeneralOpenAIRequest) *dto.Embeddin
}
return &dto.EmbeddingRequest{
Input: input,
Model: request.Model,
Model: request.Model,
}
}
@@ -53,10 +54,7 @@ func mokaEmbeddingHandler(c *gin.Context, resp *http.Response) (*dto.OpenAIError
if err != nil {
return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
}
err = resp.Body.Close()
if err != nil {
return service.OpenAIErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
}
common.CloseResponseBodyGracefully(resp)
err = json.Unmarshal(responseBody, &baiduResponse)
if err != nil {
return service.OpenAIErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
@@ -80,4 +78,3 @@ func mokaEmbeddingHandler(c *gin.Context, resp *http.Response) (*dto.OpenAIError
_, err = c.Writer.Write(jsonResponse)
return nil, &fullTextResponse.Usage
}
+3 -30
View File
@@ -1,12 +1,12 @@
package ollama
import (
"bytes"
"encoding/json"
"fmt"
"github.com/gin-gonic/gin"
"io"
"net/http"
"one-api/common"
"one-api/dto"
"one-api/service"
"strings"
@@ -88,10 +88,7 @@ func ollamaEmbeddingHandler(c *gin.Context, resp *http.Response, promptTokens in
if err != nil {
return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
}
err = resp.Body.Close()
if err != nil {
return service.OpenAIErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
}
common.CloseResponseBodyGracefully(resp)
err = json.Unmarshal(responseBody, &ollamaEmbeddingResponse)
if err != nil {
return service.OpenAIErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
@@ -120,31 +117,7 @@ func ollamaEmbeddingHandler(c *gin.Context, resp *http.Response, promptTokens in
if err != nil {
return service.OpenAIErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil
}
resp.Body = io.NopCloser(bytes.NewBuffer(doResponseBody))
// We shouldn't set the header before we parse the response body, because the parse part may fail.
// And then we will have to send an error response, but in this case, the header has already been set.
// So the httpClient will be confused by the response.
// For example, Postman will report error, and we cannot check the response at all.
// Copy headers
for k, v := range resp.Header {
// 删除任何现有的相同头部,以防止重复添加头部
c.Writer.Header().Del(k)
for _, vv := range v {
c.Writer.Header().Add(k, vv)
}
}
// reset content length
c.Writer.Header().Del("Content-Length")
c.Writer.Header().Set("Content-Length", fmt.Sprintf("%d", len(doResponseBody)))
c.Writer.WriteHeader(resp.StatusCode)
_, err = io.Copy(c.Writer, resp.Body)
if err != nil {
return service.OpenAIErrorWrapper(err, "copy_response_body_failed", http.StatusInternalServerError), nil
}
err = resp.Body.Close()
if err != nil {
return service.OpenAIErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
}
common.IOCopyBytesGracefully(c, resp, doResponseBody)
return nil, usage
}
+37 -99
View File
@@ -2,7 +2,6 @@ package openai
import (
"bytes"
"encoding/json"
"fmt"
"io"
"math"
@@ -34,7 +33,7 @@ func sendStreamData(c *gin.Context, info *relaycommon.RelayInfo, data string, fo
}
var lastStreamResponse dto.ChatCompletionsStreamResponse
if err := common.DecodeJsonStr(data, &lastStreamResponse); err != nil {
if err := common.UnmarshalJsonStr(data, &lastStreamResponse); err != nil {
return err
}
@@ -111,12 +110,13 @@ func OaiStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.Rel
return service.OpenAIErrorWrapper(fmt.Errorf("invalid response"), "invalid_response", http.StatusInternalServerError), nil
}
containStreamUsage := false
defer common.CloseResponseBodyGracefully(resp)
model := info.UpstreamModelName
var responseId string
var createAt int64 = 0
var systemFingerprint string
model := info.UpstreamModelName
var containStreamUsage bool
var responseTextBuilder strings.Builder
var toolCount int
var usage = &dto.Usage{}
@@ -148,31 +148,15 @@ func OaiStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.Rel
return true
})
// 处理最后的响应
shouldSendLastResp := true
var lastStreamResponse dto.ChatCompletionsStreamResponse
err := common.DecodeJsonStr(lastStreamData, &lastStreamResponse)
if err == nil {
responseId = lastStreamResponse.Id
createAt = lastStreamResponse.Created
systemFingerprint = lastStreamResponse.GetSystemFingerprint()
model = lastStreamResponse.Model
if service.ValidUsage(lastStreamResponse.Usage) {
containStreamUsage = true
usage = lastStreamResponse.Usage
if !info.ShouldIncludeUsage {
shouldSendLastResp = false
}
}
for _, choice := range lastStreamResponse.Choices {
if choice.FinishReason != nil {
shouldSendLastResp = true
}
}
if err := handleLastResponse(lastStreamData, &responseId, &createAt, &systemFingerprint, &model, &usage,
&containStreamUsage, info, &shouldSendLastResp); err != nil {
common.SysError("error handling last response: " + err.Error())
}
if shouldSendLastResp {
sendStreamData(c, info, lastStreamData, forceFormat, thinkToContent)
//err = handleStreamFormat(c, info, lastStreamData, forceFormat, thinkToContent)
if shouldSendLastResp && info.RelayFormat == relaycommon.RelayFormatOpenAI {
_ = sendStreamData(c, info, lastStreamData, forceFormat, thinkToContent)
}
// 处理token计算
@@ -197,16 +181,14 @@ func OaiStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.Rel
}
func OpenaiHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
defer common.CloseResponseBodyGracefully(resp)
var simpleResponse dto.OpenAITextResponse
responseBody, err := io.ReadAll(resp.Body)
if err != nil {
return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
}
err = resp.Body.Close()
if err != nil {
return service.OpenAIErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
}
err = common.DecodeJson(responseBody, &simpleResponse)
err = common.UnmarshalJson(responseBody, &simpleResponse)
if err != nil {
return service.OpenAIErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
}
@@ -238,7 +220,7 @@ func OpenaiHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayI
switch info.RelayFormat {
case relaycommon.RelayFormatOpenAI:
if forceFormat {
responseBody, err = json.Marshal(simpleResponse)
responseBody, err = common.EncodeJson(simpleResponse)
if err != nil {
return service.OpenAIErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil
}
@@ -247,29 +229,15 @@ func OpenaiHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayI
}
case relaycommon.RelayFormatClaude:
claudeResp := service.ResponseOpenAI2Claude(&simpleResponse, info)
claudeRespStr, err := json.Marshal(claudeResp)
claudeRespStr, err := common.EncodeJson(claudeResp)
if err != nil {
return service.OpenAIErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil
}
responseBody = claudeRespStr
}
// Reset response body
resp.Body = io.NopCloser(bytes.NewBuffer(responseBody))
// We shouldn't set the header before we parse the response body, because the parse part may fail.
// And then we will have to send an error response, but in this case, the header has already been set.
// So the httpClient will be confused by the response.
// For example, Postman will report error, and we cannot check the response at all.
for k, v := range resp.Header {
c.Writer.Header().Set(k, v[0])
}
c.Writer.WriteHeader(resp.StatusCode)
_, err = io.Copy(c.Writer, resp.Body)
if err != nil {
//return service.OpenAIErrorWrapper(err, "copy_response_body_failed", http.StatusInternalServerError), nil
common.SysError("error copying response body: " + err.Error())
}
resp.Body.Close()
common.IOCopyBytesGracefully(c, resp, responseBody)
return nil, &simpleResponse.Usage
}
@@ -280,7 +248,7 @@ func OpenaiTTSHandler(c *gin.Context, resp *http.Response, info *relaycommon.Rel
// if the upstream returns a specific status code, once the upstream has already written the header,
// the subsequent failure of the response body should be regarded as a non-recoverable error,
// and can be terminated directly.
defer resp.Body.Close()
defer common.CloseResponseBodyGracefully(resp)
usage := &dto.Usage{}
usage.PromptTokens = info.PromptTokens
usage.TotalTokens = info.PromptTokens
@@ -297,6 +265,8 @@ func OpenaiTTSHandler(c *gin.Context, resp *http.Response, info *relaycommon.Rel
}
func OpenaiSTTHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo, responseFormat string) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
defer common.CloseResponseBodyGracefully(resp)
// count tokens by audio file duration
audioTokens, err := countAudioTokens(c)
if err != nil {
@@ -306,25 +276,8 @@ func OpenaiSTTHandler(c *gin.Context, resp *http.Response, info *relaycommon.Rel
if err != nil {
return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
}
err = resp.Body.Close()
if err != nil {
return service.OpenAIErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
}
// Reset response body
resp.Body = io.NopCloser(bytes.NewBuffer(responseBody))
// We shouldn't set the header before we parse the response body, because the parse part may fail.
// And then we will have to send an error response, but in this case, the header has already been set.
// So the httpClient will be confused by the response.
// For example, Postman will report error, and we cannot check the response at all.
for k, v := range resp.Header {
c.Writer.Header().Set(k, v[0])
}
c.Writer.WriteHeader(resp.StatusCode)
_, err = io.Copy(c.Writer, resp.Body)
if err != nil {
return service.OpenAIErrorWrapper(err, "copy_response_body_failed", http.StatusInternalServerError), nil
}
resp.Body.Close()
// 写入新的 response body
common.IOCopyBytesGracefully(c, resp, responseBody)
usage := &dto.Usage{}
usage.PromptTokens = audioTokens
@@ -415,7 +368,7 @@ func OpenaiRealtimeHandler(c *gin.Context, info *relaycommon.RelayInfo) (*dto.Op
}
realtimeEvent := &dto.RealtimeEvent{}
err = json.Unmarshal(message, realtimeEvent)
err = common.UnmarshalJson(message, realtimeEvent)
if err != nil {
errChan <- fmt.Errorf("error unmarshalling message: %v", err)
return
@@ -475,7 +428,7 @@ func OpenaiRealtimeHandler(c *gin.Context, info *relaycommon.RelayInfo) (*dto.Op
}
info.SetFirstResponseTime()
realtimeEvent := &dto.RealtimeEvent{}
err = json.Unmarshal(message, realtimeEvent)
err = common.UnmarshalJson(message, realtimeEvent)
if err != nil {
errChan <- fmt.Errorf("error unmarshalling message: %v", err)
return
@@ -522,9 +475,9 @@ func OpenaiRealtimeHandler(c *gin.Context, info *relaycommon.RelayInfo) (*dto.Op
localUsage = &dto.RealtimeUsage{}
// print now usage
}
//common.LogInfo(c, fmt.Sprintf("realtime streaming sumUsage: %v", sumUsage))
//common.LogInfo(c, fmt.Sprintf("realtime streaming localUsage: %v", localUsage))
//common.LogInfo(c, fmt.Sprintf("realtime streaming localUsage: %v", localUsage))
common.LogInfo(c, fmt.Sprintf("realtime streaming sumUsage: %v", sumUsage))
common.LogInfo(c, fmt.Sprintf("realtime streaming localUsage: %v", localUsage))
common.LogInfo(c, fmt.Sprintf("realtime streaming localUsage: %v", localUsage))
} else if realtimeEvent.Type == dto.RealtimeEventTypeSessionUpdated || realtimeEvent.Type == dto.RealtimeEventTypeSessionCreated {
realtimeSession := realtimeEvent.Session
@@ -601,40 +554,25 @@ func preConsumeUsage(ctx *gin.Context, info *relaycommon.RelayInfo, usage *dto.R
}
func OpenaiHandlerWithUsage(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
defer common.CloseResponseBodyGracefully(resp)
responseBody, err := io.ReadAll(resp.Body)
if err != nil {
return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
}
err = resp.Body.Close()
var usageResp dto.SimpleResponse
err = common.UnmarshalJson(responseBody, &usageResp)
if err != nil {
return service.OpenAIErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
return service.OpenAIErrorWrapper(err, "parse_response_body_failed", http.StatusInternalServerError), nil
}
// Reset response body
resp.Body = io.NopCloser(bytes.NewBuffer(responseBody))
// We shouldn't set the header before we parse the response body, because the parse part may fail.
// And then we will have to send an error response, but in this case, the header has already been set.
// So the httpClient will be confused by the response.
// For example, Postman will report error, and we cannot check the response at all.
for k, v := range resp.Header {
c.Writer.Header().Set(k, v[0])
}
// reset content length
c.Writer.Header().Set("Content-Length", fmt.Sprintf("%d", len(responseBody)))
c.Writer.WriteHeader(resp.StatusCode)
_, err = io.Copy(c.Writer, resp.Body)
if err != nil {
common.SysError("error copying response body: " + err.Error())
}
_ = resp.Body.Close()
// 写入新的 response body
common.IOCopyBytesGracefully(c, resp, responseBody)
// Once we've written to the client, we should not return errors anymore
// because the upstream has already consumed resources and returned content
// We should still perform billing even if parsing fails
var usageResp dto.SimpleResponse
err = json.Unmarshal(responseBody, &usageResp)
if err != nil {
return service.OpenAIErrorWrapper(err, "parse_response_body_failed", http.StatusInternalServerError), nil
}
// format
if usageResp.InputTokens > 0 {
usageResp.PromptTokens += usageResp.InputTokens
+7 -23
View File
@@ -1,7 +1,6 @@
package openai
import (
"bytes"
"fmt"
"io"
"net/http"
@@ -16,17 +15,15 @@ import (
)
func OaiResponsesHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
defer common.CloseResponseBodyGracefully(resp)
// read response body
var responsesResponse dto.OpenAIResponsesResponse
responseBody, err := io.ReadAll(resp.Body)
if err != nil {
return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
}
err = resp.Body.Close()
if err != nil {
return service.OpenAIErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
}
err = common.DecodeJson(responseBody, &responsesResponse)
err = common.UnmarshalJson(responseBody, &responsesResponse)
if err != nil {
return service.OpenAIErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
}
@@ -41,22 +38,9 @@ func OaiResponsesHandler(c *gin.Context, resp *http.Response, info *relaycommon.
}, nil
}
// reset response body
resp.Body = io.NopCloser(bytes.NewBuffer(responseBody))
// We shouldn't set the header before we parse the response body, because the parse part may fail.
// And then we will have to send an error response, but in this case, the header has already been set.
// So the httpClient will be confused by the response.
// For example, Postman will report error, and we cannot check the response at all.
for k, v := range resp.Header {
c.Writer.Header().Set(k, v[0])
}
c.Writer.WriteHeader(resp.StatusCode)
// copy response body
_, err = io.Copy(c.Writer, resp.Body)
if err != nil {
common.SysError("error copying response body: " + err.Error())
}
resp.Body.Close()
// 写入新的 response body
common.IOCopyBytesGracefully(c, resp, responseBody)
// compute usage
usage := dto.Usage{}
usage.PromptTokens = responsesResponse.Usage.InputTokens
@@ -82,7 +66,7 @@ func OaiResponsesStreamHandler(c *gin.Context, resp *http.Response, info *relayc
// 检查当前数据是否包含 completed 状态和 usage 信息
var streamResponse dto.ResponsesStreamResponse
if err := common.DecodeJsonStr(data, &streamResponse); err == nil {
if err := common.UnmarshalJsonStr(data, &streamResponse); err == nil {
sendResponsesStreamData(c, streamResponse, data)
switch streamResponse.Type {
case "response.completed":
+3 -14
View File
@@ -83,12 +83,7 @@ func palmStreamHandler(c *gin.Context, resp *http.Response) (*dto.OpenAIErrorWit
stopChan <- true
return
}
err = resp.Body.Close()
if err != nil {
common.SysError("error closing stream response: " + err.Error())
stopChan <- true
return
}
common.CloseResponseBodyGracefully(resp)
var palmResponse PaLMChatResponse
err = json.Unmarshal(responseBody, &palmResponse)
if err != nil {
@@ -122,10 +117,7 @@ func palmStreamHandler(c *gin.Context, resp *http.Response) (*dto.OpenAIErrorWit
return false
}
})
err := resp.Body.Close()
if err != nil {
return service.OpenAIErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), ""
}
common.CloseResponseBodyGracefully(resp)
return nil, responseText
}
@@ -134,10 +126,7 @@ func palmHandler(c *gin.Context, resp *http.Response, promptTokens int, model st
if err != nil {
return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
}
err = resp.Body.Close()
if err != nil {
return service.OpenAIErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
}
common.CloseResponseBodyGracefully(resp)
var palmResponse PaLMChatResponse
err = json.Unmarshal(responseBody, &palmResponse)
if err != nil {
@@ -5,6 +5,7 @@ import (
"github.com/gin-gonic/gin"
"io"
"net/http"
"one-api/common"
"one-api/dto"
"one-api/service"
)
@@ -14,10 +15,7 @@ func siliconflowRerankHandler(c *gin.Context, resp *http.Response) (*dto.OpenAIE
if err != nil {
return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
}
err = resp.Body.Close()
if err != nil {
return service.OpenAIErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
}
common.CloseResponseBodyGracefully(resp)
var siliconflowResp SFRerankResponse
err = json.Unmarshal(responseBody, &siliconflowResp)
if err != nil {
+4 -4
View File
@@ -77,8 +77,8 @@ func (a *TaskAdaptor) Init(info *relaycommon.TaskRelayInfo) {
a.ChannelType = info.ChannelType
a.baseURL = info.BaseUrl
// apiKey format: "access_key,secret_key"
keyParts := strings.Split(info.ApiKey, ",")
// apiKey format: "access_key|secret_key"
keyParts := strings.Split(info.ApiKey, "|")
if len(keyParts) == 2 {
a.accessKey = strings.TrimSpace(keyParts[0])
a.secretKey = strings.TrimSpace(keyParts[1])
@@ -192,9 +192,9 @@ func (a *TaskAdaptor) FetchTask(baseUrl, key string, body map[string]any) (*http
req.Header.Set("Accept", "application/json")
req.Header.Set("Content-Type", "application/json")
keyParts := strings.Split(key, ",")
keyParts := strings.Split(key, "|")
if len(keyParts) != 2 {
return nil, fmt.Errorf("invalid api key format for jimeng: expected 'ak,sk'")
return nil, fmt.Errorf("invalid api key format for jimeng: expected 'ak|sk'")
}
accessKey := strings.TrimSpace(keyParts[0])
secretKey := strings.TrimSpace(keyParts[1])
+2 -8
View File
@@ -124,10 +124,7 @@ func tencentStreamHandler(c *gin.Context, resp *http.Response) (*dto.OpenAIError
helper.Done(c)
err := resp.Body.Close()
if err != nil {
return service.OpenAIErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), ""
}
common.CloseResponseBodyGracefully(resp)
return nil, responseText
}
@@ -138,10 +135,7 @@ func tencentHandler(c *gin.Context, resp *http.Response) (*dto.OpenAIErrorWithSt
if err != nil {
return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
}
err = resp.Body.Close()
if err != nil {
return service.OpenAIErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
}
common.CloseResponseBodyGracefully(resp)
err = json.Unmarshal(responseBody, &tencentSb)
if err != nil {
return service.OpenAIErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
+15 -3
View File
@@ -11,6 +11,7 @@ import (
"net/http"
"net/url"
relaycommon "one-api/relay/common"
"one-api/service"
"strings"
"fmt"
@@ -45,7 +46,7 @@ func getAccessToken(a *Adaptor, info *relaycommon.RelayInfo) (string, error) {
if err != nil {
return "", fmt.Errorf("failed to create signed JWT: %w", err)
}
newToken, err := exchangeJwtForAccessToken(signedJWT)
newToken, err := exchangeJwtForAccessToken(signedJWT, info)
if err != nil {
return "", fmt.Errorf("failed to exchange JWT for access token: %w", err)
}
@@ -96,14 +97,25 @@ func createSignedJWT(email, privateKeyPEM string) (string, error) {
return signedToken, nil
}
func exchangeJwtForAccessToken(signedJWT string) (string, error) {
func exchangeJwtForAccessToken(signedJWT string, info *relaycommon.RelayInfo) (string, error) {
authURL := "https://www.googleapis.com/oauth2/v4/token"
data := url.Values{}
data.Set("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer")
data.Set("assertion", signedJWT)
resp, err := http.PostForm(authURL, data)
var client *http.Client
var err error
if proxyURL, ok := info.ChannelSetting["proxy"]; ok {
client, err = service.NewProxyHttpClient(proxyURL.(string))
if err != nil {
return "", fmt.Errorf("new proxy http client failed: %w", err)
}
} else {
client = service.GetHttpClient()
}
resp, err := client.PostForm(authURL, data)
if err != nil {
return "", err
}
+8 -24
View File
@@ -1,9 +1,7 @@
package xai
import (
"bytes"
"encoding/json"
"github.com/gin-gonic/gin"
"io"
"net/http"
"one-api/common"
@@ -13,6 +11,8 @@ import (
"one-api/relay/helper"
"one-api/service"
"strings"
"github.com/gin-gonic/gin"
)
func streamResponseXAI2OpenAI(xAIResp *dto.ChatCompletionsStreamResponse, usage *dto.Usage) *dto.ChatCompletionsStreamResponse {
@@ -73,18 +73,16 @@ func xAIStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.Rel
}
helper.Done(c)
err := resp.Body.Close()
if err != nil {
//return service.OpenAIErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
common.SysError("close_response_body_failed: " + err.Error())
}
common.CloseResponseBodyGracefully(resp)
return nil, usage
}
func xAIHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
defer common.CloseResponseBodyGracefully(resp)
responseBody, err := io.ReadAll(resp.Body)
var response *dto.TextResponse
err = common.DecodeJson(responseBody, &response)
var response *dto.SimpleResponse
err = common.UnmarshalJson(responseBody, &response)
if err != nil {
common.SysError("error unmarshalling stream response: " + err.Error())
return nil, nil
@@ -99,21 +97,7 @@ func xAIHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo
return nil, nil
}
// set new body
resp.Body = io.NopCloser(bytes.NewBuffer(encodeJson))
for k, v := range resp.Header {
c.Writer.Header().Set(k, v[0])
}
c.Writer.WriteHeader(resp.StatusCode)
_, err = io.Copy(c.Writer, resp.Body)
if err != nil {
return service.OpenAIErrorWrapper(err, "copy_response_body_failed", http.StatusInternalServerError), nil
}
err = resp.Body.Close()
if err != nil {
return service.OpenAIErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
}
common.IOCopyBytesGracefully(c, resp, encodeJson)
return nil, &response.Usage
}
+2 -8
View File
@@ -210,10 +210,7 @@ func zhipuStreamHandler(c *gin.Context, resp *http.Response) (*dto.OpenAIErrorWi
return false
}
})
err := resp.Body.Close()
if err != nil {
return service.OpenAIErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
}
common.CloseResponseBodyGracefully(resp)
return nil, usage
}
@@ -223,10 +220,7 @@ func zhipuHandler(c *gin.Context, resp *http.Response) (*dto.OpenAIErrorWithStat
if err != nil {
return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
}
err = resp.Body.Close()
if err != nil {
return service.OpenAIErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
}
common.CloseResponseBodyGracefully(resp)
err = json.Unmarshal(responseBody, &zhipuResponse)
if err != nil {
return service.OpenAIErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
+3 -6
View File
@@ -16,17 +16,14 @@ func RerankHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Respo
if err != nil {
return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
}
err = resp.Body.Close()
if err != nil {
return service.OpenAIErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
}
common.CloseResponseBodyGracefully(resp)
if common.DebugEnabled {
println("reranker response body: ", string(responseBody))
}
var jinaResp dto.RerankResponse
if info.ChannelType == common.ChannelTypeXinference {
var xinRerankResponse xinference.XinRerankResponse
err = common.DecodeJson(responseBody, &xinRerankResponse)
err = common.UnmarshalJson(responseBody, &xinRerankResponse)
if err != nil {
return service.OpenAIErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
}
@@ -61,7 +58,7 @@ func RerankHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Respo
},
}
} else {
err = common.DecodeJson(responseBody, &jinaResp)
err = common.UnmarshalJson(responseBody, &jinaResp)
if err != nil {
return service.OpenAIErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
}
+22 -15
View File
@@ -20,8 +20,8 @@ import (
)
const (
InitialScannerBufferSize = 64 << 10 // 64KB (64*1024)
MaxScannerBufferSize = 10 << 20 // 10MB (10*1024*1024)
InitialScannerBufferSize = 64 << 10 // 64KB (64*1024)
MaxScannerBufferSize = 10 << 20 // 10MB (10*1024*1024)
DefaultPingInterval = 10 * time.Second
)
@@ -49,7 +49,7 @@ func StreamScannerHandler(c *gin.Context, resp *http.Response, info *relaycommon
scanner = bufio.NewScanner(resp.Body)
ticker = time.NewTicker(streamingTimeout)
pingTicker *time.Ticker
writeMutex sync.Mutex // Mutex to protect concurrent writes
writeMutex sync.Mutex // Mutex to protect concurrent writes
wg sync.WaitGroup // 用于等待所有 goroutine 退出
)
@@ -64,32 +64,39 @@ func StreamScannerHandler(c *gin.Context, resp *http.Response, info *relaycommon
pingTicker = time.NewTicker(pingInterval)
}
if common.DebugEnabled {
// print timeout and ping interval for debugging
println("relay timeout seconds:", common.RelayTimeout)
println("streaming timeout seconds:", int64(streamingTimeout.Seconds()))
println("ping interval seconds:", int64(pingInterval.Seconds()))
}
// 改进资源清理,确保所有 goroutine 正确退出
defer func() {
// 通知所有 goroutine 停止
common.SafeSendBool(stopChan, true)
ticker.Stop()
if pingTicker != nil {
pingTicker.Stop()
}
// 等待所有 goroutine 退出,最多等待5秒
done := make(chan struct{})
go func() {
wg.Wait()
close(done)
}()
select {
case <-done:
case <-time.After(5 * time.Second):
common.LogError(c, "timeout waiting for goroutines to exit")
}
close(stopChan)
}()
scanner.Buffer(make([]byte, InitialScannerBufferSize), MaxScannerBufferSize)
scanner.Split(bufio.ScanLines)
SetEventStreamHeaders(c)
@@ -113,12 +120,12 @@ func StreamScannerHandler(c *gin.Context, resp *http.Response, info *relaycommon
println("ping goroutine exited")
}
}()
// 添加超时保护,防止 goroutine 无限运行
maxPingDuration := 30 * time.Minute // 最大 ping 持续时间
pingTimeout := time.NewTimer(maxPingDuration)
defer pingTimeout.Stop()
for {
select {
case <-pingTicker.C:
@@ -129,7 +136,7 @@ func StreamScannerHandler(c *gin.Context, resp *http.Response, info *relaycommon
defer writeMutex.Unlock()
done <- PingData(c)
}()
select {
case err := <-done:
if err != nil {
@@ -175,7 +182,7 @@ func StreamScannerHandler(c *gin.Context, resp *http.Response, info *relaycommon
println("scanner goroutine exited")
}
}()
for scanner.Scan() {
// 检查是否需要停止
select {
@@ -187,7 +194,7 @@ func StreamScannerHandler(c *gin.Context, resp *http.Response, info *relaycommon
return
default:
}
ticker.Reset(streamingTimeout)
data := scanner.Text()
if common.DebugEnabled {
@@ -205,7 +212,7 @@ func StreamScannerHandler(c *gin.Context, resp *http.Response, info *relaycommon
data = strings.TrimSuffix(data, "\r")
if !strings.HasPrefix(data, "[DONE]") {
info.SetFirstResponseTime()
// 使用超时机制防止写操作阻塞
done := make(chan bool, 1)
go func() {
@@ -213,7 +220,7 @@ func StreamScannerHandler(c *gin.Context, resp *http.Response, info *relaycommon
defer writeMutex.Unlock()
done <- dataHandler(data)
}()
select {
case success := <-done:
if !success {
+1 -4
View File
@@ -279,10 +279,7 @@ func RelayMidjourneyTaskImageSeed(c *gin.Context) *dto.MidjourneyResponse {
if err != nil {
return service.MidjourneyErrorWrapper(constant.MjRequestError, "unmarshal_response_body_failed")
}
_, err = io.Copy(c.Writer, bytes.NewBuffer(respBody))
if err != nil {
return service.MidjourneyErrorWrapper(constant.MjRequestError, "copy_response_body_failed")
}
common.IOCopyBytesGracefully(c, nil, respBody)
return nil
}
+1 -4
View File
@@ -90,10 +90,7 @@ func RelayErrorHandler(resp *http.Response, showBodyWhenFail bool) (errWithStatu
if err != nil {
return
}
err = resp.Body.Close()
if err != nil {
return
}
common.CloseResponseBodyGracefully(resp)
var errResponse dto.GeneralErrorResponse
err = json.Unmarshal(responseBody, &errResponse)
if err != nil {
+1 -10
View File
@@ -13,9 +13,8 @@ import (
)
var httpClient *http.Client
var impatientHTTPClient *http.Client
func init() {
func InitHttpClient() {
if common.RelayTimeout == 0 {
httpClient = &http.Client{}
} else {
@@ -23,20 +22,12 @@ func init() {
Timeout: time.Duration(common.RelayTimeout) * time.Second,
}
}
impatientHTTPClient = &http.Client{
Timeout: 5 * time.Second,
}
}
func GetHttpClient() *http.Client {
return httpClient
}
func GetImpatientHttpClient() *http.Client {
return impatientHTTPClient
}
// NewProxyHttpClient 创建支持代理的 HTTP 客户端
func NewProxyHttpClient(proxyURL string) (*http.Client, error) {
if proxyURL == "" {
+1 -4
View File
@@ -228,10 +228,7 @@ func DoMidjourneyHttpRequest(c *gin.Context, timeout time.Duration, fullRequestU
if err != nil {
return MidjourneyErrorWithStatusCodeWrapper(constant.MjErrorUnknown, "read_response_body_failed", statusCode), nullBytes, err
}
err = resp.Body.Close()
if err != nil {
return MidjourneyErrorWithStatusCodeWrapper(constant.MjErrorUnknown, "close_response_body_failed", statusCode), responseBody, err
}
common.CloseResponseBodyGracefully(resp)
respStr := string(responseBody)
log.Printf("respStr: %s", respStr)
if respStr == "" {
+1 -1
View File
@@ -101,7 +101,7 @@ func SendWebhookNotify(webhookURL string, secret string, data dto.Notify) error
}
// 发送请求
client := GetImpatientHttpClient()
client := GetHttpClient()
resp, err = client.Do(req)
if err != nil {
return fmt.Errorf("failed to send webhook request: %v", err)
+5 -2
View File
@@ -509,8 +509,11 @@ func getHardcodedCompletionModelRatio(name string) (float64, bool) {
}
return 3.5 / 0.15, false
}
if strings.HasPrefix(name, "gemini-2.5-flash-lite-preview") {
return 4, true
if strings.HasPrefix(name, "gemini-2.5-flash-lite") {
if strings.HasPrefix(name, "gemini-2.5-flash-lite-preview") {
return 4, false
}
return 4, false
}
return 2.5 / 0.3, true
}
-2
View File
@@ -399,7 +399,6 @@ const LoginForm = () => {
placeholder={t('请输入您的用户名或邮箱地址')}
name="username"
size="large"
className="!rounded-md"
onChange={(value) => handleChange('username', value)}
prefix={<IconMail />}
/>
@@ -411,7 +410,6 @@ const LoginForm = () => {
name="password"
mode="password"
size="large"
className="!rounded-md"
onChange={(value) => handleChange('password', value)}
prefix={<IconLock />}
/>
@@ -113,7 +113,6 @@ const PasswordResetConfirm = () => {
label={t('邮箱')}
name="email"
size="large"
className="!rounded-md"
disabled={true}
prefix={<IconMail />}
placeholder={email ? '' : t('等待获取邮箱信息...')}
@@ -125,7 +124,6 @@ const PasswordResetConfirm = () => {
label={t('新密码')}
name="newPassword"
size="large"
className="!rounded-md"
disabled={true}
prefix={<IconLock />}
suffix={
@@ -102,7 +102,6 @@ const PasswordResetForm = () => {
placeholder={t('请输入您的邮箱地址')}
name="email"
size="large"
className="!rounded-md"
value={email}
onChange={handleChange}
prefix={<IconMail />}
-6
View File
@@ -394,7 +394,6 @@ const RegisterForm = () => {
placeholder={t('请输入用户名')}
name="username"
size="large"
className="!rounded-md"
onChange={(value) => handleChange('username', value)}
prefix={<IconUser />}
/>
@@ -406,7 +405,6 @@ const RegisterForm = () => {
name="password"
mode="password"
size="large"
className="!rounded-md"
onChange={(value) => handleChange('password', value)}
prefix={<IconLock />}
/>
@@ -418,7 +416,6 @@ const RegisterForm = () => {
name="password2"
mode="password"
size="large"
className="!rounded-md"
onChange={(value) => handleChange('password2', value)}
prefix={<IconLock />}
/>
@@ -432,7 +429,6 @@ const RegisterForm = () => {
name="email"
type="email"
size="large"
className="!rounded-md"
onChange={(value) => handleChange('email', value)}
prefix={<IconMail />}
suffix={
@@ -440,7 +436,6 @@ const RegisterForm = () => {
onClick={sendVerificationCode}
loading={verificationCodeLoading}
size="small"
className="!rounded-md mr-2"
>
{t('获取验证码')}
</Button>
@@ -452,7 +447,6 @@ const RegisterForm = () => {
placeholder={t('输入验证码')}
name="verification_code"
size="large"
className="!rounded-md"
onChange={(value) => handleChange('verification_code', value)}
prefix={<IconKey />}
/>
+2 -2
View File
@@ -170,8 +170,8 @@ const NoticeModal = ({ visible, onClose, isMobile, defaultTab = 'inApp', unreadK
onCancel={onClose}
footer={(
<div className="flex justify-end">
<Button type='secondary' className='!rounded-full' onClick={handleCloseTodayNotice}>{t('今日关闭')}</Button>
<Button type="primary" className='!rounded-full' onClick={onClose}>{t('关闭公告')}</Button>
<Button type='secondary' onClick={handleCloseTodayNotice}>{t('今日关闭')}</Button>
<Button type="primary" onClick={onClose}>{t('关闭公告')}</Button>
</div>
)}
size={isMobile ? 'full-width' : 'large'}
+25 -65
View File
@@ -10,16 +10,6 @@ import {
getChannelIcon,
renderQuotaWithAmount
} from '../../helpers/index.js';
import {
CheckCircle,
XCircle,
AlertCircle,
HelpCircle,
Coins,
Tags,
} from 'lucide-react';
import { CHANNEL_OPTIONS, ITEMS_PER_PAGE, MODEL_TABLE_PAGE_SIZE } from '../../constants/index.js';
import {
Button,
@@ -50,12 +40,8 @@ import {
import EditChannel from '../../pages/Channel/EditChannel.js';
import {
IconTreeTriangleDown,
IconPlus,
IconSearch,
IconDelete,
IconMore,
IconCopy,
IconSmallTriangleRight
} from '@douyinfe/semi-icons';
import { loadChannelModels, isMobile, copy } from '../../helpers';
import EditTagModal from '../../pages/Channel/EditTagModal.js';
@@ -91,7 +77,6 @@ const ChannelsTable = () => {
return (
<Tag
color='light-blue'
prefixIcon={<Tags size={14} />}
size='large'
shape='circle'
type='light'
@@ -105,25 +90,25 @@ const ChannelsTable = () => {
switch (status) {
case 1:
return (
<Tag size='large' color='green' shape='circle' prefixIcon={<CheckCircle size={14} />}>
<Tag size='large' color='green' shape='circle'>
{t('已启用')}
</Tag>
);
case 2:
return (
<Tag size='large' color='red' shape='circle' prefixIcon={<XCircle size={14} />}>
<Tag size='large' color='red' shape='circle'>
{t('已禁用')}
</Tag>
);
case 3:
return (
<Tag size='large' color='yellow' shape='circle' prefixIcon={<AlertCircle size={14} />}>
<Tag size='large' color='yellow' shape='circle'>
{t('自动禁用')}
</Tag>
);
default:
return (
<Tag size='large' color='grey' shape='circle' prefixIcon={<HelpCircle size={14} />}>
<Tag size='large' color='grey' shape='circle'>
{t('未知状态')}
</Tag>
);
@@ -346,7 +331,7 @@ const ChannelsTable = () => {
<div>
<Space spacing={1}>
<Tooltip content={t('已用额度')}>
<Tag color='white' type='ghost' size='large' shape='circle' prefixIcon={<Coins size={14} />}>
<Tag color='white' type='ghost' size='large' shape='circle'>
{renderQuota(record.used_quota)}
</Tag>
</Tooltip>
@@ -356,7 +341,6 @@ const ChannelsTable = () => {
type='ghost'
size='large'
shape='circle'
prefixIcon={<Coins size={14} />}
onClick={() => updateChannelBalance(record)}
>
{renderQuotaWithAmount(record.balance)}
@@ -368,7 +352,7 @@ const ChannelsTable = () => {
} else {
return (
<Tooltip content={t('已用额度')}>
<Tag color='white' type='ghost' size='large' shape='circle' prefixIcon={<Coins size={14} />}>
<Tag color='white' type='ghost' size='large' shape='circle'>
{renderQuota(record.used_quota)}
</Tag>
</Tooltip>
@@ -492,7 +476,6 @@ const ChannelsTable = () => {
{
node: 'item',
name: t('删除'),
icon: <IconDelete />,
type: 'danger',
onClick: () => {
Modal.confirm({
@@ -509,7 +492,6 @@ const ChannelsTable = () => {
{
node: 'item',
name: t('复制'),
icon: <IconCopy />,
type: 'primary',
onClick: () => {
Modal.confirm({
@@ -524,7 +506,7 @@ const ChannelsTable = () => {
return (
<Space wrap>
<SplitButtonGroup
className="!rounded-full overflow-hidden"
className="overflow-hidden"
aria-label={t('测试单个渠道操作项目组')}
>
<Button
@@ -550,7 +532,6 @@ const ChannelsTable = () => {
theme='light'
type='warning'
size="small"
className="!rounded-full"
onClick={() => manageChannel(record.id, 'disable', record)}
>
{t('禁用')}
@@ -560,7 +541,6 @@ const ChannelsTable = () => {
theme='light'
type='secondary'
size="small"
className="!rounded-full"
onClick={() => manageChannel(record.id, 'enable', record)}
>
{t('启用')}
@@ -571,7 +551,6 @@ const ChannelsTable = () => {
theme='light'
type='tertiary'
size="small"
className="!rounded-full"
onClick={() => {
setEditingChannel(record);
setShowEdit(true);
@@ -590,7 +569,6 @@ const ChannelsTable = () => {
theme='light'
type='tertiary'
size="small"
className="!rounded-full"
/>
</Dropdown>
</Space>
@@ -603,7 +581,6 @@ const ChannelsTable = () => {
theme='light'
type='secondary'
size="small"
className="!rounded-full"
onClick={() => manageTag(record.key, 'enable')}
>
{t('启用全部')}
@@ -612,7 +589,6 @@ const ChannelsTable = () => {
theme='light'
type='warning'
size="small"
className="!rounded-full"
onClick={() => manageTag(record.key, 'disable')}
>
{t('禁用全部')}
@@ -621,7 +597,6 @@ const ChannelsTable = () => {
theme='light'
type='tertiary'
size="small"
className="!rounded-full"
onClick={() => {
setShowEditTag(true);
setEditingTag(record.key);
@@ -694,21 +669,18 @@ const ChannelsTable = () => {
<Button
theme="light"
onClick={() => initDefaultColumns()}
className="!rounded-full"
>
{t('重置')}
</Button>
<Button
theme="light"
onClick={() => setShowColumnSelector(false)}
className="!rounded-full"
>
{t('取消')}
</Button>
<Button
type='primary'
onClick={() => setShowColumnSelector(false)}
className="!rounded-full"
>
{t('确定')}
</Button>
@@ -1508,7 +1480,7 @@ const ChannelsTable = () => {
disabled={!enableBatchDelete}
theme='light'
type='danger'
className="!rounded-full w-full md:w-auto"
className="w-full md:w-auto"
onClick={() => {
Modal.confirm({
title: t('确定是否要删除所选通道?'),
@@ -1526,7 +1498,7 @@ const ChannelsTable = () => {
theme='light'
type='primary'
onClick={() => setShowBatchSetTag(true)}
className="!rounded-full w-full md:w-auto"
className="w-full md:w-auto"
>
{t('批量设置标签')}
</Button>
@@ -1541,7 +1513,7 @@ const ChannelsTable = () => {
size='small'
theme='light'
type='warning'
className="!rounded-full w-full"
className="w-full"
onClick={() => {
Modal.confirm({
title: t('确定?'),
@@ -1560,7 +1532,7 @@ const ChannelsTable = () => {
size='small'
theme='light'
type='secondary'
className="!rounded-full w-full"
className="w-full"
onClick={() => {
Modal.confirm({
title: t('确定?'),
@@ -1579,7 +1551,7 @@ const ChannelsTable = () => {
size='small'
theme='light'
type='danger'
className="!rounded-full w-full"
className="w-full"
onClick={() => {
Modal.confirm({
title: t('确定是否要删除禁用通道?'),
@@ -1598,7 +1570,7 @@ const ChannelsTable = () => {
size='small'
theme='light'
type='tertiary'
className="!rounded-full w-full"
className="w-full"
onClick={() => {
Modal.confirm({
title: t('确定是否要修复数据库一致性?'),
@@ -1615,7 +1587,7 @@ const ChannelsTable = () => {
</Dropdown.Menu>
}
>
<Button size='small' theme='light' type='tertiary' className="!rounded-full w-full md:w-auto">
<Button size='small' theme='light' type='tertiary' className="w-full md:w-auto">
{t('批量操作')}
</Button>
</Dropdown>
@@ -1624,7 +1596,7 @@ const ChannelsTable = () => {
size='small'
theme='light'
type='secondary'
className="!rounded-full w-full md:w-auto"
className="w-full md:w-auto"
onClick={() => setCompactMode(!compactMode)}
>
{compactMode ? t('自适应列表') : t('紧凑列表')}
@@ -1713,8 +1685,7 @@ const ChannelsTable = () => {
size='small'
theme='light'
type='primary'
icon={<IconPlus />}
className="!rounded-full w-full md:w-auto"
className="w-full md:w-auto"
onClick={() => {
setEditingChannel({
id: undefined,
@@ -1729,7 +1700,7 @@ const ChannelsTable = () => {
size='small'
theme='light'
type='primary'
className="!rounded-full w-full md:w-auto"
className="w-full md:w-auto"
onClick={refresh}
>
{t('刷新')}
@@ -1740,7 +1711,7 @@ const ChannelsTable = () => {
theme='light'
type='tertiary'
onClick={() => setShowColumnSelector(true)}
className="!rounded-full w-full md:w-auto"
className="w-full md:w-auto"
>
{t('列设置')}
</Button>
@@ -1764,7 +1735,6 @@ const ChannelsTable = () => {
field="searchKeyword"
prefix={<IconSearch />}
placeholder={t('渠道ID,名称,密钥,API地址')}
className="!rounded-full"
showClear
pure
/>
@@ -1775,7 +1745,6 @@ const ChannelsTable = () => {
field="searchModel"
prefix={<IconSearch />}
placeholder={t('模型关键字')}
className="!rounded-full"
showClear
pure
/>
@@ -1789,7 +1758,7 @@ const ChannelsTable = () => {
{ label: t('选择分组'), value: null },
...groupOptions,
]}
className="!rounded-full w-full"
className="w-full"
showClear
pure
onChange={() => {
@@ -1805,7 +1774,7 @@ const ChannelsTable = () => {
type="primary"
htmlType="submit"
loading={loading || searching}
className="!rounded-full w-full md:w-auto"
className="w-full md:w-auto"
>
{t('查询')}
</Button>
@@ -1821,7 +1790,7 @@ const ChannelsTable = () => {
}, 100);
}
}}
className="!rounded-full w-full md:w-auto"
className="w-full md:w-auto"
>
{t('重置')}
</Button>
@@ -1917,7 +1886,6 @@ const ChannelsTable = () => {
value={batchSetTagValue}
onChange={(v) => setBatchSetTagValue(v)}
size='large'
className="!rounded-full"
/>
<div className="mt-4">
<Typography.Text type='secondary'>
@@ -1949,15 +1917,13 @@ const ChannelsTable = () => {
setModelSearchKeyword(v);
setModelTablePage(1);
}}
className="!w-full !rounded-full"
className="!w-full"
prefix={<IconSearch />}
showClear
/>
<Button
theme='light'
icon={<IconCopy />}
className="!rounded-full"
onClick={() => {
if (selectedModelKeys.length === 0) {
showError(t('请先选择模型!'));
@@ -1978,7 +1944,6 @@ const ChannelsTable = () => {
<Button
theme='light'
type='primary'
className="!rounded-full"
onClick={() => {
if (!currentTestChannel) return;
const successKeys = currentTestChannel.models
@@ -2008,7 +1973,6 @@ const ChannelsTable = () => {
<Button
theme='light'
type='warning'
className="!rounded-full"
onClick={handleCloseModal}
>
{t('停止测试')}
@@ -2017,7 +1981,6 @@ const ChannelsTable = () => {
<Button
theme='light'
type='tertiary'
className="!rounded-full"
onClick={handleCloseModal}
>
{t('取消')}
@@ -2026,7 +1989,6 @@ const ChannelsTable = () => {
<Button
theme='light'
type='primary'
className="!rounded-full"
onClick={batchTestModels}
loading={isBatchTesting}
disabled={isBatchTesting}
@@ -2071,7 +2033,7 @@ const ChannelsTable = () => {
if (isTesting) {
return (
<Tag size='large' color='blue' className="!rounded-full">
<Tag size='large' color='blue' shape='circle'>
{t('测试中')}
</Tag>
);
@@ -2079,7 +2041,7 @@ const ChannelsTable = () => {
if (!testResult) {
return (
<Tag size='large' color='grey' className="!rounded-full">
<Tag size='large' color='grey' shape='circle'>
{t('未开始')}
</Tag>
);
@@ -2090,7 +2052,7 @@ const ChannelsTable = () => {
<Tag
size='large'
color={testResult.success ? 'green' : 'red'}
className="!rounded-full"
shape='circle'
>
{testResult.success ? t('成功') : t('失败')}
</Tag>
@@ -2112,11 +2074,9 @@ const ChannelsTable = () => {
<Button
theme='light'
type='primary'
className="!rounded-full"
onClick={() => testChannel(currentTestChannel, record.model)}
loading={isTesting}
size='small'
icon={<IconSmallTriangleRight />}
>
{t('测试')}
</Button>
+6 -19
View File
@@ -47,7 +47,7 @@ import {
} from '@douyinfe/semi-illustrations';
import { ITEMS_PER_PAGE } from '../../constants';
import Paragraph from '@douyinfe/semi-ui/lib/es/typography/paragraph';
import { IconSetting, IconSearch, IconHelpCircle, IconDescend } from '@douyinfe/semi-icons';
import { IconSearch, IconHelpCircle } from '@douyinfe/semi-icons';
import { Route } from 'lucide-react';
import { useTableCompactMode } from '../../hooks/useTableCompactMode';
@@ -696,21 +696,18 @@ const LogsTable = () => {
<Button
theme='light'
onClick={() => initDefaultColumns()}
className='!rounded-full'
>
{t('重置')}
</Button>
<Button
theme='light'
onClick={() => setShowColumnSelector(false)}
className='!rounded-full'
>
{t('取消')}
</Button>
<Button
type='primary'
onClick={() => setShowColumnSelector(false)}
className='!rounded-full'
>
{t('确定')}
</Button>
@@ -1221,10 +1218,10 @@ const LogsTable = () => {
size='large'
style={{
padding: 15,
borderRadius: '9999px',
fontWeight: 500,
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
}}
className='!rounded-lg'
>
{t('消耗额度')}: {renderQuota(stat.quota)}
</Tag>
@@ -1233,10 +1230,10 @@ const LogsTable = () => {
size='large'
style={{
padding: 15,
borderRadius: '9999px',
fontWeight: 500,
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
}}
className='!rounded-lg'
>
RPM: {stat.rpm}
</Tag>
@@ -1247,9 +1244,9 @@ const LogsTable = () => {
padding: 15,
border: 'none',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
borderRadius: '9999px',
fontWeight: 500,
}}
className='!rounded-lg'
>
TPM: {stat.tpm}
</Tag>
@@ -1258,8 +1255,7 @@ const LogsTable = () => {
<Button
theme='light'
type='secondary'
icon={<IconDescend />}
className="!rounded-full w-full md:w-auto"
className="w-full md:w-auto"
onClick={() => setCompactMode(!compactMode)}
>
{compactMode ? t('自适应列表') : t('紧凑列表')}
@@ -1299,7 +1295,6 @@ const LogsTable = () => {
field='token_name'
prefix={<IconSearch />}
placeholder={t('令牌名称')}
className='!rounded-full'
showClear
pure
/>
@@ -1308,7 +1303,6 @@ const LogsTable = () => {
field='model_name'
prefix={<IconSearch />}
placeholder={t('模型名称')}
className='!rounded-full'
showClear
pure
/>
@@ -1317,7 +1311,6 @@ const LogsTable = () => {
field='group'
prefix={<IconSearch />}
placeholder={t('分组')}
className='!rounded-full'
showClear
pure
/>
@@ -1328,7 +1321,6 @@ const LogsTable = () => {
field='channel'
prefix={<IconSearch />}
placeholder={t('渠道 ID')}
className='!rounded-full'
showClear
pure
/>
@@ -1336,7 +1328,6 @@ const LogsTable = () => {
field='username'
prefix={<IconSearch />}
placeholder={t('用户名称')}
className='!rounded-full'
showClear
pure
/>
@@ -1351,7 +1342,7 @@ const LogsTable = () => {
<Form.Select
field='logType'
placeholder={t('日志类型')}
className='!rounded-full w-full sm:w-auto min-w-[120px]'
className='w-full sm:w-auto min-w-[120px]'
showClear
pure
onChange={() => {
@@ -1387,7 +1378,6 @@ const LogsTable = () => {
type='primary'
htmlType='submit'
loading={loading}
className='!rounded-full'
>
{t('查询')}
</Button>
@@ -1402,16 +1392,13 @@ const LogsTable = () => {
}, 100);
}
}}
className='!rounded-full'
>
{t('重置')}
</Button>
<Button
theme='light'
type='tertiary'
icon={<IconSetting />}
onClick={() => setShowColumnSelector(true)}
className='!rounded-full'
>
{t('列设置')}
</Button>
+1 -14
View File
@@ -59,8 +59,6 @@ import { ITEMS_PER_PAGE } from '../../constants';
import {
IconEyeOpened,
IconSearch,
IconSetting,
IconDescend
} from '@douyinfe/semi-icons';
import { useTableCompactMode } from '../../hooks/useTableCompactMode';
@@ -517,7 +515,6 @@ const LogsTable = () => {
setModalImageUrl(text);
setIsModalOpenurl(true);
}}
className="!rounded-full"
>
{t('查看图片')}
</Button>
@@ -735,21 +732,18 @@ const LogsTable = () => {
<Button
theme="light"
onClick={() => initDefaultColumns()}
className="!rounded-full"
>
{t('重置')}
</Button>
<Button
theme="light"
onClick={() => setShowColumnSelector(false)}
className="!rounded-full"
>
{t('取消')}
</Button>
<Button
type='primary'
onClick={() => setShowColumnSelector(false)}
className="!rounded-full"
>
{t('确定')}
</Button>
@@ -827,8 +821,7 @@ const LogsTable = () => {
<Button
theme='light'
type='secondary'
icon={<IconDescend />}
className="!rounded-full w-full md:w-auto"
className="w-full md:w-auto"
onClick={() => setCompactMode(!compactMode)}
>
{compactMode ? t('自适应列表') : t('紧凑列表')}
@@ -867,7 +860,6 @@ const LogsTable = () => {
field='mj_id'
prefix={<IconSearch />}
placeholder={t('任务 ID')}
className="!rounded-full"
showClear
pure
/>
@@ -878,7 +870,6 @@ const LogsTable = () => {
field='channel_id'
prefix={<IconSearch />}
placeholder={t('渠道 ID')}
className="!rounded-full"
showClear
pure
/>
@@ -893,7 +884,6 @@ const LogsTable = () => {
type='primary'
htmlType='submit'
loading={loading}
className="!rounded-full"
>
{t('查询')}
</Button>
@@ -908,16 +898,13 @@ const LogsTable = () => {
}, 100);
}
}}
className="!rounded-full"
>
{t('重置')}
</Button>
<Button
theme='light'
type='tertiary'
icon={<IconSetting />}
onClick={() => setShowColumnSelector(true)}
className="!rounded-full"
>
{t('列设置')}
</Button>
+3 -10
View File
@@ -161,6 +161,7 @@ const ModelPricing = () => {
<Tag
color='blue'
size='large'
shape='circle'
onClick={() => {
setSelectedGroup(group);
showInfo(
@@ -170,7 +171,7 @@ const ModelPricing = () => {
}),
);
}}
className="cursor-pointer hover:opacity-80 transition-opacity !rounded-full"
className="cursor-pointer hover:opacity-80 transition-opacity"
>
{group}
</Tag>
@@ -404,7 +405,6 @@ const ModelPricing = () => {
<Input
prefix={<IconSearch />}
placeholder={t('模糊搜索模型名称')}
className="!rounded-lg"
onCompositionStart={handleCompositionStart}
onCompositionEnd={handleCompositionEnd}
onChange={handleChange}
@@ -418,7 +418,7 @@ const ModelPricing = () => {
icon={<IconCopy />}
onClick={() => copyText(selectedRowKeys)}
disabled={selectedRowKeys.length === 0}
className="!rounded-lg !bg-blue-500 hover:!bg-blue-600 text-white"
className="!bg-blue-500 hover:!bg-blue-600 text-white"
size="large"
>
{t('复制选中模型')}
@@ -477,13 +477,6 @@ const ModelPricing = () => {
}}
bodyStyle={{ padding: 0 }}
>
{/* 装饰性背景元素 */}
<div className="absolute inset-0 overflow-hidden">
<div className="absolute -top-10 -right-10 w-40 h-40 bg-white opacity-5 rounded-full"></div>
<div className="absolute -bottom-16 -left-16 w-48 h-48 bg-white opacity-3 rounded-full"></div>
<div className="absolute top-1/2 right-1/4 w-24 h-24 bg-yellow-400 opacity-10 rounded-full"></div>
</div>
<div className="relative p-6 sm:p-8" style={{ color: 'white' }}>
<div className="flex flex-col lg:flex-row lg:items-start lg:justify-between gap-4 lg:gap-6">
<div className="flex items-start">
+14 -44
View File
@@ -8,14 +8,7 @@ import {
renderQuota
} from '../../helpers';
import {
CheckCircle,
XCircle,
Minus,
HelpCircle,
Coins,
Ticket
} from 'lucide-react';
import { Ticket } from 'lucide-react';
import { ITEMS_PER_PAGE } from '../../constants';
import {
@@ -37,16 +30,8 @@ import {
IllustrationNoResultDark
} from '@douyinfe/semi-illustrations';
import {
IconPlus,
IconCopy,
IconSearch,
IconEyeOpened,
IconEdit,
IconDelete,
IconStop,
IconPlay,
IconMore,
IconDescend
} from '@douyinfe/semi-icons';
import EditRedemption from '../../pages/Redemption/EditRedemption';
import { useTranslation } from 'react-i18next';
@@ -68,31 +53,31 @@ const RedemptionsTable = () => {
const renderStatus = (status, record) => {
if (isExpired(record)) {
return (
<Tag color='orange' size='large' shape='circle' prefixIcon={<Minus size={14} />}>{t('已过期')}</Tag>
<Tag color='orange' size='large' shape='circle'>{t('已过期')}</Tag>
);
}
switch (status) {
case 1:
return (
<Tag color='green' size='large' shape='circle' prefixIcon={<CheckCircle size={14} />}>
<Tag color='green' size='large' shape='circle'>
{t('未使用')}
</Tag>
);
case 2:
return (
<Tag color='red' size='large' shape='circle' prefixIcon={<XCircle size={14} />}>
<Tag color='red' size='large' shape='circle'>
{t('已禁用')}
</Tag>
);
case 3:
return (
<Tag color='grey' size='large' shape='circle' prefixIcon={<Minus size={14} />}>
<Tag color='grey' size='large' shape='circle'>
{t('已使用')}
</Tag>
);
default:
return (
<Tag color='black' size='large' shape='circle' prefixIcon={<HelpCircle size={14} />}>
<Tag color='black' size='large' shape='circle'>
{t('未知状态')}
</Tag>
);
@@ -122,7 +107,7 @@ const RedemptionsTable = () => {
render: (text, record, index) => {
return (
<div>
<Tag size={'large'} color={'grey'} shape='circle' prefixIcon={<Coins size={14} />}>
<Tag size={'large'} color={'grey'} shape='circle'>
{renderQuota(parseInt(text))}
</Tag>
</div>
@@ -160,7 +145,6 @@ const RedemptionsTable = () => {
{
node: 'item',
name: t('删除'),
icon: <IconDelete />,
type: 'danger',
onClick: () => {
Modal.confirm({
@@ -180,7 +164,6 @@ const RedemptionsTable = () => {
moreMenuItems.push({
node: 'item',
name: t('禁用'),
icon: <IconStop />,
type: 'warning',
onClick: () => {
manageRedemption(record.id, 'disable', record);
@@ -190,7 +173,6 @@ const RedemptionsTable = () => {
moreMenuItems.push({
node: 'item',
name: t('启用'),
icon: <IconPlay />,
type: 'secondary',
onClick: () => {
manageRedemption(record.id, 'enable', record);
@@ -203,21 +185,17 @@ const RedemptionsTable = () => {
<Space>
<Popover content={record.key} style={{ padding: 20 }} position='top'>
<Button
icon={<IconEyeOpened />}
theme='light'
type='tertiary'
size="small"
className="!rounded-full"
>
{t('查看')}
</Button>
</Popover>
<Button
icon={<IconCopy />}
theme='light'
type='secondary'
size="small"
className="!rounded-full"
onClick={async () => {
await copyText(record.key);
}}
@@ -225,11 +203,9 @@ const RedemptionsTable = () => {
{t('复制')}
</Button>
<Button
icon={<IconEdit />}
theme='light'
type='tertiary'
size="small"
className="!rounded-full"
onClick={() => {
setEditingRedemption(record);
setShowEdit(true);
@@ -244,11 +220,10 @@ const RedemptionsTable = () => {
menu={moreMenuItems}
>
<Button
icon={<IconMore />}
theme='light'
type='tertiary'
size="small"
className="!rounded-full"
icon={<IconMore />}
/>
</Dropdown>
</Space>
@@ -451,8 +426,7 @@ const RedemptionsTable = () => {
<Button
theme='light'
type='secondary'
icon={<IconDescend />}
className="!rounded-full w-full md:w-auto"
className="w-full md:w-auto"
onClick={() => setCompactMode(!compactMode)}
>
{compactMode ? t('自适应列表') : t('紧凑列表')}
@@ -468,8 +442,7 @@ const RedemptionsTable = () => {
<Button
theme='light'
type='primary'
icon={<IconPlus />}
className="!rounded-full w-full sm:w-auto"
className="w-full sm:w-auto"
onClick={() => {
setEditingRedemption({
id: undefined,
@@ -481,8 +454,7 @@ const RedemptionsTable = () => {
</Button>
<Button
type='warning'
icon={<IconCopy />}
className="!rounded-full w-full sm:w-auto"
className="w-full sm:w-auto"
onClick={async () => {
if (selectedKeys.length === 0) {
showError(t('请至少选择一个兑换码!'));
@@ -501,8 +473,7 @@ const RedemptionsTable = () => {
</div>
<Button
type='danger'
icon={<IconDelete />}
className="!rounded-full w-full sm:w-auto"
className="w-full sm:w-auto"
onClick={() => {
Modal.confirm({
title: t('确定清除所有失效兑换码?'),
@@ -546,7 +517,6 @@ const RedemptionsTable = () => {
field="searchKeyword"
prefix={<IconSearch />}
placeholder={t('关键字(id或者名称)')}
className="!rounded-full"
showClear
pure
/>
@@ -556,7 +526,7 @@ const RedemptionsTable = () => {
type="primary"
htmlType="submit"
loading={loading || searching}
className="!rounded-full flex-1 md:flex-initial md:w-auto"
className="flex-1 md:flex-initial md:w-auto"
>
{t('查询')}
</Button>
@@ -572,7 +542,7 @@ const RedemptionsTable = () => {
}, 100);
}
}}
className="!rounded-full flex-1 md:flex-initial md:w-auto"
className="flex-1 md:flex-initial md:w-auto"
>
{t('重置')}
</Button>
+1 -13
View File
@@ -47,8 +47,6 @@ import { ITEMS_PER_PAGE } from '../../constants';
import {
IconEyeOpened,
IconSearch,
IconSetting,
IconDescend
} from '@douyinfe/semi-icons';
import { useTableCompactMode } from '../../hooks/useTableCompactMode';
@@ -600,21 +598,18 @@ const LogsTable = () => {
<Button
theme="light"
onClick={() => initDefaultColumns()}
className="!rounded-full"
>
{t('重置')}
</Button>
<Button
theme="light"
onClick={() => setShowColumnSelector(false)}
className="!rounded-full"
>
{t('取消')}
</Button>
<Button
type='primary'
onClick={() => setShowColumnSelector(false)}
className="!rounded-full"
>
{t('确定')}
</Button>
@@ -684,8 +679,7 @@ const LogsTable = () => {
<Button
theme='light'
type='secondary'
icon={<IconDescend />}
className="!rounded-full w-full md:w-auto"
className="w-full md:w-auto"
onClick={() => setCompactMode(!compactMode)}
>
{compactMode ? t('自适应列表') : t('紧凑列表')}
@@ -724,7 +718,6 @@ const LogsTable = () => {
field='task_id'
prefix={<IconSearch />}
placeholder={t('任务 ID')}
className="!rounded-full"
showClear
pure
/>
@@ -735,7 +728,6 @@ const LogsTable = () => {
field='channel_id'
prefix={<IconSearch />}
placeholder={t('渠道 ID')}
className="!rounded-full"
showClear
pure
/>
@@ -750,7 +742,6 @@ const LogsTable = () => {
type='primary'
htmlType='submit'
loading={loading}
className="!rounded-full"
>
{t('查询')}
</Button>
@@ -765,16 +756,13 @@ const LogsTable = () => {
}, 100);
}
}}
className="!rounded-full"
>
{t('重置')}
</Button>
<Button
theme='light'
type='tertiary'
icon={<IconSetting />}
onClick={() => setShowColumnSelector(true)}
className="!rounded-full"
>
{t('列设置')}
</Button>
+17 -56
View File
@@ -1,4 +1,4 @@
import React, { useEffect, useState, useMemo } from 'react';
import React, { useEffect, useState } from 'react';
import {
API,
copy,
@@ -9,7 +9,6 @@ import {
renderQuota,
getQuotaPerUnit
} from '../../helpers';
import { ITEMS_PER_PAGE } from '../../constants';
import {
Button,
@@ -29,32 +28,12 @@ import {
IllustrationNoResult,
IllustrationNoResultDark
} from '@douyinfe/semi-illustrations';
import {
CheckCircle,
Shield,
XCircle,
Clock,
Gauge,
HelpCircle,
Infinity,
Coins,
Key
} from 'lucide-react';
import {
IconPlus,
IconCopy,
IconSearch,
IconTreeTriangleDown,
IconEyeOpened,
IconEdit,
IconDelete,
IconStop,
IconPlay,
IconMore,
IconDescend
} from '@douyinfe/semi-icons';
import { Key } from 'lucide-react';
import EditToken from '../../pages/Token/EditToken';
import { useTranslation } from 'react-i18next';
import { useTableCompactMode } from '../../hooks/useTableCompactMode';
@@ -73,38 +52,38 @@ const TokensTable = () => {
case 1:
if (model_limits_enabled) {
return (
<Tag color='green' size='large' shape='circle' prefixIcon={<Shield size={14} />}>
<Tag color='green' size='large' shape='circle' >
{t('已启用:限制模型')}
</Tag>
);
} else {
return (
<Tag color='green' size='large' shape='circle' prefixIcon={<CheckCircle size={14} />}>
<Tag color='green' size='large' shape='circle' >
{t('已启用')}
</Tag>
);
}
case 2:
return (
<Tag color='red' size='large' shape='circle' prefixIcon={<XCircle size={14} />}>
<Tag color='red' size='large' shape='circle' >
{t('已禁用')}
</Tag>
);
case 3:
return (
<Tag color='yellow' size='large' shape='circle' prefixIcon={<Clock size={14} />}>
<Tag color='yellow' size='large' shape='circle' >
{t('已过期')}
</Tag>
);
case 4:
return (
<Tag color='grey' size='large' shape='circle' prefixIcon={<Gauge size={14} />}>
<Tag color='grey' size='large' shape='circle' >
{t('已耗尽')}
</Tag>
);
default:
return (
<Tag color='black' size='large' shape='circle' prefixIcon={<HelpCircle size={14} />}>
<Tag color='black' size='large' shape='circle' >
{t('未知状态')}
</Tag>
);
@@ -137,7 +116,7 @@ const TokensTable = () => {
render: (text, record, index) => {
return (
<div>
<Tag size={'large'} color={'grey'} shape='circle' prefixIcon={<Coins size={14} />}>
<Tag size={'large'} color={'grey'} shape='circle' >
{renderQuota(parseInt(text))}
</Tag>
</div>
@@ -164,7 +143,7 @@ const TokensTable = () => {
return (
<div>
{record.unlimited_quota ? (
<Tag size={'large'} color={'white'} shape='circle' prefixIcon={<Infinity size={14} />}>
<Tag size={'large'} color={'white'} shape='circle' >
{t('无限制')}
</Tag>
) : (
@@ -172,7 +151,6 @@ const TokensTable = () => {
size={'large'}
color={getQuotaColor(parseInt(text))}
shape='circle'
prefixIcon={<Coins size={14} />}
>
{renderQuota(parseInt(text))}
</Tag>
@@ -238,7 +216,6 @@ const TokensTable = () => {
{
node: 'item',
name: t('查看'),
icon: <IconEyeOpened />,
onClick: () => {
Modal.info({
title: t('令牌详情'),
@@ -250,7 +227,6 @@ const TokensTable = () => {
{
node: 'item',
name: t('删除'),
icon: <IconDelete />,
type: 'danger',
onClick: () => {
Modal.confirm({
@@ -271,7 +247,6 @@ const TokensTable = () => {
moreMenuItems.push({
node: 'item',
name: t('禁用'),
icon: <IconStop />,
type: 'warning',
onClick: () => {
manageToken(record.id, 'disable', record);
@@ -281,7 +256,6 @@ const TokensTable = () => {
moreMenuItems.push({
node: 'item',
name: t('启用'),
icon: <IconPlay />,
type: 'secondary',
onClick: () => {
manageToken(record.id, 'enable', record);
@@ -292,7 +266,7 @@ const TokensTable = () => {
return (
<Space wrap>
<SplitButtonGroup
className="!rounded-full overflow-hidden"
className="overflow-hidden"
aria-label={t('项目操作按钮组')}
>
<Button
@@ -331,11 +305,9 @@ const TokensTable = () => {
</SplitButtonGroup>
<Button
icon={<IconCopy />}
theme='light'
type='secondary'
size="small"
className="!rounded-full"
onClick={async (text) => {
await copyText('sk-' + record.key);
}}
@@ -344,11 +316,9 @@ const TokensTable = () => {
</Button>
<Button
icon={<IconEdit />}
theme='light'
type='tertiary'
size="small"
className="!rounded-full"
onClick={() => {
setEditingToken(record);
setShowEdit(true);
@@ -367,7 +337,6 @@ const TokensTable = () => {
theme='light'
type='tertiary'
size="small"
className="!rounded-full"
/>
</Dropdown>
</Space>
@@ -621,8 +590,7 @@ const TokensTable = () => {
<Button
theme="light"
type="secondary"
icon={<IconDescend />}
className="!rounded-full w-full md:w-auto"
className="w-full md:w-auto"
onClick={() => setCompactMode(!compactMode)}
>
{compactMode ? t('自适应列表') : t('紧凑列表')}
@@ -637,8 +605,7 @@ const TokensTable = () => {
<Button
theme="light"
type="primary"
icon={<IconPlus />}
className="!rounded-full flex-1 md:flex-initial"
className="flex-1 md:flex-initial"
onClick={() => {
setEditingToken({
id: undefined,
@@ -651,8 +618,7 @@ const TokensTable = () => {
<Button
theme="light"
type="warning"
icon={<IconCopy />}
className="!rounded-full flex-1 md:flex-initial"
className="flex-1 md:flex-initial"
onClick={() => {
if (selectedKeys.length === 0) {
showError(t('请至少选择一个令牌!'));
@@ -667,7 +633,6 @@ const TokensTable = () => {
<Button
type="primary"
theme="solid"
icon={<IconCopy />}
onClick={async () => {
let content = '';
for (let i = 0; i < selectedKeys.length; i++) {
@@ -682,7 +647,6 @@ const TokensTable = () => {
</Button>
<Button
theme="light"
icon={<IconCopy />}
onClick={async () => {
let content = '';
for (let i = 0; i < selectedKeys.length; i++) {
@@ -704,8 +668,7 @@ const TokensTable = () => {
<Button
theme="light"
type="danger"
icon={<IconDelete />}
className="!rounded-full w-full md:w-auto"
className="w-full md:w-auto"
onClick={() => {
if (selectedKeys.length === 0) {
showError(t('请至少选择一个令牌!'));
@@ -743,7 +706,6 @@ const TokensTable = () => {
field="searchKeyword"
prefix={<IconSearch />}
placeholder={t('搜索关键字')}
className="!rounded-full"
showClear
pure
/>
@@ -753,7 +715,6 @@ const TokensTable = () => {
field="searchToken"
prefix={<IconSearch />}
placeholder={t('密钥')}
className="!rounded-full"
showClear
pure
/>
@@ -763,7 +724,7 @@ const TokensTable = () => {
type="primary"
htmlType="submit"
loading={loading || searching}
className="!rounded-full flex-1 md:flex-initial md:w-auto"
className="flex-1 md:flex-initial md:w-auto"
>
{t('查询')}
</Button>
@@ -778,7 +739,7 @@ const TokensTable = () => {
}, 100);
}
}}
className="!rounded-full flex-1 md:flex-initial md:w-auto"
className="flex-1 md:flex-initial md:w-auto"
>
{t('重置')}
</Button>
+9 -28
View File
@@ -34,17 +34,9 @@ import {
IllustrationNoResultDark
} from '@douyinfe/semi-illustrations';
import {
IconPlus,
IconSearch,
IconEdit,
IconDelete,
IconStop,
IconPlay,
IconMore,
IconUserAdd,
IconArrowUp,
IconArrowDown,
IconDescend
IconMore,
} from '@douyinfe/semi-icons';
import { ITEMS_PER_PAGE } from '../../constants';
import AddUser from '../../pages/User/AddUser';
@@ -127,7 +119,7 @@ const UsersTable = () => {
<Tooltip content={remark} position="top" showArrow>
<Tag color='white' size='large' shape='circle' className="!text-xs">
<div className="flex items-center gap-1">
<div className="w-2 h-2 rounded-full flex-shrink-0" style={{ backgroundColor: '#10b981' }} />
<div className="w-2 h-2 flex-shrink-0" style={{ backgroundColor: '#10b981' }} />
{displayRemark}
</div>
</Tag>
@@ -221,7 +213,6 @@ const UsersTable = () => {
{
node: 'item',
name: t('提升'),
icon: <IconArrowUp />,
type: 'warning',
onClick: () => {
Modal.confirm({
@@ -236,7 +227,6 @@ const UsersTable = () => {
{
node: 'item',
name: t('降级'),
icon: <IconArrowDown />,
type: 'secondary',
onClick: () => {
Modal.confirm({
@@ -251,7 +241,6 @@ const UsersTable = () => {
{
node: 'item',
name: t('注销'),
icon: <IconDelete />,
type: 'danger',
onClick: () => {
Modal.confirm({
@@ -272,7 +261,6 @@ const UsersTable = () => {
moreMenuItems.splice(-1, 0, {
node: 'item',
name: t('禁用'),
icon: <IconStop />,
type: 'warning',
onClick: () => {
manageUser(record.id, 'disable', record);
@@ -282,7 +270,6 @@ const UsersTable = () => {
moreMenuItems.splice(-1, 0, {
node: 'item',
name: t('启用'),
icon: <IconPlay />,
type: 'secondary',
onClick: () => {
manageUser(record.id, 'enable', record);
@@ -294,11 +281,9 @@ const UsersTable = () => {
return (
<Space>
<Button
icon={<IconEdit />}
theme='light'
type='tertiary'
size="small"
className="!rounded-full"
onClick={() => {
setEditingUser(record);
setShowEditUser(true);
@@ -312,11 +297,10 @@ const UsersTable = () => {
menu={moreMenuItems}
>
<Button
icon={<IconMore />}
theme='light'
type='tertiary'
size="small"
className="!rounded-full"
icon={<IconMore />}
/>
</Dropdown>
</Space>
@@ -538,8 +522,7 @@ const UsersTable = () => {
<Button
theme='light'
type='secondary'
icon={<IconDescend />}
className="!rounded-full w-full md:w-auto"
className="w-full md:w-auto"
onClick={() => setCompactMode(!compactMode)}
>
{compactMode ? t('自适应列表') : t('紧凑列表')}
@@ -554,8 +537,7 @@ const UsersTable = () => {
<Button
theme='light'
type='primary'
icon={<IconPlus />}
className="!rounded-full w-full md:w-auto"
className="w-full md:w-auto"
onClick={() => {
setShowAddUser(true);
}}
@@ -584,7 +566,6 @@ const UsersTable = () => {
field="searchKeyword"
prefix={<IconSearch />}
placeholder={t('支持搜索用户的 ID、用户名、显示名称和邮箱地址')}
className="!rounded-full"
showClear
pure
/>
@@ -601,7 +582,7 @@ const UsersTable = () => {
searchUsers(1, pageSize);
}, 100);
}}
className="!rounded-full w-full"
className="w-full"
showClear
pure
/>
@@ -611,7 +592,7 @@ const UsersTable = () => {
type="primary"
htmlType="submit"
loading={loading || searching}
className="!rounded-full flex-1 md:flex-initial md:w-auto"
className="flex-1 md:flex-initial md:w-auto"
>
{t('查询')}
</Button>
@@ -627,7 +608,7 @@ const UsersTable = () => {
}, 100);
}
}}
className="!rounded-full flex-1 md:flex-initial md:w-auto"
className="flex-1 md:flex-initial md:w-auto"
>
{t('重置')}
</Button>
@@ -689,7 +670,7 @@ const UsersTable = () => {
style={{ padding: 30 }}
/>
}
className="rounded-xl overflow-hidden"
className="overflow-hidden"
size="middle"
/>
</Card>
+2 -2
View File
@@ -456,7 +456,7 @@
"创建新的令牌": "Create New Token",
"令牌分组,默认为用户的分组": "Token group, default is the your's group",
"IP白名单": "IP whitelist",
"注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制": "Note that the quota of the token is only used to limit the maximum quota usage of the token itself, and the actual usage is limited by the remaining quota of the account.",
"令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制": "The quota of the token is only used to limit the maximum quota usage of the token itself, and the actual usage is limited by the remaining quota of the account",
"无限额度": "Unlimited quota",
"更新令牌信息": "Update Token Information",
"请输入充值码!": "Please enter the recharge code!",
@@ -1746,7 +1746,7 @@
"请先选择模型!": "Please select a model first!",
"已复制 ${count} 个模型": "Copied ${count} models",
"复制失败,请手动复制": "Copy failed, please copy manually",
"快捷设置": "Quick settings",
"过期时间快捷设置": "Expiration time quick settings",
"批量创建时会在名称后自动添加随机后缀": "When creating in batches, a random suffix will be automatically added to the name",
"额度必须大于0": "Quota must be greater than 0",
"生成数量必须大于0": "Generation quantity must be greater than 0",
+2 -1
View File
@@ -43,6 +43,7 @@ code {
/* ==================== 导航和侧边栏样式 ==================== */
/* 导航项样式 */
.semi-input-textarea-wrapper,
.semi-navigation-sub-title,
.semi-chat-inputBox-sendButton,
.semi-page-item,
@@ -53,7 +54,7 @@ code {
.semi-select,
.semi-button,
.semi-datepicker-range-input {
border-radius: 9999px !important;
border-radius: 10px !important;
}
.semi-navigation-item {
+3 -41
View File
@@ -67,6 +67,8 @@ function type2secretPrompt(type) {
return '按照如下格式输入:Ak|Sk|Region';
case 50:
return '按照如下格式输入: AccessKey|SecretKey';
case 51:
return '按照如下格式输入: Access Key ID|Secret Access Key';
default:
return '请输入渠道对应的鉴权密钥';
}
@@ -449,10 +451,6 @@ const EditChannel = (props) => {
</Title>
</Space>
}
headerStyle={{
borderBottom: '1px solid var(--semi-color-border)',
padding: '24px'
}}
bodyStyle={{ padding: '0' }}
visible={props.visible}
width={isMobile() ? '100%' : 600}
@@ -461,7 +459,6 @@ const EditChannel = (props) => {
<Space>
<Button
theme="solid"
className="!rounded-full"
onClick={submit}
icon={<IconSave />}
>
@@ -469,7 +466,6 @@ const EditChannel = (props) => {
</Button>
<Button
theme="light"
className="!rounded-full"
type="primary"
onClick={handleCancel}
icon={<IconClose />}
@@ -483,7 +479,7 @@ const EditChannel = (props) => {
onCancel={() => handleCancel()}
>
<Spin spinning={loading}>
<div className="p-6">
<div className="p-2">
<Card className="!rounded-2xl shadow-sm border-0 mb-6">
{/* Header: Basic Info */}
<div className="flex items-center mb-2">
@@ -509,7 +505,6 @@ const EditChannel = (props) => {
filter
searchPosition='dropdown'
placeholder={t('请选择渠道类型')}
className="!rounded-lg"
/>
</div>
@@ -524,7 +519,6 @@ const EditChannel = (props) => {
}}
value={inputs.name}
autoComplete='new-password'
className="!rounded-lg"
/>
</div>
@@ -541,7 +535,6 @@ const EditChannel = (props) => {
value={inputs.key}
autosize={{ minRows: 6, maxRows: 6 }}
autoComplete='new-password'
className="!rounded-lg"
/>
) : (
<>
@@ -570,7 +563,6 @@ const EditChannel = (props) => {
autosize={{ minRows: 10 }}
value={inputs.key}
autoComplete='new-password'
className="!rounded-lg font-mono"
/>
) : (
<Input
@@ -582,7 +574,6 @@ const EditChannel = (props) => {
}}
value={inputs.key}
autoComplete='new-password'
className="!rounded-lg"
/>
)}
</>
@@ -631,7 +622,6 @@ const EditChannel = (props) => {
</Text>
</div>
}
className='!rounded-lg'
/>
)}
@@ -650,7 +640,6 @@ const EditChannel = (props) => {
onChange={(value) => handleInputChange('base_url', value)}
value={inputs.base_url}
autoComplete='new-password'
className="!rounded-lg"
/>
</div>
<div>
@@ -661,7 +650,6 @@ const EditChannel = (props) => {
onChange={(value) => handleInputChange('other', value)}
value={inputs.other}
autoComplete='new-password'
className="!rounded-lg"
/>
</div>
</>
@@ -682,7 +670,6 @@ const EditChannel = (props) => {
onChange={(value) => handleInputChange('base_url', value)}
value={inputs.base_url}
autoComplete='new-password'
className="!rounded-lg"
/>
</div>
</>
@@ -705,7 +692,6 @@ const EditChannel = (props) => {
onChange={(value) => handleInputChange('base_url', value)}
value={inputs.base_url}
autoComplete='new-password'
className="!rounded-lg"
/>
<Text type="tertiary" className="mt-1 text-xs">
{t('对于官方渠道,new-api已经内置地址,除非是第三方代理站点或者Azure的特殊接入地址,否则不需要填写')}
@@ -722,7 +708,6 @@ const EditChannel = (props) => {
onChange={(value) => handleInputChange('base_url', value)}
value={inputs.base_url}
autoComplete='new-password'
className="!rounded-lg"
/>
</div>
)}
@@ -738,7 +723,6 @@ const EditChannel = (props) => {
onChange={(value) => handleInputChange('base_url', value)}
value={inputs.base_url}
autoComplete='new-password'
className="!rounded-lg"
/>
</div>
)}
@@ -773,7 +757,6 @@ const EditChannel = (props) => {
value={inputs.models}
autoComplete='new-password'
optionList={modelOptions}
className="!rounded-lg"
/>
</div>
@@ -781,14 +764,12 @@ const EditChannel = (props) => {
<Button
type='primary'
onClick={() => handleInputChange('models', basicModels)}
className="!rounded-lg"
>
{t('填入相关模型')}
</Button>
<Button
type='secondary'
onClick={() => handleInputChange('models', fullModels)}
className="!rounded-lg"
>
{t('填入所有模型')}
</Button>
@@ -796,7 +777,6 @@ const EditChannel = (props) => {
<Button
type='tertiary'
onClick={() => fetchUpstreamModelList('models')}
className="!rounded-lg"
>
{t('获取模型列表')}
</Button>
@@ -804,7 +784,6 @@ const EditChannel = (props) => {
<Button
type='warning'
onClick={() => handleInputChange('models', [])}
className="!rounded-lg"
>
{t('清除所有模型')}
</Button>
@@ -822,7 +801,6 @@ const EditChannel = (props) => {
showError(t('复制失败'));
}
}}
className="!rounded-lg"
>
{t('复制所有模型')}
</Button>
@@ -846,7 +824,6 @@ const EditChannel = (props) => {
placeholder={t('输入自定义模型名称')}
value={customModel}
onChange={(value) => setCustomModel(value.trim())}
className="!rounded-lg"
/>
</div>
@@ -862,7 +839,6 @@ const EditChannel = (props) => {
autosize
value={inputs.model_mapping}
autoComplete='new-password'
className="!rounded-lg font-mono"
/>
<Text
className="!text-semi-color-primary cursor-pointer mt-1 block"
@@ -879,7 +855,6 @@ const EditChannel = (props) => {
placeholder={t('不填则为模型列表第一个')}
onChange={(value) => handleInputChange('test_model', value)}
value={inputs.test_model}
className="!rounded-lg"
/>
</div>
</div>
@@ -913,7 +888,6 @@ const EditChannel = (props) => {
value={inputs.groups}
autoComplete='new-password'
optionList={groupOptions}
className="!rounded-lg"
/>
</div>
@@ -926,7 +900,6 @@ const EditChannel = (props) => {
onChange={(value) => handleInputChange('other', value)}
value={inputs.other}
autoComplete='new-password'
className="!rounded-lg"
/>
</div>
)}
@@ -947,7 +920,6 @@ const EditChannel = (props) => {
onChange={(value) => handleInputChange('other', value)}
value={inputs.other}
autoComplete='new-password'
className="!rounded-lg font-mono"
/>
<Text
className="!text-semi-color-primary cursor-pointer mt-1 block"
@@ -967,7 +939,6 @@ const EditChannel = (props) => {
onChange={(value) => handleInputChange('other', value)}
value={inputs.other}
autoComplete='new-password'
className="!rounded-lg"
/>
</div>
)}
@@ -981,7 +952,6 @@ const EditChannel = (props) => {
onChange={(value) => handleInputChange('other', value)}
value={inputs.other}
autoComplete='new-password'
className="!rounded-lg"
/>
</div>
)}
@@ -995,7 +965,6 @@ const EditChannel = (props) => {
onChange={(value) => handleInputChange('other', value)}
value={inputs.other}
autoComplete='new-password'
className="!rounded-lg"
/>
</div>
)}
@@ -1008,7 +977,6 @@ const EditChannel = (props) => {
onChange={(value) => handleInputChange('tag', value)}
value={inputs.tag}
autoComplete='new-password'
className="!rounded-lg"
/>
</div>
@@ -1027,7 +995,6 @@ const EditChannel = (props) => {
}}
value={inputs.priority}
autoComplete='new-password'
className="!rounded-lg"
/>
</div>
@@ -1046,7 +1013,6 @@ const EditChannel = (props) => {
}}
value={inputs.weight}
autoComplete='new-password'
className="!rounded-lg"
/>
</div>
@@ -1062,7 +1028,6 @@ const EditChannel = (props) => {
autosize
value={inputs.setting}
autoComplete='new-password'
className="!rounded-lg font-mono"
/>
<div className="flex gap-2 mt-1">
<Text
@@ -1101,7 +1066,6 @@ const EditChannel = (props) => {
autosize
value={inputs.param_override}
autoComplete='new-password'
className="!rounded-lg font-mono"
/>
</div>
@@ -1113,7 +1077,6 @@ const EditChannel = (props) => {
placeholder={t('请输入组织org-xxx')}
onChange={(value) => handleInputChange('openai_organization', value)}
value={inputs.openai_organization}
className="!rounded-lg"
/>
<Text type="tertiary" className="mt-1 text-xs">
{t('组织,可选,不填则为默认组织')}
@@ -1146,7 +1109,6 @@ const EditChannel = (props) => {
autosize
value={inputs.status_code_mapping}
autoComplete='new-password'
className="!rounded-lg font-mono"
/>
<Text
className="!text-semi-color-primary cursor-pointer mt-1 block"
+1 -12
View File
@@ -274,10 +274,6 @@ const EditTagModal = (props) => {
</Title>
</Space>
}
headerStyle={{
borderBottom: '1px solid var(--semi-color-border)',
padding: '24px'
}}
bodyStyle={{ padding: '0' }}
visible={visible}
width={600}
@@ -287,7 +283,6 @@ const EditTagModal = (props) => {
<Space>
<Button
theme="solid"
className="!rounded-full"
onClick={handleSave}
loading={loading}
icon={<IconSave />}
@@ -296,7 +291,6 @@ const EditTagModal = (props) => {
</Button>
<Button
theme="light"
className="!rounded-full"
type="primary"
onClick={handleClose}
icon={<IconClose />}
@@ -309,7 +303,7 @@ const EditTagModal = (props) => {
closeIcon={null}
>
<Spin spinning={loading}>
<div className="p-6">
<div className="p-2">
<Card className="!rounded-2xl shadow-sm border-0 mb-6">
{/* Header: Tag Info */}
<div className="flex items-center mb-2">
@@ -335,7 +329,6 @@ const EditTagModal = (props) => {
value={inputs.new_tag}
onChange={(value) => setInputs({ ...inputs, new_tag: value })}
placeholder={t('请输入新标签,留空则解散标签')}
className="!rounded-lg"
/>
</div>
</div>
@@ -370,7 +363,6 @@ const EditTagModal = (props) => {
onChange={(value) => handleInputChange('models', value)}
value={inputs.models}
optionList={modelOptions}
className="!rounded-lg"
/>
</div>
@@ -384,7 +376,6 @@ const EditTagModal = (props) => {
placeholder={t('输入自定义模型名称')}
value={customModel}
onChange={(value) => setCustomModel(value.trim())}
className="!rounded-lg"
/>
</div>
@@ -396,7 +387,6 @@ const EditTagModal = (props) => {
onChange={(value) => handleInputChange('model_mapping', value)}
autosize
value={inputs.model_mapping}
className="!rounded-lg font-mono"
/>
<Space className="mt-2">
<Text
@@ -446,7 +436,6 @@ const EditTagModal = (props) => {
onChange={(value) => handleInputChange('groups', value)}
value={inputs.groups}
optionList={groupOptions}
className="!rounded-lg"
/>
</div>
</div>
+1
View File
@@ -149,6 +149,7 @@ const Home = () => {
type="primary"
onClick={handleCopyBaseURL}
icon={<IconCopy />}
className="!rounded-full"
/>
</div>
}
+1 -7
View File
@@ -154,10 +154,6 @@ const EditRedemption = (props) => {
</Title>
</Space>
}
headerStyle={{
borderBottom: '1px solid var(--semi-color-border)',
padding: '24px'
}}
bodyStyle={{ padding: '0' }}
visible={props.visiable}
width={isMobile() ? '100%' : 600}
@@ -166,7 +162,6 @@ const EditRedemption = (props) => {
<Space>
<Button
theme="solid"
className="!rounded-full"
onClick={() => formApiRef.current?.submitForm()}
icon={<IconSave />}
loading={loading}
@@ -175,7 +170,6 @@ const EditRedemption = (props) => {
</Button>
<Button
theme="light"
className="!rounded-full"
type="primary"
onClick={handleCancel}
icon={<IconClose />}
@@ -195,7 +189,7 @@ const EditRedemption = (props) => {
onSubmit={submit}
>
{({ values }) => (
<div className="p-6 space-y-6">
<div className="p-2">
<Card className="!rounded-2xl shadow-sm border-0 mb-6">
{/* Header: Basic Info */}
<div className="flex items-center mb-2">
@@ -230,7 +230,7 @@ const SettingsAPIInfo = ({ options, refresh }) => {
render: (text, record) => (
<Tag
color={record.color}
className="!rounded-full"
shape='circle'
style={{ maxWidth: '280px' }}
>
{text}
@@ -277,7 +277,6 @@ const SettingsAPIInfo = ({ options, refresh }) => {
theme='light'
type='tertiary'
size='small'
className="!rounded-full"
onClick={() => handleEditApi(record)}
>
{t('编辑')}
@@ -287,7 +286,6 @@ const SettingsAPIInfo = ({ options, refresh }) => {
type='danger'
theme='light'
size='small'
className="!rounded-full"
onClick={() => handleDeleteApi(record)}
>
{t('删除')}
@@ -327,7 +325,7 @@ const SettingsAPIInfo = ({ options, refresh }) => {
theme='light'
type='primary'
icon={<Plus size={14} />}
className="!rounded-full w-full md:w-auto"
className="w-full md:w-auto"
onClick={handleAddApi}
>
{t('添加API')}
@@ -338,7 +336,7 @@ const SettingsAPIInfo = ({ options, refresh }) => {
theme='light'
onClick={handleBatchDelete}
disabled={selectedRowKeys.length === 0}
className="!rounded-full w-full md:w-auto"
className="w-full md:w-auto"
>
{t('批量删除')} {selectedRowKeys.length > 0 && `(${selectedRowKeys.length})`}
</Button>
@@ -348,7 +346,7 @@ const SettingsAPIInfo = ({ options, refresh }) => {
loading={loading}
disabled={!hasChanges}
type='secondary'
className="!rounded-full w-full md:w-auto"
className="w-full md:w-auto"
>
{t('保存设置')}
</Button>
@@ -430,7 +428,7 @@ const SettingsAPIInfo = ({ options, refresh }) => {
style={{ padding: 30 }}
/>
}
className="rounded-xl overflow-hidden"
className="overflow-hidden"
/>
</Form.Section>
@@ -441,7 +439,6 @@ const SettingsAPIInfo = ({ options, refresh }) => {
onCancel={() => setShowApiModal(false)}
okText={t('保存')}
cancelText={t('取消')}
className="rounded-xl"
confirmLoading={modalLoading}
>
<Form layout='vertical' initValues={apiForm} key={editingApi ? editingApi.id : 'new'}>
@@ -495,7 +492,6 @@ const SettingsAPIInfo = ({ options, refresh }) => {
okText={t('确认删除')}
cancelText={t('取消')}
type="warning"
className="rounded-xl"
okButtonProps={{
type: 'danger',
theme: 'solid'
@@ -155,7 +155,6 @@ const SettingsAnnouncements = ({ options, refresh }) => {
theme='light'
type='tertiary'
size='small'
className="!rounded-full"
onClick={() => handleEditAnnouncement(record)}
>
{t('编辑')}
@@ -165,7 +164,6 @@ const SettingsAnnouncements = ({ options, refresh }) => {
type='danger'
theme='light'
size='small'
className="!rounded-full"
onClick={() => handleDeleteAnnouncement(record)}
>
{t('删除')}
@@ -365,7 +363,7 @@ const SettingsAnnouncements = ({ options, refresh }) => {
theme='light'
type='primary'
icon={<Plus size={14} />}
className="!rounded-full w-full md:w-auto"
className="w-full md:w-auto"
onClick={handleAddAnnouncement}
>
{t('添加公告')}
@@ -376,7 +374,7 @@ const SettingsAnnouncements = ({ options, refresh }) => {
theme='light'
onClick={handleBatchDelete}
disabled={selectedRowKeys.length === 0}
className="!rounded-full w-full md:w-auto"
className="w-full md:w-auto"
>
{t('批量删除')} {selectedRowKeys.length > 0 && `(${selectedRowKeys.length})`}
</Button>
@@ -386,7 +384,7 @@ const SettingsAnnouncements = ({ options, refresh }) => {
loading={loading}
disabled={!hasChanges}
type='secondary'
className="!rounded-full w-full md:w-auto"
className="w-full md:w-auto"
>
{t('保存设置')}
</Button>
@@ -471,7 +469,7 @@ const SettingsAnnouncements = ({ options, refresh }) => {
style={{ padding: 30 }}
/>
}
className="rounded-xl overflow-hidden"
className="overflow-hidden"
/>
</Form.Section>
@@ -482,7 +480,6 @@ const SettingsAnnouncements = ({ options, refresh }) => {
onCancel={() => setShowAnnouncementModal(false)}
okText={t('保存')}
cancelText={t('取消')}
className="rounded-xl"
confirmLoading={modalLoading}
>
<Form
@@ -543,7 +540,6 @@ const SettingsAnnouncements = ({ options, refresh }) => {
okText={t('确认删除')}
cancelText={t('取消')}
type="warning"
className="rounded-xl"
okButtonProps={{
type: 'danger',
theme: 'solid'
@@ -566,7 +562,6 @@ const SettingsAnnouncements = ({ options, refresh }) => {
onCancel={() => setShowContentModal(false)}
okText={t('确定')}
cancelText={t('取消')}
className="rounded-xl"
width={800}
>
<TextArea
@@ -98,7 +98,6 @@ const SettingsFAQ = ({ options, refresh }) => {
theme='light'
type='tertiary'
size='small'
className="!rounded-full"
onClick={() => handleEditFaq(record)}
>
{t('编辑')}
@@ -108,7 +107,6 @@ const SettingsFAQ = ({ options, refresh }) => {
type='danger'
theme='light'
size='small'
className="!rounded-full"
onClick={() => handleDeleteFaq(record)}
>
{t('删除')}
@@ -297,7 +295,7 @@ const SettingsFAQ = ({ options, refresh }) => {
theme='light'
type='primary'
icon={<Plus size={14} />}
className="!rounded-full w-full md:w-auto"
className="w-full md:w-auto"
onClick={handleAddFaq}
>
{t('添加问答')}
@@ -308,7 +306,7 @@ const SettingsFAQ = ({ options, refresh }) => {
theme='light'
onClick={handleBatchDelete}
disabled={selectedRowKeys.length === 0}
className="!rounded-full w-full md:w-auto"
className="w-full md:w-auto"
>
{t('批量删除')} {selectedRowKeys.length > 0 && `(${selectedRowKeys.length})`}
</Button>
@@ -318,7 +316,7 @@ const SettingsFAQ = ({ options, refresh }) => {
loading={loading}
disabled={!hasChanges}
type='secondary'
className="!rounded-full w-full md:w-auto"
className="w-full md:w-auto"
>
{t('保存设置')}
</Button>
@@ -397,7 +395,7 @@ const SettingsFAQ = ({ options, refresh }) => {
style={{ padding: 30 }}
/>
}
className="rounded-xl overflow-hidden"
className="overflow-hidden"
/>
</Form.Section>
@@ -408,7 +406,6 @@ const SettingsFAQ = ({ options, refresh }) => {
onCancel={() => setShowFaqModal(false)}
okText={t('保存')}
cancelText={t('取消')}
className="rounded-xl"
confirmLoading={modalLoading}
width={800}
>
@@ -444,7 +441,6 @@ const SettingsFAQ = ({ options, refresh }) => {
okText={t('确认删除')}
cancelText={t('取消')}
type="warning"
className="rounded-xl"
okButtonProps={{
type: 'danger',
theme: 'solid'
@@ -101,7 +101,6 @@ const SettingsUptimeKuma = ({ options, refresh }) => {
theme='light'
type='tertiary'
size='small'
className="!rounded-full"
onClick={() => handleEditGroup(record)}
>
{t('编辑')}
@@ -111,7 +110,6 @@ const SettingsUptimeKuma = ({ options, refresh }) => {
type='danger'
theme='light'
size='small'
className="!rounded-full"
onClick={() => handleDeleteGroup(record)}
>
{t('删除')}
@@ -314,7 +312,7 @@ const SettingsUptimeKuma = ({ options, refresh }) => {
theme='light'
type='primary'
icon={<Plus size={14} />}
className="!rounded-full w-full md:w-auto"
className="w-full md:w-auto"
onClick={handleAddGroup}
>
{t('添加分类')}
@@ -325,7 +323,7 @@ const SettingsUptimeKuma = ({ options, refresh }) => {
theme='light'
onClick={handleBatchDelete}
disabled={selectedRowKeys.length === 0}
className="!rounded-full w-full md:w-auto"
className="w-full md:w-auto"
>
{t('批量删除')} {selectedRowKeys.length > 0 && `(${selectedRowKeys.length})`}
</Button>
@@ -335,7 +333,7 @@ const SettingsUptimeKuma = ({ options, refresh }) => {
loading={loading}
disabled={!hasChanges}
type='secondary'
className="!rounded-full w-full md:w-auto"
className="w-full md:w-auto"
>
{t('保存设置')}
</Button>
@@ -413,7 +411,7 @@ const SettingsUptimeKuma = ({ options, refresh }) => {
style={{ padding: 30 }}
/>
}
className="rounded-xl overflow-hidden"
className="overflow-hidden"
/>
</Form.Section>
@@ -424,7 +422,6 @@ const SettingsUptimeKuma = ({ options, refresh }) => {
onCancel={() => setShowUptimeModal(false)}
okText={t('保存')}
cancelText={t('取消')}
className="rounded-xl"
confirmLoading={modalLoading}
width={600}
>
@@ -467,7 +464,6 @@ const SettingsUptimeKuma = ({ options, refresh }) => {
okText={t('确认删除')}
cancelText={t('取消')}
type="warning"
className="rounded-xl"
okButtonProps={{
type: 'danger',
theme: 'solid'
@@ -175,7 +175,7 @@ export default function RequestRateLimit(props) {
]}
extraText={
<div>
<p style={{ marginBottom: -15 }}>{t('说明:')}</p>
<p>{t('说明:')}</p>
<ul>
<li>{t('使用 JSON 对象格式,格式为:{"组名": [最多请求次数, 最多请求完成次数]}')}</li>
<li>{t('示例:{"default": [200, 100], "vip": [0, 1000]}。')}</li>
+30 -33
View File
@@ -9,7 +9,6 @@ import {
renderQuotaWithPrompt,
} from '../../helpers';
import {
Banner,
Button,
SideSheet,
Space,
@@ -27,7 +26,7 @@ import {
IconLink,
IconSave,
IconClose,
IconPlusCircle,
IconKey,
} from '@douyinfe/semi-icons';
import { useTranslation } from 'react-i18next';
import { StatusContext } from '../../context/Status';
@@ -37,11 +36,11 @@ const { Text, Title } = Typography;
const EditToken = (props) => {
const { t } = useTranslation();
const [statusState, statusDispatch] = useContext(StatusContext);
const [isEdit, setIsEdit] = useState(false);
const [loading, setLoading] = useState(false);
const formApiRef = useRef(null);
const [models, setModels] = useState([]);
const [groups, setGroups] = useState([]);
const isEdit = props.editingToken.id !== undefined;
const getInitValues = () => ({
name: '',
@@ -136,10 +135,6 @@ const EditToken = (props) => {
setLoading(false);
};
useEffect(() => {
setIsEdit(props.editingToken.id !== undefined);
}, [props.editingToken.id]);
useEffect(() => {
if (formApiRef.current) {
if (!isEdit) {
@@ -150,7 +145,7 @@ const EditToken = (props) => {
}
loadModels();
loadGroups();
}, [isEdit]);
}, [props.editingToken.id]);
const generateRandomSuffix = () => {
const characters =
@@ -254,10 +249,6 @@ const EditToken = (props) => {
</Title>
</Space>
}
headerStyle={{
borderBottom: '1px solid var(--semi-color-border)',
padding: '24px',
}}
bodyStyle={{ padding: '0' }}
visible={props.visiable}
width={isMobile() ? '100%' : 600}
@@ -266,7 +257,7 @@ const EditToken = (props) => {
<Space>
<Button
theme='solid'
className='!rounded-full'
className='!rounded-lg'
onClick={() => formApiRef.current?.submitForm()}
icon={<IconSave />}
loading={loading}
@@ -275,7 +266,7 @@ const EditToken = (props) => {
</Button>
<Button
theme='light'
className='!rounded-full'
className='!rounded-lg'
type='primary'
onClick={handleCancel}
icon={<IconClose />}
@@ -296,12 +287,12 @@ const EditToken = (props) => {
onSubmit={submit}
>
{({ values }) => (
<div className='p-6 space-y-6'>
<div className='p-2'>
{/* 基本信息 */}
<Card className='!rounded-2xl shadow-sm border-0'>
<div className='flex items-center mb-2'>
<Avatar size='small' color='blue' className='mr-2 shadow-md'>
<IconPlusCircle size={16} />
<IconKey size={16} />
</Avatar>
<div>
<Text className='text-lg font-medium'>{t('基本信息')}</Text>
@@ -326,33 +317,36 @@ const EditToken = (props) => {
placeholder={t('令牌分组,默认为用户的分组')}
optionList={groups}
renderOptionItem={renderGroupOption}
showClear
style={{ width: '100%' }}
/>
) : (
<Form.Select
placeholder={t('管理员未设置用户可选分组')}
disabled
label={t('令牌分组')}
style={{ width: '100%' }}
/>
)}
</Col>
<Col span={10}>
<Col xs={24} sm={24} md={24} lg={10} xl={10}>
<Form.DatePicker
field='expired_time'
label={t('过期时间')}
type='dateTime'
placeholder={t('请选择过期时间')}
style={{ width: '100%' }}
rules={[{ required: true, message: t('请选择过期时间') }]}
showClear
style={{ width: '100%' }}
/>
</Col>
<Col span={14} className='flex flex-col justify-end'>
<Form.Slot label={t('快捷设置')}>
<Col xs={24} sm={24} md={24} lg={14} xl={14}>
<Form.Slot label={t('过期时间快捷设置')}>
<Space wrap>
<Button
theme='light'
type='primary'
onClick={() => setExpiredTime(0, 0, 0, 0)}
className='!rounded-full'
>
{t('永不过期')}
</Button>
@@ -360,7 +354,6 @@ const EditToken = (props) => {
theme='light'
type='tertiary'
onClick={() => setExpiredTime(1, 0, 0, 0)}
className='!rounded-full'
>
{t('一个月')}
</Button>
@@ -368,7 +361,6 @@ const EditToken = (props) => {
theme='light'
type='tertiary'
onClick={() => setExpiredTime(0, 1, 0, 0)}
className='!rounded-full'
>
{t('一天')}
</Button>
@@ -376,7 +368,6 @@ const EditToken = (props) => {
theme='light'
type='tertiary'
onClick={() => setExpiredTime(0, 0, 1, 0)}
className='!rounded-full'
>
{t('一小时')}
</Button>
@@ -391,6 +382,7 @@ const EditToken = (props) => {
min={1}
extraText={t('批量创建时会在名称后自动添加随机后缀')}
rules={[{ required: true, message: t('请输入新建数量') }]}
style={{ width: '100%' }}
/>
</Col>
)}
@@ -409,7 +401,7 @@ const EditToken = (props) => {
</div>
</div>
<Row gutter={12}>
<Col span={10}>
<Col span={24}>
<Form.AutoComplete
field='remain_quota'
label={t('额度')}
@@ -428,15 +420,15 @@ const EditToken = (props) => {
]}
/>
</Col>
<Col span={14} className='flex justify-end'>
<Form.Switch field='unlimited_quota' label={t('无限额度')} size='large' />
<Col span={24}>
<Form.Switch
field='unlimited_quota'
label={t('无限额度')}
size='large'
extraText={t('令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制')}
/>
</Col>
</Row>
<Banner
type='warning'
description={t('注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。')}
className='mb-4 !rounded-lg'
/>
</Card>
{/* 访问限制 */}
@@ -456,8 +448,11 @@ const EditToken = (props) => {
field='allow_ips'
label={t('IP白名单')}
placeholder={t('允许的IP,一行一个,不填写则不限制')}
rows={4}
autosize
rows={1}
extraText={t('请勿过度信任此功能,IP可能被伪造')}
showClear
style={{ width: '100%' }}
/>
</Col>
<Col span={24}>
@@ -469,6 +464,8 @@ const EditToken = (props) => {
optionList={models}
maxTagCount={3}
extraText={t('非必要,不建议启用模型限制')}
showClear
style={{ width: '100%' }}
/>
</Col>
</Row>
+13 -11
View File
@@ -65,10 +65,6 @@ const AddUser = (props) => {
</Title>
</Space>
}
headerStyle={{
borderBottom: '1px solid var(--semi-color-border)',
padding: '24px'
}}
bodyStyle={{ padding: '0' }}
visible={props.visible}
width={isMobile() ? '100%' : 600}
@@ -77,7 +73,6 @@ const AddUser = (props) => {
<Space>
<Button
theme="solid"
className="!rounded-full"
onClick={() => formApiRef.current?.submitForm()}
icon={<IconSave />}
loading={loading}
@@ -86,7 +81,6 @@ const AddUser = (props) => {
</Button>
<Button
theme="light"
className="!rounded-full"
type="primary"
onClick={handleCancel}
icon={<IconClose />}
@@ -110,7 +104,7 @@ const AddUser = (props) => {
formApiRef.current?.scrollToError();
}}
>
<div className="p-6 space-y-6">
<div className="p-2">
<Card className="!rounded-2xl shadow-sm border-0">
<div className="flex items-center mb-2">
<Avatar size="small" color="blue" className="mr-2 shadow-md">
@@ -128,13 +122,17 @@ const AddUser = (props) => {
field='username'
label={t('用户名')}
placeholder={t('请输入用户名')}
rules={[{ required: true, message: t('请输入用户名') }]} />
rules={[{ required: true, message: t('请输入用户名') }]}
showClear
/>
</Col>
<Col span={24}>
<Form.Input
field='display_name'
label={t('显示名称')}
placeholder={t('请输入显示名称')} />
placeholder={t('请输入显示名称')}
showClear
/>
</Col>
<Col span={24}>
<Form.Input
@@ -142,13 +140,17 @@ const AddUser = (props) => {
label={t('密码')}
type='password'
placeholder={t('请输入密码')}
rules={[{ required: true, message: t('请输入密码') }]} />
rules={[{ required: true, message: t('请输入密码') }]}
showClear
/>
</Col>
<Col span={24}>
<Form.Input
field='remark'
label={t('备注')}
placeholder={t('请输入备注(仅管理员可见)')} />
placeholder={t('请输入备注(仅管理员可见)')}
showClear
/>
</Col>
</Row>
</Card>
+1 -4
View File
@@ -134,7 +134,6 @@ const EditUser = (props) => {
</Title>
</Space>
}
headerStyle={{ borderBottom: '1px solid var(--semi-color-border)', padding: '24px' }}
bodyStyle={{ padding: 0 }}
visible={props.visible}
width={isMobile() ? '100%' : 600}
@@ -143,7 +142,6 @@ const EditUser = (props) => {
<Space>
<Button
theme='solid'
className='!rounded-full'
onClick={() => formApiRef.current?.submitForm()}
icon={<IconSave />}
loading={loading}
@@ -152,7 +150,6 @@ const EditUser = (props) => {
</Button>
<Button
theme='light'
className='!rounded-full'
type='primary'
onClick={handleCancel}
icon={<IconClose />}
@@ -172,7 +169,7 @@ const EditUser = (props) => {
onSubmit={submit}
>
{({ values }) => (
<div className='p-6 space-y-6'>
<div className='p-2'>
{/* 基本信息 */}
<Card className='!rounded-2xl shadow-sm border-0'>
<div className='flex items-center mb-2'>