diff --git a/Dockerfile b/Dockerfile
index 3b42089b159..08cc86f7254 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -2,6 +2,7 @@ FROM oven/bun:latest AS builder
WORKDIR /build
COPY web/package.json .
+COPY web/bun.lock .
RUN bun install
COPY ./web .
COPY ./VERSION .
diff --git a/common/api_type.go b/common/api_type.go
index d907123698c..f045866ac6a 100644
--- a/common/api_type.go
+++ b/common/api_type.go
@@ -63,6 +63,8 @@ func ChannelType2APIType(channelType int) (int, bool) {
apiType = constant.APITypeXai
case constant.ChannelTypeCoze:
apiType = constant.APITypeCoze
+ case constant.ChannelTypeJimeng:
+ apiType = constant.APITypeJimeng
}
if apiType == -1 {
return constant.APITypeOpenAI, false
diff --git a/common/constants.go b/common/constants.go
index e4f5f047574..30522411501 100644
--- a/common/constants.go
+++ b/common/constants.go
@@ -193,3 +193,9 @@ const (
ChannelStatusManuallyDisabled = 2 // also don't use 0
ChannelStatusAutoDisabled = 3
)
+
+const (
+ TopUpStatusPending = "pending"
+ TopUpStatusSuccess = "success"
+ TopUpStatusExpired = "expired"
+)
diff --git a/common/gin.go b/common/gin.go
index d428184acc0..8c67bb4d573 100644
--- a/common/gin.go
+++ b/common/gin.go
@@ -4,6 +4,7 @@ import (
"bytes"
"github.com/gin-gonic/gin"
"io"
+ "net/http"
"one-api/constant"
"strings"
"time"
@@ -32,7 +33,7 @@ func UnmarshalBodyReusable(c *gin.Context, v any) error {
}
contentType := c.Request.Header.Get("Content-Type")
if strings.HasPrefix(contentType, "application/json") {
- err = UnmarshalJson(requestBody, &v)
+ err = Unmarshal(requestBody, &v)
} else {
// skip for now
// TODO: someday non json request have variant model, we will need to implementation this
@@ -86,3 +87,25 @@ func GetContextKeyType[T any](c *gin.Context, key constant.ContextKey) (T, bool)
var t T
return t, false
}
+
+func ApiError(c *gin.Context, err error) {
+ c.JSON(http.StatusOK, gin.H{
+ "success": false,
+ "message": err.Error(),
+ })
+}
+
+func ApiErrorMsg(c *gin.Context, msg string) {
+ c.JSON(http.StatusOK, gin.H{
+ "success": false,
+ "message": msg,
+ })
+}
+
+func ApiSuccess(c *gin.Context, data any) {
+ c.JSON(http.StatusOK, gin.H{
+ "success": true,
+ "message": "",
+ "data": data,
+ })
+}
diff --git a/common/hash.go b/common/hash.go
new file mode 100644
index 00000000000..50191938573
--- /dev/null
+++ b/common/hash.go
@@ -0,0 +1,34 @@
+package common
+
+import (
+ "crypto/hmac"
+ "crypto/sha1"
+ "crypto/sha256"
+ "encoding/hex"
+)
+
+func Sha256Raw(data []byte) []byte {
+ h := sha256.New()
+ h.Write(data)
+ return h.Sum(nil)
+}
+
+func Sha1Raw(data []byte) []byte {
+ h := sha1.New()
+ h.Write(data)
+ return h.Sum(nil)
+}
+
+func Sha1(data []byte) string {
+ return hex.EncodeToString(Sha1Raw(data))
+}
+
+func HmacSha256Raw(message, key []byte) []byte {
+ h := hmac.New(sha256.New, key)
+ h.Write(message)
+ return h.Sum(nil)
+}
+
+func HmacSha256(message, key string) string {
+ return hex.EncodeToString(HmacSha256Raw([]byte(message), []byte(key)))
+}
diff --git a/common/json.go b/common/json.go
index 512ad0c3012..69aa952e954 100644
--- a/common/json.go
+++ b/common/json.go
@@ -5,7 +5,7 @@ import (
"encoding/json"
)
-func UnmarshalJson(data []byte, v any) error {
+func Unmarshal(data []byte, v any) error {
return json.Unmarshal(data, v)
}
@@ -17,6 +17,6 @@ func DecodeJson(reader *bytes.Reader, v any) error {
return json.NewDecoder(reader).Decode(v)
}
-func EncodeJson(v any) ([]byte, error) {
+func Marshal(v any) ([]byte, error) {
return json.Marshal(v)
}
diff --git a/common/logger.go b/common/logger.go
index 86d15fa4db7..0f6dc3c3b56 100644
--- a/common/logger.go
+++ b/common/logger.go
@@ -75,6 +75,9 @@ func logHelper(ctx context.Context, level string, msg string) {
writer = gin.DefaultWriter
}
id := ctx.Value(RequestIdKey)
+ if id == nil {
+ id = "SYSTEM"
+ }
now := time.Now()
_, _ = fmt.Fprintf(writer, "[%s] %v | %s | %s \n", level, now.Format("2006/01/02 - 15:04:05"), id, msg)
logCount++ // we don't need accurate count, so no lock here
diff --git a/common/page_info.go b/common/page_info.go
index 20a90fa298e..5e4535e3d51 100644
--- a/common/page_info.go
+++ b/common/page_info.go
@@ -1,15 +1,14 @@
package common
import (
- "github.com/gin-gonic/gin"
"strconv"
+
+ "github.com/gin-gonic/gin"
)
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"` // 秒级
+ Page int `json:"page"` // page num 页码
+ PageSize int `json:"page_size"` // page size 页大小
Total int `json:"total"` // 总条数,后设置
Items any `json:"items"` // 数据,后设置
@@ -39,11 +38,14 @@ func (p *PageInfo) SetItems(items any) {
p.Items = items
}
-func GetPageQuery(c *gin.Context) (*PageInfo, error) {
+func GetPageQuery(c *gin.Context) *PageInfo {
pageInfo := &PageInfo{}
- err := c.BindQuery(pageInfo)
- if err != nil {
- return nil, err
+ // 手动获取并处理每个参数
+ if page, err := strconv.Atoi(c.Query("page")); err == nil {
+ pageInfo.Page = page
+ }
+ if pageSize, err := strconv.Atoi(c.Query("page_size")); err == nil {
+ pageInfo.PageSize = pageSize
}
if pageInfo.Page < 1 {
// 兼容
@@ -56,7 +58,25 @@ func GetPageQuery(c *gin.Context) (*PageInfo, error) {
}
if pageInfo.PageSize == 0 {
- pageInfo.PageSize = ItemsPerPage
+ // 兼容
+ pageSize, _ := strconv.Atoi(c.Query("ps"))
+ if pageSize != 0 {
+ pageInfo.PageSize = pageSize
+ }
+ if pageInfo.PageSize == 0 {
+ pageSize, _ = strconv.Atoi(c.Query("size")) // token page
+ if pageSize != 0 {
+ pageInfo.PageSize = pageSize
+ }
+ }
+ if pageInfo.PageSize == 0 {
+ pageInfo.PageSize = ItemsPerPage
+ }
}
- return pageInfo, nil
+
+ if pageInfo.PageSize > 100 {
+ pageInfo.PageSize = 100
+ }
+
+ return pageInfo
}
diff --git a/common/str.go b/common/str.go
index 76dd801ad35..88b58c7206f 100644
--- a/common/str.go
+++ b/common/str.go
@@ -32,16 +32,30 @@ func MapToJsonStr(m map[string]interface{}) string {
return string(bytes)
}
-func StrToMap(str string) map[string]interface{} {
+func StrToMap(str string) (map[string]interface{}, error) {
m := make(map[string]interface{})
- err := json.Unmarshal([]byte(str), &m)
+ err := Unmarshal([]byte(str), &m)
if err != nil {
- return nil
+ return nil, err
}
- return m
+ return m, nil
}
-func IsJsonStr(str string) bool {
+func StrToJsonArray(str string) ([]interface{}, error) {
+ var js []interface{}
+ err := json.Unmarshal([]byte(str), &js)
+ if err != nil {
+ return nil, err
+ }
+ return js, nil
+}
+
+func IsJsonArray(str string) bool {
+ var js []interface{}
+ return json.Unmarshal([]byte(str), &js) == nil
+}
+
+func IsJsonObject(str string) bool {
var js map[string]interface{}
return json.Unmarshal([]byte(str), &js) == nil
}
diff --git a/constant/api_type.go b/constant/api_type.go
index ae867870c95..6ba5f2574a1 100644
--- a/constant/api_type.go
+++ b/constant/api_type.go
@@ -30,5 +30,6 @@ const (
APITypeXinference
APITypeXai
APITypeCoze
+ APITypeJimeng
APITypeDummy // this one is only for count, do not add any channel after this
)
diff --git a/constant/context_key.go b/constant/context_key.go
index 71e02f01613..4eaf3d00754 100644
--- a/constant/context_key.go
+++ b/constant/context_key.go
@@ -17,11 +17,20 @@ const (
ContextKeyTokenModelLimit ContextKey = "token_model_limit"
/* channel related keys */
- ContextKeyBaseUrl ContextKey = "base_url"
- ContextKeyChannelType ContextKey = "channel_type"
- ContextKeyChannelId ContextKey = "channel_id"
- ContextKeyChannelSetting ContextKey = "channel_setting"
- ContextKeyParamOverride ContextKey = "param_override"
+ ContextKeyChannelId ContextKey = "channel_id"
+ ContextKeyChannelName ContextKey = "channel_name"
+ ContextKeyChannelCreateTime ContextKey = "channel_create_time"
+ ContextKeyChannelBaseUrl ContextKey = "base_url"
+ ContextKeyChannelType ContextKey = "channel_type"
+ ContextKeyChannelSetting ContextKey = "channel_setting"
+ ContextKeyChannelParamOverride ContextKey = "param_override"
+ ContextKeyChannelOrganization ContextKey = "channel_organization"
+ ContextKeyChannelAutoBan ContextKey = "auto_ban"
+ ContextKeyChannelModelMapping ContextKey = "model_mapping"
+ ContextKeyChannelStatusCodeMapping ContextKey = "status_code_mapping"
+ ContextKeyChannelIsMultiKey ContextKey = "channel_is_multi_key"
+ ContextKeyChannelMultiKeyIndex ContextKey = "channel_multi_key_index"
+ ContextKeyChannelKey ContextKey = "channel_key"
/* user related keys */
ContextKeyUserId ContextKey = "id"
diff --git a/constant/multi_key_mode.go b/constant/multi_key_mode.go
new file mode 100644
index 00000000000..cd0cdbff5fc
--- /dev/null
+++ b/constant/multi_key_mode.go
@@ -0,0 +1,8 @@
+package constant
+
+type MultiKeyMode string
+
+const (
+ MultiKeyModeRandom MultiKeyMode = "random" // 随机
+ MultiKeyModePolling MultiKeyMode = "polling" // 轮询
+)
diff --git a/controller/channel-billing.go b/controller/channel-billing.go
index 3c92c78be7b..5152e0608fd 100644
--- a/controller/channel-billing.go
+++ b/controller/channel-billing.go
@@ -4,7 +4,6 @@ import (
"encoding/json"
"errors"
"fmt"
- "github.com/shopspring/decimal"
"io"
"net/http"
"one-api/common"
@@ -12,9 +11,12 @@ import (
"one-api/model"
"one-api/service"
"one-api/setting"
+ "one-api/types"
"strconv"
"time"
+ "github.com/shopspring/decimal"
+
"github.com/gin-gonic/gin"
)
@@ -409,26 +411,24 @@ func updateChannelBalance(channel *model.Channel) (float64, error) {
func UpdateChannelBalance(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
- channel, err := model.GetChannelById(id, true)
+ channel, err := model.CacheGetChannel(id)
if err != nil {
+ common.ApiError(c, err)
+ return
+ }
+ if channel.ChannelInfo.IsMultiKey {
c.JSON(http.StatusOK, gin.H{
"success": false,
- "message": err.Error(),
+ "message": "多密钥渠道不支持余额查询",
})
return
}
balance, err := updateChannelBalance(channel)
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
@@ -436,7 +436,6 @@ func UpdateChannelBalance(c *gin.Context) {
"message": "",
"balance": balance,
})
- return
}
func updateAllChannelsBalance() error {
@@ -448,6 +447,9 @@ func updateAllChannelsBalance() error {
if channel.Status != common.ChannelStatusEnabled {
continue
}
+ if channel.ChannelInfo.IsMultiKey {
+ continue // skip multi-key channels
+ }
// TODO: support Azure
//if channel.Type != common.ChannelTypeOpenAI && channel.Type != common.ChannelTypeCustom {
// continue
@@ -458,7 +460,7 @@ func updateAllChannelsBalance() error {
} else {
// err is nil & balance <= 0 means quota is used up
if balance <= 0 {
- service.DisableChannel(channel.Id, channel.Name, "余额不足")
+ service.DisableChannel(*types.NewChannelError(channel.Id, channel.Type, channel.Name, channel.ChannelInfo.IsMultiKey, "", channel.GetAutoBan()), "余额不足")
}
}
time.Sleep(common.RequestInterval)
@@ -470,10 +472,7 @@ func UpdateAllChannelsBalance(c *gin.Context) {
// TODO: make it async
err := updateAllChannelsBalance()
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
diff --git a/controller/channel-test.go b/controller/channel-test.go
index 009219a7b44..8c4a26ae67f 100644
--- a/controller/channel-test.go
+++ b/controller/channel-test.go
@@ -17,8 +17,10 @@ import (
"one-api/model"
"one-api/relay"
relaycommon "one-api/relay/common"
+ relayconstant "one-api/relay/constant"
"one-api/relay/helper"
"one-api/service"
+ "one-api/types"
"strconv"
"strings"
"sync"
@@ -29,22 +31,43 @@ import (
"github.com/gin-gonic/gin"
)
-func testChannel(channel *model.Channel, testModel string) (err error, openAIErrorWithStatusCode *dto.OpenAIErrorWithStatusCode) {
+type testResult struct {
+ context *gin.Context
+ localErr error
+ newAPIError *types.NewAPIError
+}
+
+func testChannel(channel *model.Channel, testModel string) testResult {
tik := time.Now()
if channel.Type == constant.ChannelTypeMidjourney {
- return errors.New("midjourney channel test is not supported"), nil
+ return testResult{
+ localErr: errors.New("midjourney channel test is not supported"),
+ newAPIError: nil,
+ }
}
if channel.Type == constant.ChannelTypeMidjourneyPlus {
- return errors.New("midjourney plus channel test is not supported"), nil
+ return testResult{
+ localErr: errors.New("midjourney plus channel test is not supported"),
+ newAPIError: nil,
+ }
}
if channel.Type == constant.ChannelTypeSunoAPI {
- return errors.New("suno channel test is not supported"), nil
+ return testResult{
+ localErr: errors.New("suno channel test is not supported"),
+ newAPIError: nil,
+ }
}
if channel.Type == constant.ChannelTypeKling {
- return errors.New("kling channel test is not supported"), nil
+ return testResult{
+ localErr: errors.New("kling channel test is not supported"),
+ newAPIError: nil,
+ }
}
if channel.Type == constant.ChannelTypeJimeng {
- return errors.New("jimeng channel test is not supported"), nil
+ return testResult{
+ localErr: errors.New("jimeng channel test is not supported"),
+ newAPIError: nil,
+ }
}
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
@@ -81,31 +104,49 @@ func testChannel(channel *model.Channel, testModel string) (err error, openAIErr
cache, err := model.GetUserCache(1)
if err != nil {
- return err, nil
+ return testResult{
+ localErr: err,
+ newAPIError: nil,
+ }
}
cache.WriteContext(c)
- c.Request.Header.Set("Authorization", "Bearer "+channel.Key)
+ //c.Request.Header.Set("Authorization", "Bearer "+channel.Key)
c.Request.Header.Set("Content-Type", "application/json")
c.Set("channel", channel.Type)
c.Set("base_url", channel.GetBaseURL())
group, _ := model.GetUserGroup(1, false)
c.Set("group", group)
- middleware.SetupContextForSelectedChannel(c, channel, testModel)
+ newAPIError := middleware.SetupContextForSelectedChannel(c, channel, testModel)
+ if newAPIError != nil {
+ return testResult{
+ context: c,
+ localErr: newAPIError,
+ newAPIError: newAPIError,
+ }
+ }
info := relaycommon.GenRelayInfo(c)
err = helper.ModelMappedHelper(c, info, nil)
if err != nil {
- return err, nil
+ return testResult{
+ context: c,
+ localErr: err,
+ newAPIError: types.NewError(err, types.ErrorCodeChannelModelMappedError),
+ }
}
testModel = info.UpstreamModelName
apiType, _ := common.ChannelType2APIType(channel.Type)
adaptor := relay.GetAdaptor(apiType)
if adaptor == nil {
- return fmt.Errorf("invalid api type: %d, adaptor is nil", apiType), nil
+ return testResult{
+ context: c,
+ localErr: fmt.Errorf("invalid api type: %d, adaptor is nil", apiType),
+ newAPIError: types.NewError(fmt.Errorf("invalid api type: %d, adaptor is nil", apiType), types.ErrorCodeInvalidApiType),
+ }
}
request := buildTestRequest(testModel)
@@ -116,45 +157,91 @@ func testChannel(channel *model.Channel, testModel string) (err error, openAIErr
priceData, err := helper.ModelPriceHelper(c, info, 0, int(request.MaxTokens))
if err != nil {
- return err, nil
+ return testResult{
+ context: c,
+ localErr: err,
+ newAPIError: types.NewError(err, types.ErrorCodeModelPriceError),
+ }
}
adaptor.Init(info)
- convertedRequest, err := adaptor.ConvertOpenAIRequest(c, info, request)
+ var convertedRequest any
+ // 根据 RelayMode 选择正确的转换函数
+ if info.RelayMode == relayconstant.RelayModeEmbeddings {
+ // 创建一个 EmbeddingRequest
+ embeddingRequest := dto.EmbeddingRequest{
+ Input: request.Input,
+ Model: request.Model,
+ }
+ // 调用专门用于 Embedding 的转换函数
+ convertedRequest, err = adaptor.ConvertEmbeddingRequest(c, info, embeddingRequest)
+ } else {
+ // 对其他所有请求类型(如 Chat),保持原有逻辑
+ convertedRequest, err = adaptor.ConvertOpenAIRequest(c, info, request)
+ }
+
if err != nil {
- return err, nil
+ return testResult{
+ context: c,
+ localErr: err,
+ newAPIError: types.NewError(err, types.ErrorCodeConvertRequestFailed),
+ }
}
jsonData, err := json.Marshal(convertedRequest)
if err != nil {
- return err, nil
+ return testResult{
+ context: c,
+ localErr: err,
+ newAPIError: types.NewError(err, types.ErrorCodeJsonMarshalFailed),
+ }
}
requestBody := bytes.NewBuffer(jsonData)
c.Request.Body = io.NopCloser(requestBody)
resp, err := adaptor.DoRequest(c, info, requestBody)
if err != nil {
- return err, nil
+ return testResult{
+ context: c,
+ localErr: err,
+ newAPIError: types.NewError(err, types.ErrorCodeDoRequestFailed),
+ }
}
var httpResp *http.Response
if resp != nil {
httpResp = resp.(*http.Response)
if httpResp.StatusCode != http.StatusOK {
err := service.RelayErrorHandler(httpResp, true)
- return fmt.Errorf("status code %d: %s", httpResp.StatusCode, err.Error.Message), err
+ return testResult{
+ context: c,
+ localErr: err,
+ newAPIError: types.NewError(err, types.ErrorCodeBadResponse),
+ }
}
}
usageA, respErr := adaptor.DoResponse(c, httpResp, info)
if respErr != nil {
- return fmt.Errorf("%s", respErr.Error.Message), respErr
+ return testResult{
+ context: c,
+ localErr: respErr,
+ newAPIError: respErr,
+ }
}
if usageA == nil {
- return errors.New("usage is nil"), nil
+ return testResult{
+ context: c,
+ localErr: errors.New("usage is nil"),
+ newAPIError: types.NewError(errors.New("usage is nil"), types.ErrorCodeBadResponseBody),
+ }
}
usage := usageA.(*dto.Usage)
result := w.Result()
respBody, err := io.ReadAll(result.Body)
if err != nil {
- return err, nil
+ return testResult{
+ context: c,
+ localErr: err,
+ newAPIError: types.NewError(err, types.ErrorCodeReadResponseBodyFailed),
+ }
}
info.PromptTokens = usage.PromptTokens
@@ -187,7 +274,11 @@ func testChannel(channel *model.Channel, testModel string) (err error, openAIErr
Other: other,
})
common.SysLog(fmt.Sprintf("testing channel #%d, response: \n%s", channel.Id, string(respBody)))
- return nil, nil
+ return testResult{
+ context: c,
+ localErr: nil,
+ newAPIError: nil,
+ }
}
func buildTestRequest(model string) *dto.GeneralOpenAIRequest {
@@ -202,7 +293,7 @@ func buildTestRequest(model string) *dto.GeneralOpenAIRequest {
strings.Contains(model, "bge-") {
testRequest.Model = model
// Embedding 请求
- testRequest.Input = []string{"hello world"}
+ testRequest.Input = []any{"hello world"} // 修改为any,因为dto/openai_request.go 的ParseInput方法无法处理[]string类型
return testRequest
}
// 并非Embedding 模型
@@ -230,31 +321,38 @@ func buildTestRequest(model string) *dto.GeneralOpenAIRequest {
func TestChannel(c *gin.Context) {
channelId, err := strconv.Atoi(c.Param("id"))
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
- channel, err := model.GetChannelById(channelId, true)
+ channel, err := model.CacheGetChannel(channelId)
if err != nil {
+ common.ApiError(c, err)
+ return
+ }
+ //defer func() {
+ // if channel.ChannelInfo.IsMultiKey {
+ // go func() { _ = channel.SaveChannelInfo() }()
+ // }
+ //}()
+ testModel := c.Query("model")
+ tik := time.Now()
+ result := testChannel(channel, testModel)
+ if result.localErr != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
- "message": err.Error(),
+ "message": result.localErr.Error(),
+ "time": 0.0,
})
return
}
- testModel := c.Query("model")
- tik := time.Now()
- err, _ = testChannel(channel, testModel)
tok := time.Now()
milliseconds := tok.Sub(tik).Milliseconds()
go channel.UpdateResponseTime(milliseconds)
consumedTime := float64(milliseconds) / 1000.0
- if err != nil {
+ if result.newAPIError != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
- "message": err.Error(),
+ "message": result.newAPIError.Error(),
"time": consumedTime,
})
return
@@ -279,9 +377,9 @@ func testAllChannels(notify bool) error {
}
testAllChannelsRunning = true
testAllChannelsLock.Unlock()
- channels, err := model.GetAllChannels(0, 0, true, false)
- if err != nil {
- return err
+ channels, getChannelErr := model.GetAllChannels(0, 0, true, false)
+ if getChannelErr != nil {
+ return getChannelErr
}
var disableThreshold = int64(common.ChannelDisableThreshold * 1000)
if disableThreshold == 0 {
@@ -298,32 +396,34 @@ func testAllChannels(notify bool) error {
for _, channel := range channels {
isChannelEnabled := channel.Status == common.ChannelStatusEnabled
tik := time.Now()
- err, openaiWithStatusErr := testChannel(channel, "")
+ result := testChannel(channel, "")
tok := time.Now()
milliseconds := tok.Sub(tik).Milliseconds()
shouldBanChannel := false
-
+ newAPIError := result.newAPIError
// request error disables the channel
- if openaiWithStatusErr != nil {
- oaiErr := openaiWithStatusErr.Error
- err = errors.New(fmt.Sprintf("type %s, httpCode %d, code %v, message %s", oaiErr.Type, openaiWithStatusErr.StatusCode, oaiErr.Code, oaiErr.Message))
- shouldBanChannel = service.ShouldDisableChannel(channel.Type, openaiWithStatusErr)
+ if newAPIError != nil {
+ shouldBanChannel = service.ShouldDisableChannel(channel.Type, result.newAPIError)
}
- if milliseconds > disableThreshold {
- err = errors.New(fmt.Sprintf("响应时间 %.2fs 超过阈值 %.2fs", float64(milliseconds)/1000.0, float64(disableThreshold)/1000.0))
- shouldBanChannel = true
+ // 当错误检查通过,才检查响应时间
+ if common.AutomaticDisableChannelEnabled && !shouldBanChannel {
+ if milliseconds > disableThreshold {
+ err := errors.New(fmt.Sprintf("响应时间 %.2fs 超过阈值 %.2fs", float64(milliseconds)/1000.0, float64(disableThreshold)/1000.0))
+ newAPIError = types.NewError(err, types.ErrorCodeChannelResponseTimeExceeded)
+ shouldBanChannel = true
+ }
}
// disable channel
if isChannelEnabled && shouldBanChannel && channel.GetAutoBan() {
- service.DisableChannel(channel.Id, channel.Name, err.Error())
+ go processChannelError(result.context, *types.NewChannelError(channel.Id, channel.Type, channel.Name, channel.ChannelInfo.IsMultiKey, common.GetContextKeyString(result.context, constant.ContextKeyChannelKey), channel.GetAutoBan()), newAPIError)
}
// enable channel
- if !isChannelEnabled && service.ShouldEnableChannel(err, openaiWithStatusErr, channel.Status) {
- service.EnableChannel(channel.Id, channel.Name)
+ if !isChannelEnabled && service.ShouldEnableChannel(newAPIError, channel.Status) {
+ service.EnableChannel(channel.Id, common.GetContextKeyString(result.context, constant.ContextKeyChannelKey), channel.Name)
}
channel.UpdateResponseTime(milliseconds)
@@ -340,10 +440,7 @@ func testAllChannels(notify bool) error {
func TestAllChannels(c *gin.Context) {
err := testAllChannels(true)
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
@@ -354,6 +451,10 @@ func TestAllChannels(c *gin.Context) {
}
func AutomaticallyTestChannels(frequency int) {
+ if frequency <= 0 {
+ common.SysLog("CHANNEL_TEST_FREQUENCY is not set or invalid, skipping automatic channel test")
+ return
+ }
for {
time.Sleep(time.Duration(frequency) * time.Minute)
common.SysLog("testing all channels")
diff --git a/controller/channel.go b/controller/channel.go
index 85b14b43ce3..d3bfa202ac3 100644
--- a/controller/channel.go
+++ b/controller/channel.go
@@ -53,14 +53,7 @@ func parseStatusFilter(statusParam string) int {
}
func GetAllChannels(c *gin.Context) {
- p, _ := strconv.Atoi(c.Query("p"))
- pageSize, _ := strconv.Atoi(c.Query("page_size"))
- if p < 1 {
- p = 1
- }
- if pageSize < 1 {
- pageSize = common.ItemsPerPage
- }
+ pageInfo := common.GetPageQuery(c)
channelData := make([]*model.Channel, 0)
idSort, _ := strconv.ParseBool(c.Query("id_sort"))
enableTagMode, _ := strconv.ParseBool(c.Query("tag_mode"))
@@ -79,7 +72,7 @@ func GetAllChannels(c *gin.Context) {
var total int64
if enableTagMode {
- tags, err := model.GetPaginatedTags((p-1)*pageSize, pageSize)
+ tags, err := model.GetPaginatedTags(pageInfo.GetStartIdx(), pageInfo.GetPageSize())
if err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "message": err.Error()})
return
@@ -126,7 +119,7 @@ func GetAllChannels(c *gin.Context) {
order = "id desc"
}
- err := baseQuery.Order(order).Limit(pageSize).Offset((p - 1) * pageSize).Omit("key").Find(&channelData).Error
+ err := baseQuery.Order(order).Limit(pageInfo.GetPageSize()).Offset(pageInfo.GetStartIdx()).Omit("key").Find(&channelData).Error
if err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "message": err.Error()})
return
@@ -148,17 +141,12 @@ func GetAllChannels(c *gin.Context) {
for _, r := range results {
typeCounts[r.Type] = r.Count
}
-
- c.JSON(http.StatusOK, gin.H{
- "success": true,
- "message": "",
- "data": gin.H{
- "items": channelData,
- "total": total,
- "page": p,
- "page_size": pageSize,
- "type_counts": typeCounts,
- },
+ common.ApiSuccess(c, gin.H{
+ "items": channelData,
+ "total": total,
+ "page": pageInfo.GetPage(),
+ "page_size": pageInfo.GetPageSize(),
+ "type_counts": typeCounts,
})
return
}
@@ -166,19 +154,13 @@ func GetAllChannels(c *gin.Context) {
func FetchUpstreamModels(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
channel, err := model.GetChannelById(id, true)
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
@@ -195,10 +177,7 @@ func FetchUpstreamModels(c *gin.Context) {
}
body, err := GetResponseBody("GET", url, channel, GetAuthHeader(channel.Key))
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
@@ -230,10 +209,7 @@ func FetchUpstreamModels(c *gin.Context) {
func FixChannelsAbilities(c *gin.Context) {
success, fails, err := model.FixAbility()
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
@@ -358,18 +334,12 @@ func SearchChannels(c *gin.Context) {
func GetChannel(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
channel, err := model.GetChannelById(id, false)
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
@@ -380,74 +350,167 @@ func GetChannel(c *gin.Context) {
return
}
+// validateChannel 通用的渠道校验函数
+func validateChannel(channel *model.Channel, isAdd bool) error {
+ // 校验 channel settings
+ if err := channel.ValidateSettings(); err != nil {
+ return fmt.Errorf("渠道额外设置[channel setting] 格式错误:%s", err.Error())
+ }
+
+ // 如果是添加操作,检查 channel 和 key 是否为空
+ if isAdd {
+ if channel == nil || channel.Key == "" {
+ return fmt.Errorf("channel cannot be empty")
+ }
+
+ // 检查模型名称长度是否超过 255
+ for _, m := range channel.GetModels() {
+ if len(m) > 255 {
+ return fmt.Errorf("模型名称过长: %s", m)
+ }
+ }
+ }
+
+ // VertexAI 特殊校验
+ if channel.Type == constant.ChannelTypeVertexAi {
+ if channel.Other == "" {
+ return fmt.Errorf("部署地区不能为空")
+ }
+
+ regionMap, err := common.StrToMap(channel.Other)
+ if err != nil {
+ return fmt.Errorf("部署地区必须是标准的Json格式,例如{\"default\": \"us-central1\", \"region2\": \"us-east1\"}")
+ }
+
+ if regionMap["default"] == nil {
+ return fmt.Errorf("部署地区必须包含default字段")
+ }
+ }
+
+ return nil
+}
+
+type AddChannelRequest struct {
+ Mode string `json:"mode"`
+ MultiKeyMode constant.MultiKeyMode `json:"multi_key_mode"`
+ Channel *model.Channel `json:"channel"`
+}
+
+func getVertexArrayKeys(keys string) ([]string, error) {
+ if keys == "" {
+ return nil, nil
+ }
+ var keyArray []interface{}
+ err := common.Unmarshal([]byte(keys), &keyArray)
+ if err != nil {
+ return nil, fmt.Errorf("批量添加 Vertex AI 必须使用标准的JsonArray格式,例如[{key1}, {key2}...],请检查输入: %w", err)
+ }
+ cleanKeys := make([]string, 0, len(keyArray))
+ for _, key := range keyArray {
+ var keyStr string
+ switch v := key.(type) {
+ case string:
+ keyStr = strings.TrimSpace(v)
+ default:
+ bytes, err := json.Marshal(v)
+ if err != nil {
+ return nil, fmt.Errorf("Vertex AI key JSON 编码失败: %w", err)
+ }
+ keyStr = string(bytes)
+ }
+ if keyStr != "" {
+ cleanKeys = append(cleanKeys, keyStr)
+ }
+ }
+ if len(cleanKeys) == 0 {
+ return nil, fmt.Errorf("批量添加 Vertex AI 的 keys 不能为空")
+ }
+ return cleanKeys, nil
+}
+
func AddChannel(c *gin.Context) {
- channel := model.Channel{}
- err := c.ShouldBindJSON(&channel)
+ addChannelRequest := AddChannelRequest{}
+ err := c.ShouldBindJSON(&addChannelRequest)
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
- err = channel.ValidateSettings()
- if err != nil {
+
+ // 使用统一的校验函数
+ if err := validateChannel(addChannelRequest.Channel, true); err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
- "message": "channel setting 格式错误:" + err.Error(),
+ "message": err.Error(),
})
return
}
- channel.CreatedTime = common.GetTimestamp()
- keys := strings.Split(channel.Key, "\n")
- if channel.Type == constant.ChannelTypeVertexAi {
- if channel.Other == "" {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": "部署地区不能为空",
- })
- return
+
+ addChannelRequest.Channel.CreatedTime = common.GetTimestamp()
+ keys := make([]string, 0)
+ switch addChannelRequest.Mode {
+ case "multi_to_single":
+ addChannelRequest.Channel.ChannelInfo.IsMultiKey = true
+ addChannelRequest.Channel.ChannelInfo.MultiKeyMode = addChannelRequest.MultiKeyMode
+ if addChannelRequest.Channel.Type == constant.ChannelTypeVertexAi {
+ array, err := getVertexArrayKeys(addChannelRequest.Channel.Key)
+ if err != nil {
+ c.JSON(http.StatusOK, gin.H{
+ "success": false,
+ "message": err.Error(),
+ })
+ return
+ }
+ addChannelRequest.Channel.ChannelInfo.MultiKeySize = len(array)
+ addChannelRequest.Channel.Key = strings.Join(array, "\n")
} else {
- if common.IsJsonStr(channel.Other) {
- // must have default
- regionMap := common.StrToMap(channel.Other)
- if regionMap["default"] == nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": "部署地区必须包含default字段",
- })
- return
+ cleanKeys := make([]string, 0)
+ for _, key := range strings.Split(addChannelRequest.Channel.Key, "\n") {
+ if key == "" {
+ continue
}
+ key = strings.TrimSpace(key)
+ cleanKeys = append(cleanKeys, key)
+ }
+ addChannelRequest.Channel.ChannelInfo.MultiKeySize = len(cleanKeys)
+ addChannelRequest.Channel.Key = strings.Join(cleanKeys, "\n")
+ }
+ keys = []string{addChannelRequest.Channel.Key}
+ case "batch":
+ if addChannelRequest.Channel.Type == constant.ChannelTypeVertexAi {
+ // multi json
+ keys, err = getVertexArrayKeys(addChannelRequest.Channel.Key)
+ if err != nil {
+ c.JSON(http.StatusOK, gin.H{
+ "success": false,
+ "message": err.Error(),
+ })
+ return
}
+ } else {
+ keys = strings.Split(addChannelRequest.Channel.Key, "\n")
}
- keys = []string{channel.Key}
+ case "single":
+ keys = []string{addChannelRequest.Channel.Key}
+ default:
+ c.JSON(http.StatusOK, gin.H{
+ "success": false,
+ "message": "不支持的添加模式",
+ })
+ return
}
+
channels := make([]model.Channel, 0, len(keys))
for _, key := range keys {
if key == "" {
continue
}
- localChannel := channel
+ localChannel := addChannelRequest.Channel
localChannel.Key = key
- // Validate the length of the model name
- models := strings.Split(localChannel.Models, ",")
- for _, model := range models {
- if len(model) > 255 {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": fmt.Sprintf("模型名称过长: %s", model),
- })
- return
- }
- }
- channels = append(channels, localChannel)
+ channels = append(channels, *localChannel)
}
err = model.BatchInsertChannels(channels)
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
@@ -462,12 +525,10 @@ func DeleteChannel(c *gin.Context) {
channel := model.Channel{Id: id}
err := channel.Delete()
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
+ model.InitChannelCache()
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
@@ -478,12 +539,10 @@ func DeleteChannel(c *gin.Context) {
func DeleteDisabledChannel(c *gin.Context) {
rows, err := model.DeleteDisabledChannel()
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
+ model.InitChannelCache()
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
@@ -514,12 +573,10 @@ func DisableTagChannels(c *gin.Context) {
}
err = model.DisableChannelByTag(channelTag.Tag)
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
+ model.InitChannelCache()
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
@@ -539,12 +596,10 @@ func EnableTagChannels(c *gin.Context) {
}
err = model.EnableChannelByTag(channelTag.Tag)
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
+ model.InitChannelCache()
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
@@ -571,12 +626,10 @@ func EditTagChannels(c *gin.Context) {
}
err = model.EditChannelByTag(channelTag.Tag, channelTag.NewTag, channelTag.ModelMapping, channelTag.Models, channelTag.Groups, channelTag.Priority, channelTag.Weight)
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
+ model.InitChannelCache()
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
@@ -601,12 +654,10 @@ func DeleteChannelBatch(c *gin.Context) {
}
err = model.BatchDeleteChannels(channelBatch.Ids)
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
+ model.InitChannelCache()
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
@@ -615,53 +666,50 @@ func DeleteChannelBatch(c *gin.Context) {
return
}
+type PatchChannel struct {
+ model.Channel
+ MultiKeyMode *string `json:"multi_key_mode"`
+}
+
func UpdateChannel(c *gin.Context) {
- channel := model.Channel{}
+ channel := PatchChannel{}
err := c.ShouldBindJSON(&channel)
if err != nil {
+ common.ApiError(c, err)
+ return
+ }
+
+ // 使用统一的校验函数
+ if err := validateChannel(&channel.Channel, false); err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
})
return
}
- err = channel.ValidateSettings()
+ // Preserve existing ChannelInfo to ensure multi-key channels keep correct state even if the client does not send ChannelInfo in the request.
+ originChannel, err := model.GetChannelById(channel.Id, false)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
- "message": "channel setting 格式错误:" + err.Error(),
+ "message": err.Error(),
})
return
}
- if channel.Type == constant.ChannelTypeVertexAi {
- if channel.Other == "" {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": "部署地区不能为空",
- })
- return
- } else {
- if common.IsJsonStr(channel.Other) {
- // must have default
- regionMap := common.StrToMap(channel.Other)
- if regionMap["default"] == nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": "部署地区必须包含default字段",
- })
- return
- }
- }
- }
+
+ // Always copy the original ChannelInfo so that fields like IsMultiKey and MultiKeySize are retained.
+ channel.ChannelInfo = originChannel.ChannelInfo
+
+ // If the request explicitly specifies a new MultiKeyMode, apply it on top of the original info.
+ if channel.MultiKeyMode != nil && *channel.MultiKeyMode != "" {
+ channel.ChannelInfo.MultiKeyMode = constant.MultiKeyMode(*channel.MultiKeyMode)
}
err = channel.Update()
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
+ model.InitChannelCache()
channel.Key = ""
c.JSON(http.StatusOK, gin.H{
"success": true,
@@ -764,12 +812,10 @@ func BatchSetChannelTag(c *gin.Context) {
}
err = model.BatchSetChannelTag(channelBatch.Ids, channelBatch.Tag)
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
+ model.InitChannelCache()
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
@@ -818,3 +864,53 @@ func GetTagModels(c *gin.Context) {
})
return
}
+
+// CopyChannel handles cloning an existing channel with its key.
+// POST /api/channel/copy/:id
+// Optional query params:
+//
+// suffix - string appended to the original name (default "_复制")
+// reset_balance - bool, when true will reset balance & used_quota to 0 (default true)
+func CopyChannel(c *gin.Context) {
+ id, err := strconv.Atoi(c.Param("id"))
+ if err != nil {
+ c.JSON(http.StatusOK, gin.H{"success": false, "message": "invalid id"})
+ return
+ }
+
+ suffix := c.DefaultQuery("suffix", "_复制")
+ resetBalance := true
+ if rbStr := c.DefaultQuery("reset_balance", "true"); rbStr != "" {
+ if v, err := strconv.ParseBool(rbStr); err == nil {
+ resetBalance = v
+ }
+ }
+
+ // fetch original channel with key
+ origin, err := model.GetChannelById(id, true)
+ if err != nil {
+ c.JSON(http.StatusOK, gin.H{"success": false, "message": err.Error()})
+ return
+ }
+
+ // clone channel
+ clone := *origin // shallow copy is sufficient as we will overwrite primitives
+ clone.Id = 0 // let DB auto-generate
+ clone.CreatedTime = common.GetTimestamp()
+ clone.Name = origin.Name + suffix
+ clone.TestTime = 0
+ clone.ResponseTime = 0
+ if resetBalance {
+ clone.Balance = 0
+ clone.UsedQuota = 0
+ }
+
+ // insert
+ if err := model.BatchInsertChannels([]model.Channel{clone}); err != nil {
+ c.JSON(http.StatusOK, gin.H{"success": false, "message": err.Error()})
+ return
+ }
+ model.InitChannelCache()
+ // success
+ c.JSON(http.StatusOK, gin.H{"success": true, "message": "", "data": gin.H{"id": clone.Id}})
+}
diff --git a/controller/github.go b/controller/github.go
index 7971184195e..881d6dc163a 100644
--- a/controller/github.go
+++ b/controller/github.go
@@ -5,13 +5,14 @@ import (
"encoding/json"
"errors"
"fmt"
- "github.com/gin-contrib/sessions"
- "github.com/gin-gonic/gin"
"net/http"
"one-api/common"
"one-api/model"
"strconv"
"time"
+
+ "github.com/gin-contrib/sessions"
+ "github.com/gin-gonic/gin"
)
type GitHubOAuthResponse struct {
@@ -103,10 +104,7 @@ func GitHubOAuth(c *gin.Context) {
code := c.Query("code")
githubUser, err := getGitHubUserInfoByCode(code)
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
user := model.User{
@@ -185,10 +183,7 @@ func GitHubBind(c *gin.Context) {
code := c.Query("code")
githubUser, err := getGitHubUserInfoByCode(code)
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
user := model.User{
@@ -207,19 +202,13 @@ func GitHubBind(c *gin.Context) {
user.Id = id.(int)
err = user.FillUserById()
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
user.GitHubId = githubUser.Login
err = user.Update(false)
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
@@ -239,10 +228,7 @@ func GenerateOAuthCode(c *gin.Context) {
session.Set("oauth_state", state)
err := session.Save()
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
diff --git a/controller/linuxdo.go b/controller/linuxdo.go
index 2cdb3517801..65380b65a52 100644
--- a/controller/linuxdo.go
+++ b/controller/linuxdo.go
@@ -38,10 +38,7 @@ func LinuxDoBind(c *gin.Context) {
code := c.Query("code")
linuxdoUser, err := getLinuxdoUserInfoByCode(code, c)
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
@@ -63,20 +60,14 @@ func LinuxDoBind(c *gin.Context) {
err = user.FillUserById()
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
user.LinuxDOId = strconv.Itoa(linuxdoUser.Id)
err = user.Update(false)
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
@@ -202,10 +193,7 @@ func LinuxdoOAuth(c *gin.Context) {
code := c.Query("code")
linuxdoUser, err := getLinuxdoUserInfoByCode(code, c)
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
diff --git a/controller/log.go b/controller/log.go
index 8d67c83e7a9..042fa725334 100644
--- a/controller/log.go
+++ b/controller/log.go
@@ -10,14 +10,7 @@ import (
)
func GetAllLogs(c *gin.Context) {
- p, _ := strconv.Atoi(c.Query("p"))
- pageSize, _ := strconv.Atoi(c.Query("page_size"))
- if p < 1 {
- p = 1
- }
- if pageSize < 0 {
- pageSize = common.ItemsPerPage
- }
+ pageInfo := common.GetPageQuery(c)
logType, _ := strconv.Atoi(c.Query("type"))
startTimestamp, _ := strconv.ParseInt(c.Query("start_timestamp"), 10, 64)
endTimestamp, _ := strconv.ParseInt(c.Query("end_timestamp"), 10, 64)
@@ -26,38 +19,19 @@ func GetAllLogs(c *gin.Context) {
modelName := c.Query("model_name")
channel, _ := strconv.Atoi(c.Query("channel"))
group := c.Query("group")
- logs, total, err := model.GetAllLogs(logType, startTimestamp, endTimestamp, modelName, username, tokenName, (p-1)*pageSize, pageSize, channel, group)
+ logs, total, err := model.GetAllLogs(logType, startTimestamp, endTimestamp, modelName, username, tokenName, pageInfo.GetStartIdx(), pageInfo.GetPageSize(), channel, group)
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
- c.JSON(http.StatusOK, gin.H{
- "success": true,
- "message": "",
- "data": map[string]any{
- "items": logs,
- "total": total,
- "page": p,
- "page_size": pageSize,
- },
- })
+ pageInfo.SetTotal(int(total))
+ pageInfo.SetItems(logs)
+ common.ApiSuccess(c, pageInfo)
+ return
}
func GetUserLogs(c *gin.Context) {
- p, _ := strconv.Atoi(c.Query("p"))
- pageSize, _ := strconv.Atoi(c.Query("page_size"))
- if p < 1 {
- p = 1
- }
- if pageSize < 0 {
- pageSize = common.ItemsPerPage
- }
- if pageSize > 100 {
- pageSize = 100
- }
+ pageInfo := common.GetPageQuery(c)
userId := c.GetInt("id")
logType, _ := strconv.Atoi(c.Query("type"))
startTimestamp, _ := strconv.ParseInt(c.Query("start_timestamp"), 10, 64)
@@ -65,24 +39,14 @@ func GetUserLogs(c *gin.Context) {
tokenName := c.Query("token_name")
modelName := c.Query("model_name")
group := c.Query("group")
- logs, total, err := model.GetUserLogs(userId, logType, startTimestamp, endTimestamp, modelName, tokenName, (p-1)*pageSize, pageSize, group)
+ logs, total, err := model.GetUserLogs(userId, logType, startTimestamp, endTimestamp, modelName, tokenName, pageInfo.GetStartIdx(), pageInfo.GetPageSize(), group)
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
- c.JSON(http.StatusOK, gin.H{
- "success": true,
- "message": "",
- "data": map[string]any{
- "items": logs,
- "total": total,
- "page": p,
- "page_size": pageSize,
- },
- })
+ pageInfo.SetTotal(int(total))
+ pageInfo.SetItems(logs)
+ common.ApiSuccess(c, pageInfo)
return
}
@@ -90,10 +54,7 @@ func SearchAllLogs(c *gin.Context) {
keyword := c.Query("keyword")
logs, err := model.SearchAllLogs(keyword)
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
@@ -109,10 +70,7 @@ func SearchUserLogs(c *gin.Context) {
userId := c.GetInt("id")
logs, err := model.SearchUserLogs(userId, keyword)
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
@@ -198,10 +156,7 @@ func DeleteHistoryLogs(c *gin.Context) {
}
count, err := model.DeleteOldLog(c.Request.Context(), targetTimestamp, 100)
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
diff --git a/controller/midjourney.go b/controller/midjourney.go
index 56bdcb802e8..02ad708fb6e 100644
--- a/controller/midjourney.go
+++ b/controller/midjourney.go
@@ -5,7 +5,6 @@ import (
"context"
"encoding/json"
"fmt"
- "github.com/gin-gonic/gin"
"io"
"net/http"
"one-api/common"
@@ -13,8 +12,9 @@ import (
"one-api/model"
"one-api/service"
"one-api/setting"
- "strconv"
"time"
+
+ "github.com/gin-gonic/gin"
)
func UpdateMidjourneyTaskBulk() {
@@ -213,14 +213,7 @@ func checkMjTaskNeedUpdate(oldTask *model.Midjourney, newTask dto.MidjourneyDto)
}
func GetAllMidjourney(c *gin.Context) {
- p, _ := strconv.Atoi(c.Query("p"))
- if p < 1 {
- p = 1
- }
- pageSize, _ := strconv.Atoi(c.Query("page_size"))
- if pageSize <= 0 {
- pageSize = common.ItemsPerPage
- }
+ pageInfo := common.GetPageQuery(c)
// 解析其他查询参数
queryParams := model.TaskQueryParams{
@@ -230,7 +223,7 @@ func GetAllMidjourney(c *gin.Context) {
EndTimestamp: c.Query("end_timestamp"),
}
- items := model.GetAllTasks((p-1)*pageSize, pageSize, queryParams)
+ items := model.GetAllTasks(pageInfo.GetStartIdx(), pageInfo.GetPageSize(), queryParams)
total := model.CountAllTasks(queryParams)
if setting.MjForwardUrlEnabled {
@@ -239,27 +232,13 @@ func GetAllMidjourney(c *gin.Context) {
items[i] = midjourney
}
}
- c.JSON(200, gin.H{
- "success": true,
- "message": "",
- "data": gin.H{
- "items": items,
- "total": total,
- "page": p,
- "page_size": pageSize,
- },
- })
+ pageInfo.SetTotal(int(total))
+ pageInfo.SetItems(items)
+ common.ApiSuccess(c, pageInfo)
}
func GetUserMidjourney(c *gin.Context) {
- p, _ := strconv.Atoi(c.Query("p"))
- if p < 1 {
- p = 1
- }
- pageSize, _ := strconv.Atoi(c.Query("page_size"))
- if pageSize <= 0 {
- pageSize = common.ItemsPerPage
- }
+ pageInfo := common.GetPageQuery(c)
userId := c.GetInt("id")
@@ -269,7 +248,7 @@ func GetUserMidjourney(c *gin.Context) {
EndTimestamp: c.Query("end_timestamp"),
}
- items := model.GetAllUserTask(userId, (p-1)*pageSize, pageSize, queryParams)
+ items := model.GetAllUserTask(userId, pageInfo.GetStartIdx(), pageInfo.GetPageSize(), queryParams)
total := model.CountAllUserTask(userId, queryParams)
if setting.MjForwardUrlEnabled {
@@ -278,14 +257,7 @@ func GetUserMidjourney(c *gin.Context) {
items[i] = midjourney
}
}
- c.JSON(200, gin.H{
- "success": true,
- "message": "",
- "data": gin.H{
- "items": items,
- "total": total,
- "page": p,
- "page_size": pageSize,
- },
- })
+ pageInfo.SetTotal(int(total))
+ pageInfo.SetItems(items)
+ common.ApiSuccess(c, pageInfo)
}
diff --git a/controller/misc.go b/controller/misc.go
index 4ffe86f4308..a3ed9be9ac3 100644
--- a/controller/misc.go
+++ b/controller/misc.go
@@ -57,7 +57,9 @@ func GetStatus(c *gin.Context) {
"wechat_login": common.WeChatAuthEnabled,
"server_address": setting.ServerAddress,
"price": setting.Price,
+ "stripe_unit_price": setting.StripeUnitPrice,
"min_topup": setting.MinTopUp,
+ "stripe_min_topup": setting.StripeMinTopUp,
"turnstile_check": common.TurnstileCheckEnabled,
"turnstile_site_key": common.TurnstileSiteKey,
"top_up_link": common.TopUpLink,
@@ -71,12 +73,14 @@ func GetStatus(c *gin.Context) {
"data_export_default_time": common.DataExportDefaultTime,
"default_collapse_sidebar": common.DefaultCollapseSidebar,
"enable_online_topup": setting.PayAddress != "" && setting.EpayId != "" && setting.EpayKey != "",
+ "enable_stripe_topup": setting.StripeApiSecret != "" && setting.StripeWebhookSecret != "" && setting.StripePriceId != "",
"mj_notify_enabled": setting.MjNotifyEnabled,
"chats": setting.Chats,
"demo_site_enabled": operation_setting.DemoSiteEnabled,
"self_use_mode_enabled": operation_setting.SelfUseModeEnabled,
"default_use_auto_group": setting.DefaultUseAutoGroup,
"pay_methods": setting.PayMethods,
+ "usd_exchange_rate": setting.USDExchangeRate,
// 面板启用开关
"api_info_enabled": cs.ApiInfoEnabled,
@@ -214,10 +218,7 @@ func SendEmailVerification(c *gin.Context) {
"
验证码 %d 分钟内有效,如果不是本人操作,请忽略。
", common.SystemName, code, common.VerificationValidMinutes)
err := common.SendEmail(subject, email, content)
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
@@ -253,10 +254,7 @@ func SendPasswordResetEmail(c *gin.Context) {
"重置链接 %d 分钟内有效,如果不是本人操作,请忽略。
", common.SystemName, link, link, common.VerificationValidMinutes)
err := common.SendEmail(subject, email, content)
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
@@ -291,10 +289,7 @@ func ResetPassword(c *gin.Context) {
password := common.GenerateVerificationCode(12)
err = model.ResetUserPasswordByEmail(req.Email, password)
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
common.DeleteKey(req.Email, common.PasswordResetPurpose)
diff --git a/controller/oidc.go b/controller/oidc.go
index 440e0964095..df8ea1c401f 100644
--- a/controller/oidc.go
+++ b/controller/oidc.go
@@ -126,10 +126,7 @@ func OidcAuth(c *gin.Context) {
code := c.Query("code")
oidcUser, err := getOidcUserInfoByCode(code)
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
user := model.User{
@@ -195,10 +192,7 @@ func OidcBind(c *gin.Context) {
code := c.Query("code")
oidcUser, err := getOidcUserInfoByCode(code)
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
user := model.User{
@@ -217,19 +211,13 @@ func OidcBind(c *gin.Context) {
user.Id = id.(int)
err = user.FillUserById()
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
user.OidcId = oidcUser.OpenID
err = user.Update(false)
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
diff --git a/controller/option.go b/controller/option.go
index 97bb6a5a5a4..decdb0d405d 100644
--- a/controller/option.go
+++ b/controller/option.go
@@ -160,10 +160,7 @@ func UpdateOption(c *gin.Context) {
}
err = model.UpdateOption(option.Key, option.Value)
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
diff --git a/controller/playground.go b/controller/playground.go
index 33471455cbf..0073cf06045 100644
--- a/controller/playground.go
+++ b/controller/playground.go
@@ -3,45 +3,44 @@ package controller
import (
"errors"
"fmt"
- "net/http"
"one-api/common"
"one-api/constant"
"one-api/dto"
"one-api/middleware"
"one-api/model"
- "one-api/service"
"one-api/setting"
+ "one-api/types"
"time"
"github.com/gin-gonic/gin"
)
func Playground(c *gin.Context) {
- var openaiErr *dto.OpenAIErrorWithStatusCode
+ var newAPIError *types.NewAPIError
defer func() {
- if openaiErr != nil {
- c.JSON(openaiErr.StatusCode, gin.H{
- "error": openaiErr.Error,
+ if newAPIError != nil {
+ c.JSON(newAPIError.StatusCode, gin.H{
+ "error": newAPIError.ToOpenAIError(),
})
}
}()
useAccessToken := c.GetBool("use_access_token")
if useAccessToken {
- openaiErr = service.OpenAIErrorWrapperLocal(errors.New("暂不支持使用 access token"), "access_token_not_supported", http.StatusBadRequest)
+ newAPIError = types.NewError(errors.New("暂不支持使用 access token"), types.ErrorCodeAccessDenied)
return
}
playgroundRequest := &dto.PlayGroundRequest{}
err := common.UnmarshalBodyReusable(c, playgroundRequest)
if err != nil {
- openaiErr = service.OpenAIErrorWrapperLocal(err, "unmarshal_request_failed", http.StatusBadRequest)
+ newAPIError = types.NewError(err, types.ErrorCodeInvalidRequest)
return
}
if playgroundRequest.Model == "" {
- openaiErr = service.OpenAIErrorWrapperLocal(errors.New("请选择模型"), "model_required", http.StatusBadRequest)
+ newAPIError = types.NewError(errors.New("请选择模型"), types.ErrorCodeInvalidRequest)
return
}
c.Set("original_model", playgroundRequest.Model)
@@ -52,28 +51,34 @@ func Playground(c *gin.Context) {
group = userGroup
} else {
if !setting.GroupInUserUsableGroups(group) && group != userGroup {
- openaiErr = service.OpenAIErrorWrapperLocal(errors.New("无权访问该分组"), "group_not_allowed", http.StatusForbidden)
+ newAPIError = types.NewError(errors.New("无权访问该分组"), types.ErrorCodeAccessDenied)
return
}
c.Set("group", group)
}
- c.Set("token_name", "playground-"+group)
- channel, finalGroup, err := model.CacheGetRandomSatisfiedChannel(c, group, playgroundRequest.Model, 0)
- if err != nil {
- message := fmt.Sprintf("当前分组 %s 下对于模型 %s 无可用渠道", finalGroup, playgroundRequest.Model)
- openaiErr = service.OpenAIErrorWrapperLocal(errors.New(message), "get_playground_channel_failed", http.StatusInternalServerError)
- return
- }
- middleware.SetupContextForSelectedChannel(c, channel, playgroundRequest.Model)
- common.SetContextKey(c, constant.ContextKeyRequestStartTime, time.Now())
- // Write user context to ensure acceptUnsetRatio is available
userId := c.GetInt("id")
+
+ // Write user context to ensure acceptUnsetRatio is available
userCache, err := model.GetUserCache(userId)
if err != nil {
- openaiErr = service.OpenAIErrorWrapperLocal(err, "get_user_cache_failed", http.StatusInternalServerError)
+ newAPIError = types.NewError(err, types.ErrorCodeQueryDataError)
return
}
userCache.WriteContext(c)
+
+ tempToken := &model.Token{
+ UserId: userId,
+ Name: fmt.Sprintf("playground-%s", group),
+ Group: group,
+ }
+ _ = middleware.SetupContextForToken(c, tempToken)
+ _, newAPIError = getChannel(c, group, playgroundRequest.Model, 0)
+ if newAPIError != nil {
+ return
+ }
+ //middleware.SetupContextForSelectedChannel(c, channel, playgroundRequest.Model)
+ common.SetContextKey(c, constant.ContextKeyRequestStartTime, time.Now())
+
Relay(c)
}
diff --git a/controller/redemption.go b/controller/redemption.go
index 50620597918..83ec19ad66b 100644
--- a/controller/redemption.go
+++ b/controller/redemption.go
@@ -1,91 +1,51 @@
package controller
import (
+ "errors"
"net/http"
"one-api/common"
"one-api/model"
"strconv"
- "errors"
"github.com/gin-gonic/gin"
)
func GetAllRedemptions(c *gin.Context) {
- p, _ := strconv.Atoi(c.Query("p"))
- pageSize, _ := strconv.Atoi(c.Query("page_size"))
- if p < 0 {
- p = 0
- }
- if pageSize < 1 {
- pageSize = common.ItemsPerPage
- }
- redemptions, total, err := model.GetAllRedemptions((p-1)*pageSize, pageSize)
+ pageInfo := common.GetPageQuery(c)
+ redemptions, total, err := model.GetAllRedemptions(pageInfo.GetStartIdx(), pageInfo.GetPageSize())
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
- c.JSON(http.StatusOK, gin.H{
- "success": true,
- "message": "",
- "data": gin.H{
- "items": redemptions,
- "total": total,
- "page": p,
- "page_size": pageSize,
- },
- })
+ pageInfo.SetTotal(int(total))
+ pageInfo.SetItems(redemptions)
+ common.ApiSuccess(c, pageInfo)
return
}
func SearchRedemptions(c *gin.Context) {
keyword := c.Query("keyword")
- p, _ := strconv.Atoi(c.Query("p"))
- pageSize, _ := strconv.Atoi(c.Query("page_size"))
- if p < 0 {
- p = 0
- }
- if pageSize < 1 {
- pageSize = common.ItemsPerPage
- }
- redemptions, total, err := model.SearchRedemptions(keyword, (p-1)*pageSize, pageSize)
+ pageInfo := common.GetPageQuery(c)
+ redemptions, total, err := model.SearchRedemptions(keyword, pageInfo.GetStartIdx(), pageInfo.GetPageSize())
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
- c.JSON(http.StatusOK, gin.H{
- "success": true,
- "message": "",
- "data": gin.H{
- "items": redemptions,
- "total": total,
- "page": p,
- "page_size": pageSize,
- },
- })
+ pageInfo.SetTotal(int(total))
+ pageInfo.SetItems(redemptions)
+ common.ApiSuccess(c, pageInfo)
return
}
func GetRedemption(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
redemption, err := model.GetRedemptionById(id)
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
@@ -100,10 +60,7 @@ func AddRedemption(c *gin.Context) {
redemption := model.Redemption{}
err := c.ShouldBindJSON(&redemption)
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
if len(redemption.Name) == 0 || len(redemption.Name) > 20 {
@@ -165,10 +122,7 @@ func DeleteRedemption(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
err := model.DeleteRedemptionById(id)
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
@@ -183,18 +137,12 @@ func UpdateRedemption(c *gin.Context) {
redemption := model.Redemption{}
err := c.ShouldBindJSON(&redemption)
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
cleanRedemption, err := model.GetRedemptionById(redemption.Id)
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
if statusOnly == "" {
@@ -212,10 +160,7 @@ func UpdateRedemption(c *gin.Context) {
}
err = cleanRedemption.Update()
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
@@ -229,16 +174,13 @@ func UpdateRedemption(c *gin.Context) {
func DeleteInvalidRedemption(c *gin.Context) {
rows, err := model.DeleteInvalidRedemptions()
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
- "data": rows,
+ "data": rows,
})
return
}
diff --git a/controller/relay.go b/controller/relay.go
index e375120b817..b224b42c138 100644
--- a/controller/relay.go
+++ b/controller/relay.go
@@ -17,14 +17,15 @@ import (
relayconstant "one-api/relay/constant"
"one-api/relay/helper"
"one-api/service"
+ "one-api/types"
"strings"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
)
-func relayHandler(c *gin.Context, relayMode int) *dto.OpenAIErrorWithStatusCode {
- var err *dto.OpenAIErrorWithStatusCode
+func relayHandler(c *gin.Context, relayMode int) *types.NewAPIError {
+ var err *types.NewAPIError
switch relayMode {
case relayconstant.RelayModeImagesGenerations, relayconstant.RelayModeImagesEdits:
err = relay.ImageHelper(c)
@@ -55,14 +56,14 @@ func relayHandler(c *gin.Context, relayMode int) *dto.OpenAIErrorWithStatusCode
userGroup := c.GetString("group")
channelId := c.GetInt("channel_id")
other := make(map[string]interface{})
- other["error_type"] = err.Error.Type
- other["error_code"] = err.Error.Code
+ other["error_type"] = err.ErrorType
+ other["error_code"] = err.GetErrorCode()
other["status_code"] = err.StatusCode
other["channel_id"] = channelId
other["channel_name"] = c.GetString("channel_name")
other["channel_type"] = c.GetInt("channel_type")
- model.RecordErrorLog(c, userId, channelId, modelName, tokenName, err.Error.Message, tokenId, 0, false, userGroup, other)
+ model.RecordErrorLog(c, userId, channelId, modelName, tokenName, err.Error(), tokenId, 0, false, userGroup, other)
}
return err
@@ -73,25 +74,25 @@ func Relay(c *gin.Context) {
requestId := c.GetString(common.RequestIdKey)
group := c.GetString("group")
originalModel := c.GetString("original_model")
- var openaiErr *dto.OpenAIErrorWithStatusCode
+ var newAPIError *types.NewAPIError
for i := 0; i <= common.RetryTimes; i++ {
channel, err := getChannel(c, group, originalModel, i)
if err != nil {
common.LogError(c, err.Error())
- openaiErr = service.OpenAIErrorWrapperLocal(err, "get_channel_failed", http.StatusInternalServerError)
+ newAPIError = err
break
}
- openaiErr = relayRequest(c, relayMode, channel)
+ newAPIError = relayRequest(c, relayMode, channel)
- if openaiErr == nil {
+ if newAPIError == nil {
return // 成功处理请求,直接返回
}
- go processChannelError(c, channel.Id, channel.Type, channel.Name, channel.GetAutoBan(), openaiErr)
+ go processChannelError(c, *types.NewChannelError(channel.Id, channel.Type, channel.Name, channel.ChannelInfo.IsMultiKey, common.GetContextKeyString(c, constant.ContextKeyChannelKey), channel.GetAutoBan()), newAPIError)
- if !shouldRetry(c, openaiErr, common.RetryTimes-i) {
+ if !shouldRetry(c, newAPIError, common.RetryTimes-i) {
break
}
}
@@ -101,14 +102,14 @@ func Relay(c *gin.Context) {
common.LogInfo(c, retryLogStr)
}
- if openaiErr != nil {
- if openaiErr.StatusCode == http.StatusTooManyRequests {
- common.LogError(c, fmt.Sprintf("origin 429 error: %s", openaiErr.Error.Message))
- openaiErr.Error.Message = "当前分组上游负载已饱和,请稍后再试"
- }
- openaiErr.Error.Message = common.MessageWithRequestId(openaiErr.Error.Message, requestId)
- c.JSON(openaiErr.StatusCode, gin.H{
- "error": openaiErr.Error,
+ if newAPIError != nil {
+ //if newAPIError.StatusCode == http.StatusTooManyRequests {
+ // common.LogError(c, fmt.Sprintf("origin 429 error: %s", newAPIError.Error()))
+ // newAPIError.SetMessage("当前分组上游负载已饱和,请稍后再试")
+ //}
+ newAPIError.SetMessage(common.MessageWithRequestId(newAPIError.Error(), requestId))
+ c.JSON(newAPIError.StatusCode, gin.H{
+ "error": newAPIError.ToOpenAIError(),
})
}
}
@@ -127,8 +128,7 @@ func WssRelay(c *gin.Context) {
defer ws.Close()
if err != nil {
- openaiErr := service.OpenAIErrorWrapper(err, "get_channel_failed", http.StatusInternalServerError)
- helper.WssError(c, ws, openaiErr.Error)
+ helper.WssError(c, ws, types.NewError(err, types.ErrorCodeGetChannelFailed).ToOpenAIError())
return
}
@@ -137,25 +137,25 @@ func WssRelay(c *gin.Context) {
group := c.GetString("group")
//wss://api.openai.com/v1/realtime?model=gpt-4o-realtime-preview-2024-10-01
originalModel := c.GetString("original_model")
- var openaiErr *dto.OpenAIErrorWithStatusCode
+ var newAPIError *types.NewAPIError
for i := 0; i <= common.RetryTimes; i++ {
channel, err := getChannel(c, group, originalModel, i)
if err != nil {
common.LogError(c, err.Error())
- openaiErr = service.OpenAIErrorWrapperLocal(err, "get_channel_failed", http.StatusInternalServerError)
+ newAPIError = err
break
}
- openaiErr = wssRequest(c, ws, relayMode, channel)
+ newAPIError = wssRequest(c, ws, relayMode, channel)
- if openaiErr == nil {
+ if newAPIError == nil {
return // 成功处理请求,直接返回
}
- go processChannelError(c, channel.Id, channel.Type, channel.Name, channel.GetAutoBan(), openaiErr)
+ go processChannelError(c, *types.NewChannelError(channel.Id, channel.Type, channel.Name, channel.ChannelInfo.IsMultiKey, common.GetContextKeyString(c, constant.ContextKeyChannelKey), channel.GetAutoBan()), newAPIError)
- if !shouldRetry(c, openaiErr, common.RetryTimes-i) {
+ if !shouldRetry(c, newAPIError, common.RetryTimes-i) {
break
}
}
@@ -165,12 +165,12 @@ func WssRelay(c *gin.Context) {
common.LogInfo(c, retryLogStr)
}
- if openaiErr != nil {
- if openaiErr.StatusCode == http.StatusTooManyRequests {
- openaiErr.Error.Message = "当前分组上游负载已饱和,请稍后再试"
- }
- openaiErr.Error.Message = common.MessageWithRequestId(openaiErr.Error.Message, requestId)
- helper.WssError(c, ws, openaiErr.Error)
+ if newAPIError != nil {
+ //if newAPIError.StatusCode == http.StatusTooManyRequests {
+ // newAPIError.SetMessage("当前分组上游负载已饱和,请稍后再试")
+ //}
+ newAPIError.SetMessage(common.MessageWithRequestId(newAPIError.Error(), requestId))
+ helper.WssError(c, ws, newAPIError.ToOpenAIError())
}
}
@@ -179,27 +179,25 @@ func RelayClaude(c *gin.Context) {
requestId := c.GetString(common.RequestIdKey)
group := c.GetString("group")
originalModel := c.GetString("original_model")
- var claudeErr *dto.ClaudeErrorWithStatusCode
+ var newAPIError *types.NewAPIError
for i := 0; i <= common.RetryTimes; i++ {
channel, err := getChannel(c, group, originalModel, i)
if err != nil {
common.LogError(c, err.Error())
- claudeErr = service.ClaudeErrorWrapperLocal(err, "get_channel_failed", http.StatusInternalServerError)
+ newAPIError = err
break
}
- claudeErr = claudeRequest(c, channel)
+ newAPIError = claudeRequest(c, channel)
- if claudeErr == nil {
+ if newAPIError == nil {
return // 成功处理请求,直接返回
}
- openaiErr := service.ClaudeErrorToOpenAIError(claudeErr)
+ go processChannelError(c, *types.NewChannelError(channel.Id, channel.Type, channel.Name, channel.ChannelInfo.IsMultiKey, common.GetContextKeyString(c, constant.ContextKeyChannelKey), channel.GetAutoBan()), newAPIError)
- go processChannelError(c, channel.Id, channel.Type, channel.Name, channel.GetAutoBan(), openaiErr)
-
- if !shouldRetry(c, openaiErr, common.RetryTimes-i) {
+ if !shouldRetry(c, newAPIError, common.RetryTimes-i) {
break
}
}
@@ -209,30 +207,30 @@ func RelayClaude(c *gin.Context) {
common.LogInfo(c, retryLogStr)
}
- if claudeErr != nil {
- claudeErr.Error.Message = common.MessageWithRequestId(claudeErr.Error.Message, requestId)
- c.JSON(claudeErr.StatusCode, gin.H{
+ if newAPIError != nil {
+ newAPIError.SetMessage(common.MessageWithRequestId(newAPIError.Error(), requestId))
+ c.JSON(newAPIError.StatusCode, gin.H{
"type": "error",
- "error": claudeErr.Error,
+ "error": newAPIError.ToClaudeError(),
})
}
}
-func relayRequest(c *gin.Context, relayMode int, channel *model.Channel) *dto.OpenAIErrorWithStatusCode {
+func relayRequest(c *gin.Context, relayMode int, channel *model.Channel) *types.NewAPIError {
addUsedChannel(c, channel.Id)
requestBody, _ := common.GetRequestBody(c)
c.Request.Body = io.NopCloser(bytes.NewBuffer(requestBody))
return relayHandler(c, relayMode)
}
-func wssRequest(c *gin.Context, ws *websocket.Conn, relayMode int, channel *model.Channel) *dto.OpenAIErrorWithStatusCode {
+func wssRequest(c *gin.Context, ws *websocket.Conn, relayMode int, channel *model.Channel) *types.NewAPIError {
addUsedChannel(c, channel.Id)
requestBody, _ := common.GetRequestBody(c)
c.Request.Body = io.NopCloser(bytes.NewBuffer(requestBody))
return relay.WssHelper(c, ws)
}
-func claudeRequest(c *gin.Context, channel *model.Channel) *dto.ClaudeErrorWithStatusCode {
+func claudeRequest(c *gin.Context, channel *model.Channel) *types.NewAPIError {
addUsedChannel(c, channel.Id)
requestBody, _ := common.GetRequestBody(c)
c.Request.Body = io.NopCloser(bytes.NewBuffer(requestBody))
@@ -245,7 +243,7 @@ func addUsedChannel(c *gin.Context, channelId int) {
c.Set("use_channel", useChannel)
}
-func getChannel(c *gin.Context, group, originalModel string, retryCount int) (*model.Channel, error) {
+func getChannel(c *gin.Context, group, originalModel string, retryCount int) (*model.Channel, *types.NewAPIError) {
if retryCount == 0 {
autoBan := c.GetBool("auto_ban")
autoBanInt := 1
@@ -259,19 +257,28 @@ func getChannel(c *gin.Context, group, originalModel string, retryCount int) (*m
AutoBan: &autoBanInt,
}, nil
}
- channel, _, err := model.CacheGetRandomSatisfiedChannel(c, group, originalModel, retryCount)
+ channel, selectGroup, err := model.CacheGetRandomSatisfiedChannel(c, group, originalModel, retryCount)
if err != nil {
- return nil, errors.New(fmt.Sprintf("获取重试渠道失败: %s", err.Error()))
+ if group == "auto" {
+ return nil, types.NewError(errors.New(fmt.Sprintf("获取自动分组下模型 %s 的可用渠道失败: %s", originalModel, err.Error())), types.ErrorCodeGetChannelFailed)
+ }
+ return nil, types.NewError(errors.New(fmt.Sprintf("获取分组 %s 下模型 %s 的可用渠道失败: %s", selectGroup, originalModel, err.Error())), types.ErrorCodeGetChannelFailed)
+ }
+ newAPIError := middleware.SetupContextForSelectedChannel(c, channel, originalModel)
+ if newAPIError != nil {
+ return nil, newAPIError
}
- middleware.SetupContextForSelectedChannel(c, channel, originalModel)
return channel, nil
}
-func shouldRetry(c *gin.Context, openaiErr *dto.OpenAIErrorWithStatusCode, retryTimes int) bool {
+func shouldRetry(c *gin.Context, openaiErr *types.NewAPIError, retryTimes int) bool {
if openaiErr == nil {
return false
}
- if openaiErr.LocalError {
+ if types.IsChannelError(openaiErr) {
+ return true
+ }
+ if types.IsLocalError(openaiErr) {
return false
}
if retryTimes <= 0 {
@@ -310,12 +317,12 @@ func shouldRetry(c *gin.Context, openaiErr *dto.OpenAIErrorWithStatusCode, retry
return true
}
-func processChannelError(c *gin.Context, channelId int, channelType int, channelName string, autoBan bool, err *dto.OpenAIErrorWithStatusCode) {
+func processChannelError(c *gin.Context, channelError types.ChannelError, err *types.NewAPIError) {
// 不要使用context获取渠道信息,异步处理时可能会出现渠道信息不一致的情况
// do not use context to get channel info, there may be inconsistent channel info when processing asynchronously
- common.LogError(c, fmt.Sprintf("relay error (channel #%d, status code: %d): %s", channelId, err.StatusCode, err.Error.Message))
- if service.ShouldDisableChannel(channelType, err) && autoBan {
- service.DisableChannel(channelId, channelName, err.Error.Message)
+ common.LogError(c, fmt.Sprintf("relay error (channel #%d, status code: %d): %s", channelError.ChannelId, err.StatusCode, err.Error()))
+ if service.ShouldDisableChannel(channelError.ChannelId, err) && channelError.AutoBan {
+ service.DisableChannel(channelError, err.Error())
}
}
@@ -388,9 +395,10 @@ func RelayTask(c *gin.Context) {
retryTimes = 0
}
for i := 0; shouldRetryTaskRelay(c, channelId, taskErr, retryTimes) && i < retryTimes; i++ {
- channel, _, err := model.CacheGetRandomSatisfiedChannel(c, group, originalModel, i)
- if err != nil {
- common.LogError(c, fmt.Sprintf("CacheGetRandomSatisfiedChannel failed: %s", err.Error()))
+ channel, newAPIError := getChannel(c, group, originalModel, i)
+ if newAPIError != nil {
+ common.LogError(c, fmt.Sprintf("CacheGetRandomSatisfiedChannel failed: %s", newAPIError.Error()))
+ taskErr = service.TaskErrorWrapperLocal(newAPIError.Err, "get_channel_failed", http.StatusInternalServerError)
break
}
channelId = channel.Id
@@ -398,9 +406,9 @@ func RelayTask(c *gin.Context) {
useChannel = append(useChannel, fmt.Sprintf("%d", channelId))
c.Set("use_channel", useChannel)
common.LogInfo(c, fmt.Sprintf("using channel #%d to retry (remain times %d)", channel.Id, i))
- middleware.SetupContextForSelectedChannel(c, channel, originalModel)
+ //middleware.SetupContextForSelectedChannel(c, channel, originalModel)
- requestBody, err := common.GetRequestBody(c)
+ requestBody, _ := common.GetRequestBody(c)
c.Request.Body = io.NopCloser(bytes.NewBuffer(requestBody))
taskErr = taskRelayHandler(c, relayMode)
}
diff --git a/controller/swag_video.go b/controller/swag_video.go
new file mode 100644
index 00000000000..185fd515956
--- /dev/null
+++ b/controller/swag_video.go
@@ -0,0 +1,116 @@
+package controller
+
+import (
+ "github.com/gin-gonic/gin"
+)
+
+// VideoGenerations
+// @Summary 生成视频
+// @Description 调用视频生成接口生成视频
+// @Description 支持多种视频生成服务:
+// @Description - 可灵AI (Kling): https://app.klingai.com/cn/dev/document-api/apiReference/commonInfo
+// @Description - 即梦 (Jimeng): https://www.volcengine.com/docs/85621/1538636
+// @Tags Video
+// @Accept json
+// @Produce json
+// @Param Authorization header string true "用户认证令牌 (Aeess-Token: sk-xxxx)"
+// @Param request body dto.VideoRequest true "视频生成请求参数"
+// @Failure 400 {object} dto.OpenAIError "请求参数错误"
+// @Failure 401 {object} dto.OpenAIError "未授权"
+// @Failure 403 {object} dto.OpenAIError "无权限"
+// @Failure 500 {object} dto.OpenAIError "服务器内部错误"
+// @Router /v1/video/generations [post]
+func VideoGenerations(c *gin.Context) {
+}
+
+// VideoGenerationsTaskId
+// @Summary 查询视频
+// @Description 根据任务ID查询视频生成任务的状态和结果
+// @Tags Video
+// @Accept json
+// @Produce json
+// @Security BearerAuth
+// @Param task_id path string true "Task ID"
+// @Success 200 {object} dto.VideoTaskResponse "任务状态和结果"
+// @Failure 400 {object} dto.OpenAIError "请求参数错误"
+// @Failure 401 {object} dto.OpenAIError "未授权"
+// @Failure 403 {object} dto.OpenAIError "无权限"
+// @Failure 500 {object} dto.OpenAIError "服务器内部错误"
+// @Router /v1/video/generations/{task_id} [get]
+func VideoGenerationsTaskId(c *gin.Context) {
+}
+
+// KlingText2VideoGenerations
+// @Summary 可灵文生视频
+// @Description 调用可灵AI文生视频接口,生成视频内容
+// @Tags Video
+// @Accept json
+// @Produce json
+// @Param Authorization header string true "用户认证令牌 (Aeess-Token: sk-xxxx)"
+// @Param request body KlingText2VideoRequest true "视频生成请求参数"
+// @Success 200 {object} dto.VideoTaskResponse "任务状态和结果"
+// @Failure 400 {object} dto.OpenAIError "请求参数错误"
+// @Failure 401 {object} dto.OpenAIError "未授权"
+// @Failure 403 {object} dto.OpenAIError "无权限"
+// @Failure 500 {object} dto.OpenAIError "服务器内部错误"
+// @Router /kling/v1/videos/text2video [post]
+func KlingText2VideoGenerations(c *gin.Context) {
+}
+
+type KlingText2VideoRequest struct {
+ ModelName string `json:"model_name,omitempty" example:"kling-v1"`
+ Prompt string `json:"prompt" binding:"required" example:"A cat playing piano in the garden"`
+ NegativePrompt string `json:"negative_prompt,omitempty" example:"blurry, low quality"`
+ CfgScale float64 `json:"cfg_scale,omitempty" example:"0.7"`
+ Mode string `json:"mode,omitempty" example:"std"`
+ CameraControl *KlingCameraControl `json:"camera_control,omitempty"`
+ AspectRatio string `json:"aspect_ratio,omitempty" example:"16:9"`
+ Duration string `json:"duration,omitempty" example:"5"`
+ CallbackURL string `json:"callback_url,omitempty" example:"https://your.domain/callback"`
+ ExternalTaskId string `json:"external_task_id,omitempty" example:"custom-task-001"`
+}
+
+type KlingCameraControl struct {
+ Type string `json:"type,omitempty" example:"simple"`
+ Config *KlingCameraConfig `json:"config,omitempty"`
+}
+
+type KlingCameraConfig struct {
+ Horizontal float64 `json:"horizontal,omitempty" example:"2.5"`
+ Vertical float64 `json:"vertical,omitempty" example:"0"`
+ Pan float64 `json:"pan,omitempty" example:"0"`
+ Tilt float64 `json:"tilt,omitempty" example:"0"`
+ Roll float64 `json:"roll,omitempty" example:"0"`
+ Zoom float64 `json:"zoom,omitempty" example:"0"`
+}
+
+// KlingImage2VideoGenerations
+// @Summary 可灵官方-图生视频
+// @Description 调用可灵AI图生视频接口,生成视频内容
+// @Tags Video
+// @Accept json
+// @Produce json
+// @Param Authorization header string true "用户认证令牌 (Aeess-Token: sk-xxxx)"
+// @Param request body KlingImage2VideoRequest true "图生视频请求参数"
+// @Success 200 {object} dto.VideoTaskResponse "任务状态和结果"
+// @Failure 400 {object} dto.OpenAIError "请求参数错误"
+// @Failure 401 {object} dto.OpenAIError "未授权"
+// @Failure 403 {object} dto.OpenAIError "无权限"
+// @Failure 500 {object} dto.OpenAIError "服务器内部错误"
+// @Router /kling/v1/videos/image2video [post]
+func KlingImage2VideoGenerations(c *gin.Context) {
+}
+
+type KlingImage2VideoRequest struct {
+ ModelName string `json:"model_name,omitempty" example:"kling-v2-master"`
+ Image string `json:"image" binding:"required" example:"https://h2.inkwai.com/bs2/upload-ylab-stunt/se/ai_portal_queue_mmu_image_upscale_aiweb/3214b798-e1b4-4b00-b7af-72b5b0417420_raw_image_0.jpg"`
+ Prompt string `json:"prompt,omitempty" example:"A cat playing piano in the garden"`
+ NegativePrompt string `json:"negative_prompt,omitempty" example:"blurry, low quality"`
+ CfgScale float64 `json:"cfg_scale,omitempty" example:"0.7"`
+ Mode string `json:"mode,omitempty" example:"std"`
+ CameraControl *KlingCameraControl `json:"camera_control,omitempty"`
+ AspectRatio string `json:"aspect_ratio,omitempty" example:"16:9"`
+ Duration string `json:"duration,omitempty" example:"5"`
+ CallbackURL string `json:"callback_url,omitempty" example:"https://your.domain/callback"`
+ ExternalTaskId string `json:"external_task_id,omitempty" example:"custom-task-002"`
+}
diff --git a/controller/task.go b/controller/task.go
index 5cfa728aa67..78674d8b613 100644
--- a/controller/task.go
+++ b/controller/task.go
@@ -5,8 +5,6 @@ import (
"encoding/json"
"errors"
"fmt"
- "github.com/gin-gonic/gin"
- "github.com/samber/lo"
"io"
"net/http"
"one-api/common"
@@ -17,6 +15,9 @@ import (
"sort"
"strconv"
"time"
+
+ "github.com/gin-gonic/gin"
+ "github.com/samber/lo"
)
func UpdateTaskBulk() {
@@ -225,14 +226,7 @@ func checkTaskNeedUpdate(oldTask *model.Task, newTask dto.SunoDataResponse) bool
}
func GetAllTask(c *gin.Context) {
- p, _ := strconv.Atoi(c.Query("p"))
- if p < 1 {
- p = 1
- }
- pageSize, _ := strconv.Atoi(c.Query("page_size"))
- if pageSize <= 0 {
- pageSize = common.ItemsPerPage
- }
+ pageInfo := common.GetPageQuery(c)
startTimestamp, _ := strconv.ParseInt(c.Query("start_timestamp"), 10, 64)
endTimestamp, _ := strconv.ParseInt(c.Query("end_timestamp"), 10, 64)
@@ -247,30 +241,15 @@ func GetAllTask(c *gin.Context) {
ChannelID: c.Query("channel_id"),
}
- items := model.TaskGetAllTasks((p-1)*pageSize, pageSize, queryParams)
+ items := model.TaskGetAllTasks(pageInfo.GetStartIdx(), pageInfo.GetPageSize(), queryParams)
total := model.TaskCountAllTasks(queryParams)
-
- c.JSON(200, gin.H{
- "success": true,
- "message": "",
- "data": gin.H{
- "items": items,
- "total": total,
- "page": p,
- "page_size": pageSize,
- },
- })
+ pageInfo.SetTotal(int(total))
+ pageInfo.SetItems(items)
+ common.ApiSuccess(c, pageInfo)
}
func GetUserTask(c *gin.Context) {
- p, _ := strconv.Atoi(c.Query("p"))
- if p < 1 {
- p = 1
- }
- pageSize, _ := strconv.Atoi(c.Query("page_size"))
- if pageSize <= 0 {
- pageSize = common.ItemsPerPage
- }
+ pageInfo := common.GetPageQuery(c)
userId := c.GetInt("id")
@@ -286,17 +265,9 @@ func GetUserTask(c *gin.Context) {
EndTimestamp: endTimestamp,
}
- items := model.TaskGetAllUserTask(userId, (p-1)*pageSize, pageSize, queryParams)
+ items := model.TaskGetAllUserTask(userId, pageInfo.GetStartIdx(), pageInfo.GetPageSize(), queryParams)
total := model.TaskCountAllUserTask(userId, queryParams)
-
- c.JSON(200, gin.H{
- "success": true,
- "message": "",
- "data": gin.H{
- "items": items,
- "total": total,
- "page": p,
- "page_size": pageSize,
- },
- })
+ pageInfo.SetTotal(int(total))
+ pageInfo.SetItems(items)
+ common.ApiSuccess(c, pageInfo)
}
diff --git a/controller/token.go b/controller/token.go
index 173fc22e22b..62eb5474ebe 100644
--- a/controller/token.go
+++ b/controller/token.go
@@ -1,46 +1,26 @@
package controller
import (
- "github.com/gin-gonic/gin"
"net/http"
"one-api/common"
"one-api/model"
"strconv"
+
+ "github.com/gin-gonic/gin"
)
func GetAllTokens(c *gin.Context) {
userId := c.GetInt("id")
- p, _ := strconv.Atoi(c.Query("p"))
- size, _ := strconv.Atoi(c.Query("size"))
- if p < 1 {
- p = 1
- }
- if size <= 0 {
- size = common.ItemsPerPage
- } else if size > 100 {
- size = 100
- }
- tokens, err := model.GetAllUserTokens(userId, (p-1)*size, size)
+ pageInfo := common.GetPageQuery(c)
+ tokens, err := model.GetAllUserTokens(userId, pageInfo.GetStartIdx(), pageInfo.GetPageSize())
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
- // Get total count for pagination
total, _ := model.CountUserTokens(userId)
-
- c.JSON(http.StatusOK, gin.H{
- "success": true,
- "message": "",
- "data": gin.H{
- "items": tokens,
- "total": total,
- "page": p,
- "page_size": size,
- },
- })
+ pageInfo.SetTotal(int(total))
+ pageInfo.SetItems(tokens)
+ common.ApiSuccess(c, pageInfo)
return
}
@@ -50,10 +30,7 @@ func SearchTokens(c *gin.Context) {
token := c.Query("token")
tokens, err := model.SearchUserTokens(userId, keyword, token)
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
@@ -68,18 +45,12 @@ func GetToken(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
userId := c.GetInt("id")
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
token, err := model.GetTokenByIds(id, userId)
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
@@ -95,10 +66,7 @@ func GetTokenStatus(c *gin.Context) {
userId := c.GetInt("id")
token, err := model.GetTokenByIds(tokenId, userId)
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
expiredAt := token.ExpiredTime
@@ -118,10 +86,7 @@ func AddToken(c *gin.Context) {
token := model.Token{}
err := c.ShouldBindJSON(&token)
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
if len(token.Name) > 30 {
@@ -156,10 +121,7 @@ func AddToken(c *gin.Context) {
}
err = cleanToken.Insert()
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
@@ -174,10 +136,7 @@ func DeleteToken(c *gin.Context) {
userId := c.GetInt("id")
err := model.DeleteTokenById(id, userId)
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
@@ -193,10 +152,7 @@ func UpdateToken(c *gin.Context) {
token := model.Token{}
err := c.ShouldBindJSON(&token)
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
if len(token.Name) > 30 {
@@ -208,10 +164,7 @@ func UpdateToken(c *gin.Context) {
}
cleanToken, err := model.GetTokenByIds(token.Id, userId)
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
if token.Status == common.TokenStatusEnabled {
@@ -245,10 +198,7 @@ func UpdateToken(c *gin.Context) {
}
err = cleanToken.Update()
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
@@ -275,10 +225,7 @@ func DeleteTokenBatch(c *gin.Context) {
userId := c.GetInt("id")
count, err := model.BatchDeleteTokens(tokenBatch.Ids, userId)
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
diff --git a/controller/topup_stripe.go b/controller/topup_stripe.go
new file mode 100644
index 00000000000..eb3208092f1
--- /dev/null
+++ b/controller/topup_stripe.go
@@ -0,0 +1,275 @@
+package controller
+
+import (
+ "fmt"
+ "io"
+ "log"
+ "net/http"
+ "one-api/common"
+ "one-api/model"
+ "one-api/setting"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/gin-gonic/gin"
+ "github.com/stripe/stripe-go/v81"
+ "github.com/stripe/stripe-go/v81/checkout/session"
+ "github.com/stripe/stripe-go/v81/webhook"
+ "github.com/thanhpk/randstr"
+)
+
+const (
+ PaymentMethodStripe = "stripe"
+)
+
+var stripeAdaptor = &StripeAdaptor{}
+
+type StripePayRequest struct {
+ Amount int64 `json:"amount"`
+ PaymentMethod string `json:"payment_method"`
+}
+
+type StripeAdaptor struct {
+}
+
+func (*StripeAdaptor) RequestAmount(c *gin.Context, req *StripePayRequest) {
+ if req.Amount < getStripeMinTopup() {
+ c.JSON(200, gin.H{"message": "error", "data": fmt.Sprintf("充值数量不能小于 %d", getStripeMinTopup())})
+ return
+ }
+ id := c.GetInt("id")
+ group, err := model.GetUserGroup(id, true)
+ if err != nil {
+ c.JSON(200, gin.H{"message": "error", "data": "获取用户分组失败"})
+ return
+ }
+ payMoney := getStripePayMoney(float64(req.Amount), group)
+ if payMoney <= 0.01 {
+ c.JSON(200, gin.H{"message": "error", "data": "充值金额过低"})
+ return
+ }
+ c.JSON(200, gin.H{"message": "success", "data": strconv.FormatFloat(payMoney, 'f', 2, 64)})
+}
+
+func (*StripeAdaptor) RequestPay(c *gin.Context, req *StripePayRequest) {
+ if req.PaymentMethod != PaymentMethodStripe {
+ c.JSON(200, gin.H{"message": "error", "data": "不支持的支付渠道"})
+ return
+ }
+ if req.Amount < getStripeMinTopup() {
+ c.JSON(200, gin.H{"message": fmt.Sprintf("充值数量不能小于 %d", getStripeMinTopup()), "data": 10})
+ return
+ }
+ if req.Amount > 10000 {
+ c.JSON(200, gin.H{"message": "充值数量不能大于 10000", "data": 10})
+ return
+ }
+
+ id := c.GetInt("id")
+ user, _ := model.GetUserById(id, false)
+ chargedMoney := GetChargedAmount(float64(req.Amount), *user)
+
+ reference := fmt.Sprintf("new-api-ref-%d-%d-%s", user.Id, time.Now().UnixMilli(), randstr.String(4))
+ referenceId := "ref_" + common.Sha1([]byte(reference))
+
+ payLink, err := genStripeLink(referenceId, user.StripeCustomer, user.Email, req.Amount)
+ if err != nil {
+ log.Println("获取Stripe Checkout支付链接失败", err)
+ c.JSON(200, gin.H{"message": "error", "data": "拉起支付失败"})
+ return
+ }
+
+ topUp := &model.TopUp{
+ UserId: id,
+ Amount: req.Amount,
+ Money: chargedMoney,
+ TradeNo: referenceId,
+ CreateTime: time.Now().Unix(),
+ Status: common.TopUpStatusPending,
+ }
+ err = topUp.Insert()
+ if err != nil {
+ c.JSON(200, gin.H{"message": "error", "data": "创建订单失败"})
+ return
+ }
+ c.JSON(200, gin.H{
+ "message": "success",
+ "data": gin.H{
+ "pay_link": payLink,
+ },
+ })
+}
+
+func RequestStripeAmount(c *gin.Context) {
+ var req StripePayRequest
+ err := c.ShouldBindJSON(&req)
+ if err != nil {
+ c.JSON(200, gin.H{"message": "error", "data": "参数错误"})
+ return
+ }
+ stripeAdaptor.RequestAmount(c, &req)
+}
+
+func RequestStripePay(c *gin.Context) {
+ var req StripePayRequest
+ err := c.ShouldBindJSON(&req)
+ if err != nil {
+ c.JSON(200, gin.H{"message": "error", "data": "参数错误"})
+ return
+ }
+ stripeAdaptor.RequestPay(c, &req)
+}
+
+func StripeWebhook(c *gin.Context) {
+ payload, err := io.ReadAll(c.Request.Body)
+ if err != nil {
+ log.Printf("解析Stripe Webhook参数失败: %v\n", err)
+ c.AbortWithStatus(http.StatusServiceUnavailable)
+ return
+ }
+
+ signature := c.GetHeader("Stripe-Signature")
+ endpointSecret := setting.StripeWebhookSecret
+ event, err := webhook.ConstructEventWithOptions(payload, signature, endpointSecret, webhook.ConstructEventOptions{
+ IgnoreAPIVersionMismatch: true,
+ })
+
+ if err != nil {
+ log.Printf("Stripe Webhook验签失败: %v\n", err)
+ c.AbortWithStatus(http.StatusBadRequest)
+ return
+ }
+
+ switch event.Type {
+ case stripe.EventTypeCheckoutSessionCompleted:
+ sessionCompleted(event)
+ case stripe.EventTypeCheckoutSessionExpired:
+ sessionExpired(event)
+ default:
+ log.Printf("不支持的Stripe Webhook事件类型: %s\n", event.Type)
+ }
+
+ c.Status(http.StatusOK)
+}
+
+func sessionCompleted(event stripe.Event) {
+ customerId := event.GetObjectValue("customer")
+ referenceId := event.GetObjectValue("client_reference_id")
+ status := event.GetObjectValue("status")
+ if "complete" != status {
+ log.Println("错误的Stripe Checkout完成状态:", status, ",", referenceId)
+ return
+ }
+
+ err := model.Recharge(referenceId, customerId)
+ if err != nil {
+ log.Println(err.Error(), referenceId)
+ return
+ }
+
+ total, _ := strconv.ParseFloat(event.GetObjectValue("amount_total"), 64)
+ currency := strings.ToUpper(event.GetObjectValue("currency"))
+ log.Printf("收到款项:%s, %.2f(%s)", referenceId, total/100, currency)
+}
+
+func sessionExpired(event stripe.Event) {
+ referenceId := event.GetObjectValue("client_reference_id")
+ status := event.GetObjectValue("status")
+ if "expired" != status {
+ log.Println("错误的Stripe Checkout过期状态:", status, ",", referenceId)
+ return
+ }
+
+ if len(referenceId) == 0 {
+ log.Println("未提供支付单号")
+ return
+ }
+
+ topUp := model.GetTopUpByTradeNo(referenceId)
+ if topUp == nil {
+ log.Println("充值订单不存在", referenceId)
+ return
+ }
+
+ if topUp.Status != common.TopUpStatusPending {
+ log.Println("充值订单状态错误", referenceId)
+ }
+
+ topUp.Status = common.TopUpStatusExpired
+ err := topUp.Update()
+ if err != nil {
+ log.Println("过期充值订单失败", referenceId, ", err:", err.Error())
+ return
+ }
+
+ log.Println("充值订单已过期", referenceId)
+}
+
+func genStripeLink(referenceId string, customerId string, email string, amount int64) (string, error) {
+ if !strings.HasPrefix(setting.StripeApiSecret, "sk_") && !strings.HasPrefix(setting.StripeApiSecret, "rk_") {
+ return "", fmt.Errorf("无效的Stripe API密钥")
+ }
+
+ stripe.Key = setting.StripeApiSecret
+
+ params := &stripe.CheckoutSessionParams{
+ ClientReferenceID: stripe.String(referenceId),
+ SuccessURL: stripe.String(setting.ServerAddress + "/log"),
+ CancelURL: stripe.String(setting.ServerAddress + "/topup"),
+ LineItems: []*stripe.CheckoutSessionLineItemParams{
+ {
+ Price: stripe.String(setting.StripePriceId),
+ Quantity: stripe.Int64(amount),
+ },
+ },
+ Mode: stripe.String(string(stripe.CheckoutSessionModePayment)),
+ }
+
+ if "" == customerId {
+ if "" != email {
+ params.CustomerEmail = stripe.String(email)
+ }
+
+ params.CustomerCreation = stripe.String(string(stripe.CheckoutSessionCustomerCreationAlways))
+ } else {
+ params.Customer = stripe.String(customerId)
+ }
+
+ result, err := session.New(params)
+ if err != nil {
+ return "", err
+ }
+
+ return result.URL, nil
+}
+
+func GetChargedAmount(count float64, user model.User) float64 {
+ topUpGroupRatio := common.GetTopupGroupRatio(user.Group)
+ if topUpGroupRatio == 0 {
+ topUpGroupRatio = 1
+ }
+
+ return count * topUpGroupRatio
+}
+
+func getStripePayMoney(amount float64, group string) float64 {
+ if !common.DisplayInCurrencyEnabled {
+ amount = amount / common.QuotaPerUnit
+ }
+ // Using float64 for monetary calculations is acceptable here due to the small amounts involved
+ topupGroupRatio := common.GetTopupGroupRatio(group)
+ if topupGroupRatio == 0 {
+ topupGroupRatio = 1
+ }
+ payMoney := amount * setting.StripeUnitPrice * topupGroupRatio
+ return payMoney
+}
+
+func getStripeMinTopup() int64 {
+ minTopup := setting.StripeMinTopUp
+ if !common.DisplayInCurrencyEnabled {
+ minTopup = minTopup * int(common.QuotaPerUnit)
+ }
+ return int64(minTopup)
+}
diff --git a/controller/usedata.go b/controller/usedata.go
index 270eadf3232..4adee50f728 100644
--- a/controller/usedata.go
+++ b/controller/usedata.go
@@ -1,10 +1,12 @@
package controller
import (
- "github.com/gin-gonic/gin"
"net/http"
+ "one-api/common"
"one-api/model"
"strconv"
+
+ "github.com/gin-gonic/gin"
)
func GetAllQuotaDates(c *gin.Context) {
@@ -13,10 +15,7 @@ func GetAllQuotaDates(c *gin.Context) {
username := c.Query("username")
dates, err := model.GetAllQuotaDates(startTimestamp, endTimestamp, username)
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
@@ -41,10 +40,7 @@ func GetUserQuotaDates(c *gin.Context) {
}
dates, err := model.GetQuotaDataByUserId(userId, startTimestamp, endTimestamp)
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
diff --git a/controller/user.go b/controller/user.go
index 44450836f30..292ed8c6e1f 100644
--- a/controller/user.go
+++ b/controller/user.go
@@ -188,10 +188,7 @@ func Register(c *gin.Context) {
cleanUser.Email = user.Email
}
if err := cleanUser.Insert(inviterId); err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
@@ -247,81 +244,45 @@ func Register(c *gin.Context) {
}
func GetAllUsers(c *gin.Context) {
- pageInfo, err := common.GetPageQuery(c)
- if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": "parse page query failed",
- })
- return
- }
+ pageInfo := common.GetPageQuery(c)
users, total, err := model.GetAllUsers(pageInfo)
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
pageInfo.SetTotal(int(total))
pageInfo.SetItems(users)
- c.JSON(http.StatusOK, gin.H{
- "success": true,
- "message": "",
- "data": pageInfo,
- })
+
+ common.ApiSuccess(c, pageInfo)
return
}
func SearchUsers(c *gin.Context) {
keyword := c.Query("keyword")
group := c.Query("group")
- p, _ := strconv.Atoi(c.Query("p"))
- pageSize, _ := strconv.Atoi(c.Query("page_size"))
- if p < 1 {
- p = 1
- }
- if pageSize < 0 {
- pageSize = common.ItemsPerPage
- }
- startIdx := (p - 1) * pageSize
- users, total, err := model.SearchUsers(keyword, group, startIdx, pageSize)
+ pageInfo := common.GetPageQuery(c)
+ users, total, err := model.SearchUsers(keyword, group, pageInfo.GetStartIdx(), pageInfo.GetPageSize())
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
- c.JSON(http.StatusOK, gin.H{
- "success": true,
- "message": "",
- "data": gin.H{
- "items": users,
- "total": total,
- "page": p,
- "page_size": pageSize,
- },
- })
+
+ pageInfo.SetTotal(int(total))
+ pageInfo.SetItems(users)
+ common.ApiSuccess(c, pageInfo)
return
}
func GetUser(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
user, err := model.GetUserById(id, false)
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
myRole := c.GetInt("role")
@@ -344,10 +305,7 @@ func GenerateAccessToken(c *gin.Context) {
id := c.GetInt("id")
user, err := model.GetUserById(id, true)
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
// get rand int 28-32
@@ -372,10 +330,7 @@ func GenerateAccessToken(c *gin.Context) {
}
if err := user.Update(false); err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
@@ -395,18 +350,12 @@ func TransferAffQuota(c *gin.Context) {
id := c.GetInt("id")
user, err := model.GetUserById(id, true)
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
tran := TransferAffQuotaRequest{}
if err := c.ShouldBindJSON(&tran); err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
err = user.TransferAffQuotaToQuota(tran.Quota)
@@ -427,10 +376,7 @@ func GetAffCode(c *gin.Context) {
id := c.GetInt("id")
user, err := model.GetUserById(id, true)
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
if user.AffCode == "" {
@@ -455,10 +401,7 @@ func GetSelf(c *gin.Context) {
id := c.GetInt("id")
user, err := model.GetUserById(id, false)
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
// Hide admin remarks: set to empty to trigger omitempty tag, ensuring the remark field is not included in JSON returned to regular users
@@ -479,10 +422,7 @@ func GetUserModels(c *gin.Context) {
}
user, err := model.GetUserCache(id)
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
groups := setting.GetUserUsableGroups(user.Group)
@@ -524,10 +464,7 @@ func UpdateUser(c *gin.Context) {
}
originUser, err := model.GetUserById(updatedUser.Id, false)
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
myRole := c.GetInt("role")
@@ -550,10 +487,7 @@ func UpdateUser(c *gin.Context) {
}
updatePassword := updatedUser.Password != ""
if err := updatedUser.Edit(updatePassword); err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
if originUser.Quota != updatedUser.Quota {
@@ -599,17 +533,11 @@ func UpdateSelf(c *gin.Context) {
}
updatePassword, err := checkUpdatePassword(user.OriginalPassword, user.Password, cleanUser.Id)
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
if err := cleanUser.Update(updatePassword); err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
@@ -640,18 +568,12 @@ func checkUpdatePassword(originalPassword string, newPassword string, userId int
func DeleteUser(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
originUser, err := model.GetUserById(id, false)
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
myRole := c.GetInt("role")
@@ -686,10 +608,7 @@ func DeleteSelf(c *gin.Context) {
err := model.DeleteUserById(id)
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
@@ -735,10 +654,7 @@ func CreateUser(c *gin.Context) {
DisplayName: user.DisplayName,
}
if err := cleanUser.Insert(0); err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
@@ -848,10 +764,7 @@ func ManageUser(c *gin.Context) {
}
if err := user.Update(false); err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
clearUser := model.User{
@@ -883,20 +796,14 @@ func EmailBind(c *gin.Context) {
}
err := user.FillUserById()
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
user.Email = email
// no need to check if this email already taken, because we have used verification code to check it
err = user.Update(false)
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
@@ -918,19 +825,13 @@ func TopUp(c *gin.Context) {
req := topUpRequest{}
err := c.ShouldBindJSON(&req)
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
id := c.GetInt("id")
quota, err := model.Redeem(req.Key, id)
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
@@ -1013,10 +914,7 @@ func UpdateUserSetting(c *gin.Context) {
userId := c.GetInt("id")
user, err := model.GetUserById(userId, true)
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
diff --git a/controller/wechat.go b/controller/wechat.go
index 9b5f2070ab4..9a4bdfedf61 100644
--- a/controller/wechat.go
+++ b/controller/wechat.go
@@ -4,13 +4,14 @@ import (
"encoding/json"
"errors"
"fmt"
- "github.com/gin-contrib/sessions"
- "github.com/gin-gonic/gin"
"net/http"
"one-api/common"
"one-api/model"
"strconv"
"time"
+
+ "github.com/gin-contrib/sessions"
+ "github.com/gin-gonic/gin"
)
type wechatLoginResponse struct {
@@ -150,19 +151,13 @@ func WeChatBind(c *gin.Context) {
}
err = user.FillUserById()
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
user.WeChatId = wechatId
err = user.Update(false)
if err != nil {
- c.JSON(http.StatusOK, gin.H{
- "success": false,
- "message": err.Error(),
- })
+ common.ApiError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
diff --git a/docs/api/user.md b/docs/api/user.md
deleted file mode 100644
index e69de29bb2d..00000000000
diff --git a/docs/api/web_api.md b/docs/api/web_api.md
new file mode 100644
index 00000000000..ffac74c54b1
--- /dev/null
+++ b/docs/api/web_api.md
@@ -0,0 +1,195 @@
+# One API – Web 界面后端接口文档
+
+> 本文档汇总了 **One API** 后端提供给前端 Web 界面的全部 REST 接口(不含 *Relay* 相关接口)。
+>
+> 接口前缀统一为 `https://`,以下仅列出 **路径**、**HTTP 方法**、**鉴权要求** 与 **功能简介**。
+>
+> 鉴权级别说明:
+> * **公开** – 不需要登录即可调用
+> * **用户** – 需携带用户 Token(`middleware.UserAuth`)
+> * **管理员** – 需管理员 Token(`middleware.AdminAuth`)
+> * **Root** – 仅限最高权限 Root 用户(`middleware.RootAuth`)
+
+---
+
+## 1. 初始化 / 系统状态
+| 方法 | 路径 | 鉴权 | 说明 |
+|------|------|------|------|
+| GET | /api/setup | 公开 | 获取系统初始化状态 |
+| POST | /api/setup | 公开 | 完成首次安装向导 |
+| GET | /api/status | 公开 | 获取运行状态摘要 |
+| GET | /api/uptime/status | 公开 | Uptime-Kuma 兼容状态探针 |
+| GET | /api/status/test | 管理员 | 测试后端与依赖组件是否正常 |
+
+## 2. 公共信息
+| 方法 | 路径 | 鉴权 | 说明 |
+|------|------|------|------|
+| GET | /api/models | 用户 | 获取前端可用模型列表 |
+| GET | /api/notice | 公开 | 获取公告栏内容 |
+| GET | /api/about | 公开 | 关于页面信息 |
+| GET | /api/home_page_content | 公开 | 首页自定义内容 |
+| GET | /api/pricing | 可匿名/用户 | 价格与套餐信息 |
+| GET | /api/ratio_config | 公开 | 模型倍率配置(仅公开字段) |
+
+## 3. 邮件 / 身份验证
+| 方法 | 路径 | 鉴权 | 说明 |
+|------|------|------|------|
+| GET | /api/verification | 公开 (限流) | 发送邮箱验证邮件 |
+| GET | /api/reset_password | 公开 (限流) | 发送重置密码邮件 |
+| POST | /api/user/reset | 公开 | 提交重置密码请求 |
+
+## 4. OAuth / 第三方登录
+| 方法 | 路径 | 鉴权 | 说明 |
+|------|------|------|------|
+| GET | /api/oauth/github | 公开 | GitHub OAuth 跳转 |
+| GET | /api/oauth/oidc | 公开 | OIDC 通用 OAuth 跳转 |
+| GET | /api/oauth/linuxdo | 公开 | LinuxDo OAuth 跳转 |
+| GET | /api/oauth/wechat | 公开 | 微信扫码登录跳转 |
+| GET | /api/oauth/wechat/bind | 公开 | 微信账户绑定 |
+| GET | /api/oauth/email/bind | 公开 | 邮箱绑定 |
+| GET | /api/oauth/telegram/login | 公开 | Telegram 登录 |
+| GET | /api/oauth/telegram/bind | 公开 | Telegram 账户绑定 |
+| GET | /api/oauth/state | 公开 | 获取随机 state(防 CSRF) |
+
+## 5. 用户模块
+### 5.1 账号注册/登录
+| 方法 | 路径 | 鉴权 | 说明 |
+|------|------|------|------|
+| POST | /api/user/register | 公开 | 注册新账号 |
+| POST | /api/user/login | 公开 | 用户登录 |
+| GET | /api/user/logout | 用户 | 退出登录 |
+| GET | /api/user/epay/notify | 公开 | Epay 支付回调 |
+| GET | /api/user/groups | 公开 | 列出所有分组(无鉴权版) |
+
+### 5.2 用户自身操作 (需登录)
+| GET | /api/user/self/groups | 用户 | 获取自己所在分组 |
+| GET | /api/user/self | 用户 | 获取个人资料 |
+| GET | /api/user/models | 用户 | 获取模型可见性 |
+| PUT | /api/user/self | 用户 | 修改个人资料 |
+| DELETE | /api/user/self | 用户 | 注销账号 |
+| GET | /api/user/token | 用户 | 生成用户级别 Access Token |
+| GET | /api/user/aff | 用户 | 获取推广码信息 |
+| POST | /api/user/topup | 用户 | 余额直充 |
+| POST | /api/user/pay | 用户 | 提交支付订单 |
+| POST | /api/user/amount | 用户 | 余额支付 |
+| POST | /api/user/aff_transfer | 用户 | 推广额度转账 |
+| PUT | /api/user/setting | 用户 | 更新用户设置 |
+
+### 5.3 管理员用户管理
+| 方法 | 路径 | 鉴权 | 说明 |
+|------|------|------|------|
+| GET | /api/user/ | 管理员 | 获取全部用户列表 |
+| GET | /api/user/search | 管理员 | 搜索用户 |
+| GET | /api/user/:id | 管理员 | 获取单个用户信息 |
+| POST | /api/user/ | 管理员 | 创建用户 |
+| POST | /api/user/manage | 管理员 | 冻结/重置等管理操作 |
+| PUT | /api/user/ | 管理员 | 更新用户 |
+| DELETE | /api/user/:id | 管理员 | 删除用户 |
+
+## 6. 站点选项 (Root)
+| 方法 | 路径 | 鉴权 | 说明 |
+|------|------|------|------|
+| GET | /api/option/ | Root | 获取全局配置 |
+| PUT | /api/option/ | Root | 更新全局配置 |
+| POST | /api/option/rest_model_ratio | Root | 重置模型倍率 |
+| POST | /api/option/migrate_console_setting | Root | 迁移旧版控制台配置 |
+
+## 7. 模型倍率同步 (Root)
+| 方法 | 路径 | 鉴权 | 说明 |
+|------|------|------|------|
+| GET | /api/ratio_sync/channels | Root | 获取可同步渠道列表 |
+| POST | /api/ratio_sync/fetch | Root | 从上游拉取倍率 |
+
+## 8. 渠道管理 (管理员)
+| 方法 | 路径 | 说明 |
+|------|------|------|
+| GET | /api/channel/ | 获取渠道列表 |
+| GET | /api/channel/search | 搜索渠道 |
+| GET | /api/channel/models | 查询渠道模型能力 |
+| GET | /api/channel/models_enabled | 查询启用模型能力 |
+| GET | /api/channel/:id | 获取单个渠道 |
+| GET | /api/channel/test | 批量测试渠道连通性 |
+| GET | /api/channel/test/:id | 单个渠道测试 |
+| GET | /api/channel/update_balance | 批量刷新余额 |
+| GET | /api/channel/update_balance/:id | 单个刷新余额 |
+| POST | /api/channel/ | 新增渠道 |
+| PUT | /api/channel/ | 更新渠道 |
+| DELETE | /api/channel/disabled | 删除已禁用渠道 |
+| POST | /api/channel/tag/disabled | 批量禁用标签渠道 |
+| POST | /api/channel/tag/enabled | 批量启用标签渠道 |
+| PUT | /api/channel/tag | 编辑渠道标签 |
+| DELETE | /api/channel/:id | 删除渠道 |
+| POST | /api/channel/batch | 批量删除渠道 |
+| POST | /api/channel/fix | 修复渠道能力表 |
+| GET | /api/channel/fetch_models/:id | 拉取单渠道模型 |
+| POST | /api/channel/fetch_models | 拉取全部渠道模型 |
+| POST | /api/channel/batch/tag | 批量设置渠道标签 |
+| GET | /api/channel/tag/models | 根据标签获取模型 |
+| POST | /api/channel/copy/:id | 复制渠道 |
+
+## 9. Token 管理
+| 方法 | 路径 | 鉴权 | 说明 |
+|------|------|------|------|
+| GET | /api/token/ | 用户 | 获取全部 Token |
+| GET | /api/token/search | 用户 | 搜索 Token |
+| GET | /api/token/:id | 用户 | 获取单个 Token |
+| POST | /api/token/ | 用户 | 创建 Token |
+| PUT | /api/token/ | 用户 | 更新 Token |
+| DELETE | /api/token/:id | 用户 | 删除 Token |
+| POST | /api/token/batch | 用户 | 批量删除 Token |
+
+## 10. 兑换码管理 (管理员)
+| 方法 | 路径 | 说明 |
+|------|------|------|
+| GET | /api/redemption/ | 获取兑换码列表 |
+| GET | /api/redemption/search | 搜索兑换码 |
+| GET | /api/redemption/:id | 获取单个兑换码 |
+| POST | /api/redemption/ | 创建兑换码 |
+| PUT | /api/redemption/ | 更新兑换码 |
+| DELETE | /api/redemption/invalid | 删除无效兑换码 |
+| DELETE | /api/redemption/:id | 删除兑换码 |
+
+## 11. 日志
+| 方法 | 路径 | 鉴权 | 说明 |
+|------|------|------|------|
+| GET | /api/log/ | 管理员 | 获取全部日志 |
+| DELETE | /api/log/ | 管理员 | 删除历史日志 |
+| GET | /api/log/stat | 管理员 | 日志统计 |
+| GET | /api/log/self/stat | 用户 | 我的日志统计 |
+| GET | /api/log/search | 管理员 | 搜索全部日志 |
+| GET | /api/log/self | 用户 | 获取我的日志 |
+| GET | /api/log/self/search | 用户 | 搜索我的日志 |
+| GET | /api/log/token | 公开 | 根据 Token 查询日志(支持 CORS) |
+
+## 12. 数据统计
+| 方法 | 路径 | 鉴权 | 说明 |
+|------|------|------|------|
+| GET | /api/data/ | 管理员 | 全站用量按日期统计 |
+| GET | /api/data/self | 用户 | 我的用量按日期统计 |
+
+## 13. 分组
+| GET | /api/group/ | 管理员 | 获取全部分组列表 |
+
+## 14. Midjourney 任务
+| 方法 | 路径 | 鉴权 | 说明 |
+|------|------|------|------|
+| GET | /api/mj/self | 用户 | 获取自己的 MJ 任务 |
+| GET | /api/mj/ | 管理员 | 获取全部 MJ 任务 |
+
+## 15. 任务中心
+| 方法 | 路径 | 鉴权 | 说明 |
+|------|------|------|------|
+| GET | /api/task/self | 用户 | 获取我的任务 |
+| GET | /api/task/ | 管理员 | 获取全部任务 |
+
+## 16. 账户计费面板 (Dashboard)
+| 方法 | 路径 | 鉴权 | 说明 |
+|------|------|------|------|
+| GET | /dashboard/billing/subscription | 用户 Token | 获取订阅额度信息 |
+| GET | /v1/dashboard/billing/subscription | 同上 | 兼容 OpenAI SDK 路径 |
+| GET | /dashboard/billing/usage | 用户 Token | 获取使用量信息 |
+| GET | /v1/dashboard/billing/usage | 同上 | 兼容 OpenAI SDK 路径 |
+
+---
+
+> **更新日期**:2025.07.17
\ No newline at end of file
diff --git a/dto/claude.go b/dto/claude.go
index 98e09c78c23..1a7eacb18f9 100644
--- a/dto/claude.go
+++ b/dto/claude.go
@@ -3,6 +3,7 @@ package dto
import (
"encoding/json"
"one-api/common"
+ "one-api/types"
)
type ClaudeMetadata struct {
@@ -158,6 +159,27 @@ type InputSchema struct {
Required any `json:"required,omitempty"`
}
+type ClaudeWebSearchTool struct {
+ Type string `json:"type"`
+ Name string `json:"name"`
+ MaxUses int `json:"max_uses,omitempty"`
+ UserLocation *ClaudeWebSearchUserLocation `json:"user_location,omitempty"`
+}
+
+type ClaudeWebSearchUserLocation struct {
+ Type string `json:"type"`
+ Timezone string `json:"timezone,omitempty"`
+ Country string `json:"country,omitempty"`
+ Region string `json:"region,omitempty"`
+ City string `json:"city,omitempty"`
+}
+
+type ClaudeToolChoice struct {
+ Type string `json:"type"`
+ Name string `json:"name,omitempty"`
+ DisableParallelToolUse bool `json:"disable_parallel_tool_use,omitempty"`
+}
+
type ClaudeRequest struct {
Model string `json:"model"`
Prompt string `json:"prompt,omitempty"`
@@ -176,6 +198,59 @@ type ClaudeRequest struct {
Thinking *Thinking `json:"thinking,omitempty"`
}
+// AddTool 添加工具到请求中
+func (c *ClaudeRequest) AddTool(tool any) {
+ if c.Tools == nil {
+ c.Tools = make([]any, 0)
+ }
+
+ switch tools := c.Tools.(type) {
+ case []any:
+ c.Tools = append(tools, tool)
+ default:
+ // 如果Tools不是[]any类型,重新初始化为[]any
+ c.Tools = []any{tool}
+ }
+}
+
+// GetTools 获取工具列表
+func (c *ClaudeRequest) GetTools() []any {
+ if c.Tools == nil {
+ return nil
+ }
+
+ switch tools := c.Tools.(type) {
+ case []any:
+ return tools
+ default:
+ return nil
+ }
+}
+
+// ProcessTools 处理工具列表,支持类型断言
+func ProcessTools(tools []any) ([]*Tool, []*ClaudeWebSearchTool) {
+ var normalTools []*Tool
+ var webSearchTools []*ClaudeWebSearchTool
+
+ for _, tool := range tools {
+ switch t := tool.(type) {
+ case *Tool:
+ normalTools = append(normalTools, t)
+ case *ClaudeWebSearchTool:
+ webSearchTools = append(webSearchTools, t)
+ case Tool:
+ normalTools = append(normalTools, &t)
+ case ClaudeWebSearchTool:
+ webSearchTools = append(webSearchTools, &t)
+ default:
+ // 未知类型,跳过
+ continue
+ }
+ }
+
+ return normalTools, webSearchTools
+}
+
type Thinking struct {
Type string `json:"type"`
BudgetTokens *int `json:"budget_tokens,omitempty"`
@@ -228,7 +303,7 @@ type ClaudeResponse struct {
Completion string `json:"completion,omitempty"`
StopReason string `json:"stop_reason,omitempty"`
Model string `json:"model,omitempty"`
- Error *ClaudeError `json:"error,omitempty"`
+ Error *types.ClaudeError `json:"error,omitempty"`
Usage *ClaudeUsage `json:"usage,omitempty"`
Index *int `json:"index,omitempty"`
ContentBlock *ClaudeMediaMessage `json:"content_block,omitempty"`
@@ -250,8 +325,13 @@ func (c *ClaudeResponse) GetIndex() int {
}
type ClaudeUsage struct {
- InputTokens int `json:"input_tokens"`
- CacheCreationInputTokens int `json:"cache_creation_input_tokens"`
- CacheReadInputTokens int `json:"cache_read_input_tokens"`
- OutputTokens int `json:"output_tokens"`
+ InputTokens int `json:"input_tokens"`
+ CacheCreationInputTokens int `json:"cache_creation_input_tokens"`
+ CacheReadInputTokens int `json:"cache_read_input_tokens"`
+ OutputTokens int `json:"output_tokens"`
+ ServerToolUse *ClaudeServerToolUse `json:"server_tool_use"`
+}
+
+type ClaudeServerToolUse struct {
+ WebSearchRequests int `json:"web_search_requests"`
}
diff --git a/dto/error.go b/dto/error.go
index b347f6a159e..d7f6824dc01 100644
--- a/dto/error.go
+++ b/dto/error.go
@@ -1,5 +1,7 @@
package dto
+import "one-api/types"
+
type OpenAIError struct {
Message string `json:"message"`
Type string `json:"type"`
@@ -14,11 +16,11 @@ type OpenAIErrorWithStatusCode struct {
}
type GeneralErrorResponse struct {
- Error OpenAIError `json:"error"`
- Message string `json:"message"`
- Msg string `json:"msg"`
- Err string `json:"err"`
- ErrorMsg string `json:"error_msg"`
+ Error types.OpenAIError `json:"error"`
+ Message string `json:"message"`
+ Msg string `json:"msg"`
+ Err string `json:"err"`
+ ErrorMsg string `json:"error_msg"`
Header struct {
Message string `json:"message"`
} `json:"header"`
diff --git a/dto/openai_request.go b/dto/openai_request.go
index a6567542dc9..88d3bd6cc79 100644
--- a/dto/openai_request.go
+++ b/dto/openai_request.go
@@ -55,6 +55,7 @@ type GeneralOpenAIRequest struct {
EnableThinking any `json:"enable_thinking,omitempty"` // ali
THINKING json.RawMessage `json:"thinking,omitempty"` // doubao
ExtraBody json.RawMessage `json:"extra_body,omitempty"`
+ SearchParameters any `json:"search_parameters,omitempty"` //xai
WebSearchOptions *WebSearchOptions `json:"web_search_options,omitempty"`
// OpenRouter Params
Usage json.RawMessage `json:"usage,omitempty"`
@@ -65,8 +66,8 @@ type GeneralOpenAIRequest struct {
func (r *GeneralOpenAIRequest) ToMap() map[string]any {
result := make(map[string]any)
- data, _ := common.EncodeJson(r)
- _ = common.UnmarshalJson(data, &result)
+ data, _ := common.Marshal(r)
+ _ = common.Unmarshal(data, &result)
return result
}
@@ -602,26 +603,29 @@ type WebSearchOptions struct {
UserLocation json.RawMessage `json:"user_location,omitempty"`
}
+// https://platform.openai.com/docs/api-reference/responses/create
type OpenAIResponsesRequest struct {
- Model string `json:"model"`
- Input json.RawMessage `json:"input,omitempty"`
- Include json.RawMessage `json:"include,omitempty"`
- Instructions json.RawMessage `json:"instructions,omitempty"`
- MaxOutputTokens uint `json:"max_output_tokens,omitempty"`
- Metadata json.RawMessage `json:"metadata,omitempty"`
- ParallelToolCalls bool `json:"parallel_tool_calls,omitempty"`
- PreviousResponseID string `json:"previous_response_id,omitempty"`
- Reasoning *Reasoning `json:"reasoning,omitempty"`
- ServiceTier string `json:"service_tier,omitempty"`
- Store bool `json:"store,omitempty"`
- Stream bool `json:"stream,omitempty"`
- Temperature float64 `json:"temperature,omitempty"`
- Text json.RawMessage `json:"text,omitempty"`
- ToolChoice json.RawMessage `json:"tool_choice,omitempty"`
- Tools []ResponsesToolsCall `json:"tools,omitempty"`
- TopP float64 `json:"top_p,omitempty"`
- Truncation string `json:"truncation,omitempty"`
- User string `json:"user,omitempty"`
+ Model string `json:"model"`
+ Input json.RawMessage `json:"input,omitempty"`
+ Include json.RawMessage `json:"include,omitempty"`
+ Instructions json.RawMessage `json:"instructions,omitempty"`
+ MaxOutputTokens uint `json:"max_output_tokens,omitempty"`
+ Metadata json.RawMessage `json:"metadata,omitempty"`
+ ParallelToolCalls bool `json:"parallel_tool_calls,omitempty"`
+ PreviousResponseID string `json:"previous_response_id,omitempty"`
+ Reasoning *Reasoning `json:"reasoning,omitempty"`
+ ServiceTier string `json:"service_tier,omitempty"`
+ Store bool `json:"store,omitempty"`
+ Stream bool `json:"stream,omitempty"`
+ Temperature float64 `json:"temperature,omitempty"`
+ Text json.RawMessage `json:"text,omitempty"`
+ ToolChoice json.RawMessage `json:"tool_choice,omitempty"`
+ Tools []map[string]any `json:"tools,omitempty"` // 需要处理的参数很少,MCP 参数太多不确定,所以用 map
+ TopP float64 `json:"top_p,omitempty"`
+ Truncation string `json:"truncation,omitempty"`
+ User string `json:"user,omitempty"`
+ MaxToolCalls uint `json:"max_tool_calls,omitempty"`
+ Prompt json.RawMessage `json:"prompt,omitempty"`
}
type Reasoning struct {
@@ -629,23 +633,23 @@ type Reasoning struct {
Summary string `json:"summary,omitempty"`
}
-type ResponsesToolsCall struct {
- Type string `json:"type"`
- // Web Search
- UserLocation json.RawMessage `json:"user_location,omitempty"`
- SearchContextSize string `json:"search_context_size,omitempty"`
- // File Search
- VectorStoreIds []string `json:"vector_store_ids,omitempty"`
- MaxNumResults uint `json:"max_num_results,omitempty"`
- Filters json.RawMessage `json:"filters,omitempty"`
- // Computer Use
- DisplayWidth uint `json:"display_width,omitempty"`
- DisplayHeight uint `json:"display_height,omitempty"`
- Environment string `json:"environment,omitempty"`
- // Function
- Name string `json:"name,omitempty"`
- Description string `json:"description,omitempty"`
- Parameters json.RawMessage `json:"parameters,omitempty"`
- Function json.RawMessage `json:"function,omitempty"`
- Container json.RawMessage `json:"container,omitempty"`
-}
+//type ResponsesToolsCall struct {
+// Type string `json:"type"`
+// // Web Search
+// UserLocation json.RawMessage `json:"user_location,omitempty"`
+// SearchContextSize string `json:"search_context_size,omitempty"`
+// // File Search
+// VectorStoreIds []string `json:"vector_store_ids,omitempty"`
+// MaxNumResults uint `json:"max_num_results,omitempty"`
+// Filters json.RawMessage `json:"filters,omitempty"`
+// // Computer Use
+// DisplayWidth uint `json:"display_width,omitempty"`
+// DisplayHeight uint `json:"display_height,omitempty"`
+// Environment string `json:"environment,omitempty"`
+// // Function
+// Name string `json:"name,omitempty"`
+// Description string `json:"description,omitempty"`
+// Parameters json.RawMessage `json:"parameters,omitempty"`
+// Function json.RawMessage `json:"function,omitempty"`
+// Container json.RawMessage `json:"container,omitempty"`
+//}
diff --git a/dto/openai_response.go b/dto/openai_response.go
index d95acd9eb19..4e5348230c2 100644
--- a/dto/openai_response.go
+++ b/dto/openai_response.go
@@ -1,6 +1,9 @@
package dto
-import "encoding/json"
+import (
+ "encoding/json"
+ "one-api/types"
+)
type SimpleResponse struct {
Usage `json:"usage"`
@@ -28,7 +31,7 @@ type OpenAITextResponse struct {
Object string `json:"object"`
Created any `json:"created"`
Choices []OpenAITextResponseChoice `json:"choices"`
- Error *OpenAIError `json:"error,omitempty"`
+ Error *types.OpenAIError `json:"error,omitempty"`
Usage `json:"usage"`
}
@@ -45,6 +48,19 @@ type OpenAIEmbeddingResponse struct {
Usage `json:"usage"`
}
+type FlexibleEmbeddingResponseItem struct {
+ Object string `json:"object"`
+ Index int `json:"index"`
+ Embedding any `json:"embedding"`
+}
+
+type FlexibleEmbeddingResponse struct {
+ Object string `json:"object"`
+ Data []FlexibleEmbeddingResponseItem `json:"data"`
+ Model string `json:"model"`
+ Usage `json:"usage"`
+}
+
type ChatCompletionsStreamResponseChoice struct {
Delta ChatCompletionsStreamResponseChoiceDelta `json:"delta,omitempty"`
Logprobs *any `json:"logprobs"`
@@ -179,7 +195,7 @@ type Usage struct {
OutputTokens int `json:"output_tokens"`
InputTokensDetails *InputTokenDetails `json:"input_tokens_details"`
// OpenRouter Params
- Cost float64 `json:"cost,omitempty"`
+ Cost any `json:"cost,omitempty"`
}
type InputTokenDetails struct {
@@ -197,28 +213,28 @@ type OutputTokenDetails struct {
}
type OpenAIResponsesResponse struct {
- ID string `json:"id"`
- Object string `json:"object"`
- CreatedAt int `json:"created_at"`
- Status string `json:"status"`
- Error *OpenAIError `json:"error,omitempty"`
- IncompleteDetails *IncompleteDetails `json:"incomplete_details,omitempty"`
- Instructions string `json:"instructions"`
- MaxOutputTokens int `json:"max_output_tokens"`
- Model string `json:"model"`
- Output []ResponsesOutput `json:"output"`
- ParallelToolCalls bool `json:"parallel_tool_calls"`
- PreviousResponseID string `json:"previous_response_id"`
- Reasoning *Reasoning `json:"reasoning"`
- Store bool `json:"store"`
- Temperature float64 `json:"temperature"`
- ToolChoice string `json:"tool_choice"`
- Tools []ResponsesToolsCall `json:"tools"`
- TopP float64 `json:"top_p"`
- Truncation string `json:"truncation"`
- Usage *Usage `json:"usage"`
- User json.RawMessage `json:"user"`
- Metadata json.RawMessage `json:"metadata"`
+ ID string `json:"id"`
+ Object string `json:"object"`
+ CreatedAt int `json:"created_at"`
+ Status string `json:"status"`
+ Error *types.OpenAIError `json:"error,omitempty"`
+ IncompleteDetails *IncompleteDetails `json:"incomplete_details,omitempty"`
+ Instructions string `json:"instructions"`
+ MaxOutputTokens int `json:"max_output_tokens"`
+ Model string `json:"model"`
+ Output []ResponsesOutput `json:"output"`
+ ParallelToolCalls bool `json:"parallel_tool_calls"`
+ PreviousResponseID string `json:"previous_response_id"`
+ Reasoning *Reasoning `json:"reasoning"`
+ Store bool `json:"store"`
+ Temperature float64 `json:"temperature"`
+ ToolChoice string `json:"tool_choice"`
+ Tools []map[string]any `json:"tools"`
+ TopP float64 `json:"top_p"`
+ Truncation string `json:"truncation"`
+ Usage *Usage `json:"usage"`
+ User json.RawMessage `json:"user"`
+ Metadata json.RawMessage `json:"metadata"`
}
type IncompleteDetails struct {
diff --git a/dto/realtime.go b/dto/realtime.go
index 86ae352df71..32a6905684a 100644
--- a/dto/realtime.go
+++ b/dto/realtime.go
@@ -1,5 +1,7 @@
package dto
+import "one-api/types"
+
const (
RealtimeEventTypeError = "error"
RealtimeEventTypeSessionUpdate = "session.update"
@@ -23,12 +25,12 @@ type RealtimeEvent struct {
EventId string `json:"event_id"`
Type string `json:"type"`
//PreviousItemId string `json:"previous_item_id"`
- Session *RealtimeSession `json:"session,omitempty"`
- Item *RealtimeItem `json:"item,omitempty"`
- Error *OpenAIError `json:"error,omitempty"`
- Response *RealtimeResponse `json:"response,omitempty"`
- Delta string `json:"delta,omitempty"`
- Audio string `json:"audio,omitempty"`
+ Session *RealtimeSession `json:"session,omitempty"`
+ Item *RealtimeItem `json:"item,omitempty"`
+ Error *types.OpenAIError `json:"error,omitempty"`
+ Response *RealtimeResponse `json:"response,omitempty"`
+ Delta string `json:"delta,omitempty"`
+ Audio string `json:"audio,omitempty"`
}
type RealtimeResponse struct {
diff --git a/go.mod b/go.mod
index 9479ba55212..94873c88a2c 100644
--- a/go.mod
+++ b/go.mod
@@ -27,10 +27,13 @@ require (
github.com/samber/lo v1.39.0
github.com/shirou/gopsutil v3.21.11+incompatible
github.com/shopspring/decimal v1.4.0
+ github.com/stripe/stripe-go/v81 v81.4.0
+ github.com/thanhpk/randstr v1.0.6
github.com/tiktoken-go/tokenizer v0.6.2
golang.org/x/crypto v0.35.0
golang.org/x/image v0.23.0
golang.org/x/net v0.35.0
+ golang.org/x/sync v0.11.0
gorm.io/driver/mysql v1.4.3
gorm.io/driver/postgres v1.5.2
gorm.io/gorm v1.25.2
@@ -84,7 +87,6 @@ require (
github.com/yusufpapurcu/wmi v1.2.3 // indirect
golang.org/x/arch v0.12.0 // indirect
golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 // indirect
- golang.org/x/sync v0.11.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.22.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
diff --git a/go.sum b/go.sum
index 71dd83c2202..74eecd4c2f7 100644
--- a/go.sum
+++ b/go.sum
@@ -195,6 +195,10 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/stripe/stripe-go/v81 v81.4.0 h1:AuD9XzdAvl193qUCSaLocf8H+nRopOouXhxqJUzCLbw=
+github.com/stripe/stripe-go/v81 v81.4.0/go.mod h1:C/F4jlmnGNacvYtBp/LUHCvVUJEZffFQCobkzwY1WOo=
+github.com/thanhpk/randstr v1.0.6 h1:psAOktJFD4vV9NEVb3qkhRSMvYh4ORRaj1+w/hn4B+o=
+github.com/thanhpk/randstr v1.0.6/go.mod h1:M/H2P1eNLZzlDwAzpkkkUvoyNNMbzRGhESZuEQk3r0U=
github.com/tiktoken-go/tokenizer v0.6.2 h1:t0GN2DvcUZSFWT/62YOgoqb10y7gSXBGs0A+4VCQK+g=
github.com/tiktoken-go/tokenizer v0.6.2/go.mod h1:6UCYI/DtOallbmL7sSy30p6YQv60qNyU/4aVigPOx6w=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
@@ -224,6 +228,7 @@ golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0/go.mod h1:/lliqkxwWAhPjf5oSO
golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68=
golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -232,6 +237,7 @@ golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
diff --git a/middleware/auth.go b/middleware/auth.go
index ecf4844bc9b..a158318c585 100644
--- a/middleware/auth.go
+++ b/middleware/auth.go
@@ -1,6 +1,7 @@
package middleware
import (
+ "fmt"
"net/http"
"one-api/common"
"one-api/model"
@@ -122,6 +123,18 @@ func authHelper(c *gin.Context, minRole int) {
c.Set("id", id)
c.Set("group", session.Get("group"))
c.Set("use_access_token", useAccessToken)
+
+ //userCache, err := model.GetUserCache(id.(int))
+ //if err != nil {
+ // c.JSON(http.StatusOK, gin.H{
+ // "success": false,
+ // "message": err.Error(),
+ // })
+ // c.Abort()
+ // return
+ //}
+ //userCache.WriteContext(c)
+
c.Next()
}
@@ -233,30 +246,41 @@ func TokenAuth() func(c *gin.Context) {
userCache.WriteContext(c)
- c.Set("id", token.UserId)
- c.Set("token_id", token.Id)
- c.Set("token_key", token.Key)
- c.Set("token_name", token.Name)
- c.Set("token_unlimited_quota", token.UnlimitedQuota)
- if !token.UnlimitedQuota {
- c.Set("token_quota", token.RemainQuota)
+ err = SetupContextForToken(c, token, parts...)
+ if err != nil {
+ return
}
- if token.ModelLimitsEnabled {
- c.Set("token_model_limit_enabled", true)
- c.Set("token_model_limit", token.GetModelLimitsMap())
+ c.Next()
+ }
+}
+
+func SetupContextForToken(c *gin.Context, token *model.Token, parts ...string) error {
+ if token == nil {
+ return fmt.Errorf("token is nil")
+ }
+ c.Set("id", token.UserId)
+ c.Set("token_id", token.Id)
+ c.Set("token_key", token.Key)
+ c.Set("token_name", token.Name)
+ c.Set("token_unlimited_quota", token.UnlimitedQuota)
+ if !token.UnlimitedQuota {
+ c.Set("token_quota", token.RemainQuota)
+ }
+ if token.ModelLimitsEnabled {
+ c.Set("token_model_limit_enabled", true)
+ c.Set("token_model_limit", token.GetModelLimitsMap())
+ } else {
+ c.Set("token_model_limit_enabled", false)
+ }
+ c.Set("allow_ips", token.GetIpLimitsMap())
+ c.Set("token_group", token.Group)
+ if len(parts) > 1 {
+ if model.IsAdmin(token.UserId) {
+ c.Set("specific_channel_id", parts[1])
} else {
- c.Set("token_model_limit_enabled", false)
+ abortWithOpenAiMessage(c, http.StatusForbidden, "普通用户不支持指定渠道")
+ return fmt.Errorf("普通用户不支持指定渠道")
}
- c.Set("allow_ips", token.GetIpLimitsMap())
- c.Set("token_group", token.Group)
- if len(parts) > 1 {
- if model.IsAdmin(token.UserId) {
- c.Set("specific_channel_id", parts[1])
- } else {
- abortWithOpenAiMessage(c, http.StatusForbidden, "普通用户不支持指定渠道")
- return
- }
- }
- c.Next()
}
+ return nil
}
diff --git a/middleware/distributor.go b/middleware/distributor.go
index 642b5253812..a6889e39646 100644
--- a/middleware/distributor.go
+++ b/middleware/distributor.go
@@ -12,6 +12,7 @@ import (
"one-api/service"
"one-api/setting"
"one-api/setting/ratio_setting"
+ "one-api/types"
"strconv"
"strings"
"time"
@@ -21,6 +22,7 @@ import (
type ModelRequest struct {
Model string `json:"model"`
+ Group string `json:"group,omitempty"`
}
func Distribute() func(c *gin.Context) {
@@ -237,28 +239,47 @@ func getModelRequest(c *gin.Context) (*ModelRequest, bool, error) {
}
c.Set("relay_mode", relayMode)
}
+ if strings.HasPrefix(c.Request.URL.Path, "/pg/chat/completions") {
+ // playground chat completions
+ err = common.UnmarshalBodyReusable(c, &modelRequest)
+ if err != nil {
+ return nil, false, errors.New("无效的请求, " + err.Error())
+ }
+ common.SetContextKey(c, constant.ContextKeyTokenGroup, modelRequest.Group)
+ }
return &modelRequest, shouldSelectChannel, nil
}
-func SetupContextForSelectedChannel(c *gin.Context, channel *model.Channel, modelName string) {
+func SetupContextForSelectedChannel(c *gin.Context, channel *model.Channel, modelName string) *types.NewAPIError {
c.Set("original_model", modelName) // for retry
if channel == nil {
- return
+ return types.NewError(errors.New("channel is nil"), types.ErrorCodeGetChannelFailed)
}
- c.Set("channel_id", channel.Id)
- c.Set("channel_name", channel.Name)
+ common.SetContextKey(c, constant.ContextKeyChannelId, channel.Id)
+ common.SetContextKey(c, constant.ContextKeyChannelName, channel.Name)
common.SetContextKey(c, constant.ContextKeyChannelType, channel.Type)
- c.Set("channel_create_time", channel.CreatedTime)
+ common.SetContextKey(c, constant.ContextKeyChannelCreateTime, channel.CreatedTime)
common.SetContextKey(c, constant.ContextKeyChannelSetting, channel.GetSetting())
- c.Set("param_override", channel.GetParamOverride())
- if nil != channel.OpenAIOrganization && "" != *channel.OpenAIOrganization {
- c.Set("channel_organization", *channel.OpenAIOrganization)
+ common.SetContextKey(c, constant.ContextKeyChannelParamOverride, channel.GetParamOverride())
+ if nil != channel.OpenAIOrganization && *channel.OpenAIOrganization != "" {
+ common.SetContextKey(c, constant.ContextKeyChannelOrganization, *channel.OpenAIOrganization)
}
- c.Set("auto_ban", channel.GetAutoBan())
- c.Set("model_mapping", channel.GetModelMapping())
- c.Set("status_code_mapping", channel.GetStatusCodeMapping())
- c.Request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", channel.Key))
- common.SetContextKey(c, constant.ContextKeyBaseUrl, channel.GetBaseURL())
+ common.SetContextKey(c, constant.ContextKeyChannelAutoBan, channel.GetAutoBan())
+ common.SetContextKey(c, constant.ContextKeyChannelModelMapping, channel.GetModelMapping())
+ common.SetContextKey(c, constant.ContextKeyChannelStatusCodeMapping, channel.GetStatusCodeMapping())
+
+ key, index, newAPIError := channel.GetNextEnabledKey()
+ if newAPIError != nil {
+ return newAPIError
+ }
+ if channel.ChannelInfo.IsMultiKey {
+ common.SetContextKey(c, constant.ContextKeyChannelIsMultiKey, true)
+ common.SetContextKey(c, constant.ContextKeyChannelMultiKeyIndex, index)
+ }
+ // c.Request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", key))
+ common.SetContextKey(c, constant.ContextKeyChannelKey, key)
+ common.SetContextKey(c, constant.ContextKeyChannelBaseUrl, channel.GetBaseURL())
+
// TODO: api_version统一
switch channel.Type {
case constant.ChannelTypeAzure:
@@ -278,6 +299,7 @@ func SetupContextForSelectedChannel(c *gin.Context, channel *model.Channel, mode
case constant.ChannelTypeCoze:
c.Set("bot_id", channel.Other)
}
+ return nil
}
// extractModelNameFromGeminiPath 从 Gemini API URL 路径中提取模型名
diff --git a/middleware/kling_adapter.go b/middleware/kling_adapter.go
index 8e2a35519d3..3d4943d28f7 100644
--- a/middleware/kling_adapter.go
+++ b/middleware/kling_adapter.go
@@ -18,7 +18,7 @@ func KlingRequestConvert() func(c *gin.Context) {
return
}
- model, _ := originalReq["model"].(string)
+ model, _ := originalReq["model_name"].(string)
prompt, _ := originalReq["prompt"].(string)
unifiedReq := map[string]interface{}{
@@ -36,7 +36,7 @@ func KlingRequestConvert() func(c *gin.Context) {
// Rewrite request body and path
c.Request.Body = io.NopCloser(bytes.NewBuffer(jsonData))
c.Request.URL.Path = "/v1/video/generations"
- if image := originalReq["image"]; image == "" {
+ if image, ok := originalReq["image"]; !ok || image == "" {
c.Set("action", constant.TaskActionTextGenerate)
}
diff --git a/model/ability.go b/model/ability.go
index ed124676990..f36ff76421a 100644
--- a/model/ability.go
+++ b/model/ability.go
@@ -87,26 +87,29 @@ func getPriority(group string, model string, retry int) (int, error) {
return priorityToUse, nil
}
-func getChannelQuery(group string, model string, retry int) *gorm.DB {
+func getChannelQuery(group string, model string, retry int) (*gorm.DB, error) {
maxPrioritySubQuery := DB.Model(&Ability{}).Select("MAX(priority)").Where(commonGroupCol+" = ? and model = ? and enabled = ?", group, model, true)
channelQuery := DB.Where(commonGroupCol+" = ? and model = ? and enabled = ? and priority = (?)", group, model, true, maxPrioritySubQuery)
if retry != 0 {
priority, err := getPriority(group, model, retry)
if err != nil {
- common.SysError(fmt.Sprintf("Get priority failed: %s", err.Error()))
+ return nil, err
} else {
channelQuery = DB.Where(commonGroupCol+" = ? and model = ? and enabled = ? and priority = ?", group, model, true, priority)
}
}
- return channelQuery
+ return channelQuery, nil
}
func GetRandomSatisfiedChannel(group string, model string, retry int) (*Channel, error) {
var abilities []Ability
var err error = nil
- channelQuery := getChannelQuery(group, model, retry)
+ channelQuery, err := getChannelQuery(group, model, retry)
+ if err != nil {
+ return nil, err
+ }
if common.UsingSQLite || common.UsingPostgreSQL {
err = channelQuery.Order("weight DESC").Find(&abilities).Error
} else {
diff --git a/model/channel.go b/model/channel.go
index a5f307ef48e..6277fcda2b3 100644
--- a/model/channel.go
+++ b/model/channel.go
@@ -1,9 +1,15 @@
package model
import (
+ "database/sql/driver"
"encoding/json"
+ "errors"
+ "fmt"
+ "math/rand"
"one-api/common"
+ "one-api/constant"
"one-api/dto"
+ "one-api/types"
"strings"
"sync"
@@ -36,8 +42,138 @@ type Channel struct {
AutoBan *int `json:"auto_ban" gorm:"default:1"`
OtherInfo string `json:"other_info"`
Tag *string `json:"tag" gorm:"index"`
- Setting *string `json:"setting" gorm:"type:text"`
+ Setting *string `json:"setting" gorm:"type:text"` // 渠道额外设置
ParamOverride *string `json:"param_override" gorm:"type:text"`
+ // add after v0.8.5
+ ChannelInfo ChannelInfo `json:"channel_info" gorm:"type:json"`
+}
+
+type ChannelInfo struct {
+ IsMultiKey bool `json:"is_multi_key"` // 是否多Key模式
+ MultiKeySize int `json:"multi_key_size"` // 多Key模式下的Key数量
+ MultiKeyStatusList map[int]int `json:"multi_key_status_list"` // key状态列表,key index -> status
+ MultiKeyPollingIndex int `json:"multi_key_polling_index"` // 多Key模式下轮询的key索引
+ MultiKeyMode constant.MultiKeyMode `json:"multi_key_mode"`
+}
+
+// Value implements driver.Valuer interface
+func (c ChannelInfo) Value() (driver.Value, error) {
+ return common.Marshal(&c)
+}
+
+// Scan implements sql.Scanner interface
+func (c *ChannelInfo) Scan(value interface{}) error {
+ bytesValue, _ := value.([]byte)
+ return common.Unmarshal(bytesValue, c)
+}
+
+func (channel *Channel) getKeys() []string {
+ if channel.Key == "" {
+ return []string{}
+ }
+ trimmed := strings.TrimSpace(channel.Key)
+ // If the key starts with '[', try to parse it as a JSON array (e.g., for Vertex AI scenarios)
+ if strings.HasPrefix(trimmed, "[") {
+ var arr []json.RawMessage
+ if err := json.Unmarshal([]byte(trimmed), &arr); err == nil {
+ res := make([]string, len(arr))
+ for i, v := range arr {
+ res[i] = string(v)
+ }
+ return res
+ }
+ }
+ // Otherwise, fall back to splitting by newline
+ keys := strings.Split(strings.Trim(channel.Key, "\n"), "\n")
+ return keys
+}
+
+func (channel *Channel) GetNextEnabledKey() (string, int, *types.NewAPIError) {
+ // If not in multi-key mode, return the original key string directly.
+ if !channel.ChannelInfo.IsMultiKey {
+ return channel.Key, 0, nil
+ }
+
+ // Obtain all keys (split by \n)
+ keys := channel.getKeys()
+ if len(keys) == 0 {
+ // No keys available, return error, should disable the channel
+ return "", 0, types.NewError(errors.New("no keys available"), types.ErrorCodeChannelNoAvailableKey)
+ }
+
+ statusList := channel.ChannelInfo.MultiKeyStatusList
+ // helper to get key status, default to enabled when missing
+ getStatus := func(idx int) int {
+ if statusList == nil {
+ return common.ChannelStatusEnabled
+ }
+ if status, ok := statusList[idx]; ok {
+ return status
+ }
+ return common.ChannelStatusEnabled
+ }
+
+ // Collect indexes of enabled keys
+ enabledIdx := make([]int, 0, len(keys))
+ for i := range keys {
+ if getStatus(i) == common.ChannelStatusEnabled {
+ enabledIdx = append(enabledIdx, i)
+ }
+ }
+ // If no specific status list or none enabled, fall back to first key
+ if len(enabledIdx) == 0 {
+ return keys[0], 0, nil
+ }
+
+ switch channel.ChannelInfo.MultiKeyMode {
+ case constant.MultiKeyModeRandom:
+ // Randomly pick one enabled key
+ selectedIdx := enabledIdx[rand.Intn(len(enabledIdx))]
+ return keys[selectedIdx], selectedIdx, nil
+ case constant.MultiKeyModePolling:
+ // Use channel-specific lock to ensure thread-safe polling
+ lock := getChannelPollingLock(channel.Id)
+ lock.Lock()
+ defer lock.Unlock()
+
+ channelInfo, err := CacheGetChannelInfo(channel.Id)
+ if err != nil {
+ return "", 0, types.NewError(err, types.ErrorCodeGetChannelFailed)
+ }
+ //println("before polling index:", channel.ChannelInfo.MultiKeyPollingIndex)
+ defer func() {
+ if common.DebugEnabled {
+ println(fmt.Sprintf("channel %d polling index: %d", channel.Id, channel.ChannelInfo.MultiKeyPollingIndex))
+ }
+ if !common.MemoryCacheEnabled {
+ _ = channel.SaveChannelInfo()
+ } else {
+ // CacheUpdateChannel(channel)
+ }
+ }()
+ // Start from the saved polling index and look for the next enabled key
+ start := channelInfo.MultiKeyPollingIndex
+ if start < 0 || start >= len(keys) {
+ start = 0
+ }
+ for i := 0; i < len(keys); i++ {
+ idx := (start + i) % len(keys)
+ if getStatus(idx) == common.ChannelStatusEnabled {
+ // update polling index for next call (point to the next position)
+ channel.ChannelInfo.MultiKeyPollingIndex = (idx + 1) % len(keys)
+ return keys[idx], idx, nil
+ }
+ }
+ // Fallback – should not happen, but return first enabled key
+ return keys[enabledIdx[0]], enabledIdx[0], nil
+ default:
+ // Unknown mode, default to first enabled key (or original key string)
+ return keys[enabledIdx[0]], enabledIdx[0], nil
+ }
+}
+
+func (channel *Channel) SaveChannelInfo() error {
+ return DB.Model(channel).Update("channel_info", channel.ChannelInfo).Error
}
func (channel *Channel) GetModels() []string {
@@ -175,14 +311,20 @@ func SearchChannels(keyword string, group string, model string, idSort bool) ([]
}
func GetChannelById(id int, selectAll bool) (*Channel, error) {
- channel := Channel{Id: id}
+ channel := &Channel{Id: id}
var err error = nil
if selectAll {
- err = DB.First(&channel, "id = ?", id).Error
+ err = DB.First(channel, "id = ?", id).Error
} else {
- err = DB.Omit("key").First(&channel, "id = ?", id).Error
+ err = DB.Omit("key").First(channel, "id = ?", id).Error
+ }
+ if err != nil {
+ return nil, err
}
- return &channel, err
+ if channel == nil {
+ return nil, errors.New("channel not found")
+ }
+ return channel, nil
}
func BatchInsertChannels(channels []Channel) error {
@@ -266,6 +408,44 @@ func (channel *Channel) Insert() error {
}
func (channel *Channel) Update() error {
+ // If this is a multi-key channel, recalculate MultiKeySize based on the current key list to avoid inconsistency after editing keys
+ if channel.ChannelInfo.IsMultiKey {
+ var keyStr string
+ if channel.Key != "" {
+ keyStr = channel.Key
+ } else {
+ // If key is not provided, read the existing key from the database
+ if existing, err := GetChannelById(channel.Id, true); err == nil {
+ keyStr = existing.Key
+ }
+ }
+ // Parse the key list (supports newline separation or JSON array)
+ keys := []string{}
+ if keyStr != "" {
+ trimmed := strings.TrimSpace(keyStr)
+ if strings.HasPrefix(trimmed, "[") {
+ var arr []json.RawMessage
+ if err := json.Unmarshal([]byte(trimmed), &arr); err == nil {
+ keys = make([]string, len(arr))
+ for i, v := range arr {
+ keys[i] = string(v)
+ }
+ }
+ }
+ if len(keys) == 0 { // fallback to newline split
+ keys = strings.Split(strings.Trim(keyStr, "\n"), "\n")
+ }
+ }
+ channel.ChannelInfo.MultiKeySize = len(keys)
+ // Clean up status data that exceeds the new key count to prevent index out of range
+ if channel.ChannelInfo.MultiKeyStatusList != nil {
+ for idx := range channel.ChannelInfo.MultiKeyStatusList {
+ if idx >= channel.ChannelInfo.MultiKeySize {
+ delete(channel.ChannelInfo.MultiKeyStatusList, idx)
+ }
+ }
+ }
+ }
var err error
err = DB.Model(channel).Updates(channel).Error
if err != nil {
@@ -308,48 +488,128 @@ func (channel *Channel) Delete() error {
var channelStatusLock sync.Mutex
-func UpdateChannelStatusById(id int, status int, reason string) bool {
+// channelPollingLocks stores locks for each channel.id to ensure thread-safe polling
+var channelPollingLocks sync.Map
+
+// getChannelPollingLock returns or creates a mutex for the given channel ID
+func getChannelPollingLock(channelId int) *sync.Mutex {
+ if lock, exists := channelPollingLocks.Load(channelId); exists {
+ return lock.(*sync.Mutex)
+ }
+ // Create new lock for this channel
+ newLock := &sync.Mutex{}
+ actual, _ := channelPollingLocks.LoadOrStore(channelId, newLock)
+ return actual.(*sync.Mutex)
+}
+
+// CleanupChannelPollingLocks removes locks for channels that no longer exist
+// This is optional and can be called periodically to prevent memory leaks
+func CleanupChannelPollingLocks() {
+ var activeChannelIds []int
+ DB.Model(&Channel{}).Pluck("id", &activeChannelIds)
+
+ activeChannelSet := make(map[int]bool)
+ for _, id := range activeChannelIds {
+ activeChannelSet[id] = true
+ }
+
+ channelPollingLocks.Range(func(key, value interface{}) bool {
+ channelId := key.(int)
+ if !activeChannelSet[channelId] {
+ channelPollingLocks.Delete(channelId)
+ }
+ return true
+ })
+}
+
+func handlerMultiKeyUpdate(channel *Channel, usingKey string, status int) {
+ keys := channel.getKeys()
+ if len(keys) == 0 {
+ channel.Status = status
+ } else {
+ var keyIndex int
+ for i, key := range keys {
+ if key == usingKey {
+ keyIndex = i
+ break
+ }
+ }
+ if channel.ChannelInfo.MultiKeyStatusList == nil {
+ channel.ChannelInfo.MultiKeyStatusList = make(map[int]int)
+ }
+ if status == common.ChannelStatusEnabled {
+ delete(channel.ChannelInfo.MultiKeyStatusList, keyIndex)
+ } else {
+ channel.ChannelInfo.MultiKeyStatusList[keyIndex] = status
+ }
+ if len(channel.ChannelInfo.MultiKeyStatusList) >= channel.ChannelInfo.MultiKeySize {
+ channel.Status = common.ChannelStatusAutoDisabled
+ info := channel.GetOtherInfo()
+ info["status_reason"] = "All keys are disabled"
+ info["status_time"] = common.GetTimestamp()
+ channel.SetOtherInfo(info)
+ }
+ }
+}
+
+func UpdateChannelStatus(channelId int, usingKey string, status int, reason string) bool {
if common.MemoryCacheEnabled {
channelStatusLock.Lock()
defer channelStatusLock.Unlock()
- channelCache, _ := CacheGetChannel(id)
- // 如果缓存渠道存在,且状态已是目标状态,直接返回
- if channelCache != nil && channelCache.Status == status {
+ channelCache, _ := CacheGetChannel(channelId)
+ if channelCache == nil {
return false
}
- // 如果缓存渠道不存在(说明已经被禁用),且要设置的状态不为启用,直接返回
- if channelCache == nil && status != common.ChannelStatusEnabled {
- return false
+ if channelCache.ChannelInfo.IsMultiKey {
+ // 如果是多Key模式,更新缓存中的状态
+ handlerMultiKeyUpdate(channelCache, usingKey, status)
+ //CacheUpdateChannel(channelCache)
+ //return true
+ } else {
+ // 如果缓存渠道存在,且状态已是目标状态,直接返回
+ if channelCache.Status == status {
+ return false
+ }
+ // 如果缓存渠道不存在(说明已经被禁用),且要设置的状态不为启用,直接返回
+ if status != common.ChannelStatusEnabled {
+ return false
+ }
+ CacheUpdateChannelStatus(channelId, status)
}
- CacheUpdateChannelStatus(id, status)
}
- err := UpdateAbilityStatus(id, status == common.ChannelStatusEnabled)
+
+ shouldUpdateAbilities := false
+ defer func() {
+ if shouldUpdateAbilities {
+ err := UpdateAbilityStatus(channelId, status == common.ChannelStatusEnabled)
+ if err != nil {
+ common.SysError("failed to update ability status: " + err.Error())
+ }
+ }
+ }()
+ channel, err := GetChannelById(channelId, true)
if err != nil {
- common.SysError("failed to update ability status: " + err.Error())
return false
- }
- channel, err := GetChannelById(id, true)
- if err != nil {
- // find channel by id error, directly update status
- result := DB.Model(&Channel{}).Where("id = ?", id).Update("status", status)
- if result.Error != nil {
- common.SysError("failed to update channel status: " + result.Error.Error())
- return false
- }
- if result.RowsAffected == 0 {
- return false
- }
} else {
if channel.Status == status {
return false
}
- // find channel by id success, update status and other info
- info := channel.GetOtherInfo()
- info["status_reason"] = reason
- info["status_time"] = common.GetTimestamp()
- channel.SetOtherInfo(info)
- channel.Status = status
+
+ if channel.ChannelInfo.IsMultiKey {
+ beforeStatus := channel.Status
+ handlerMultiKeyUpdate(channel, usingKey, status)
+ if beforeStatus != channel.Status {
+ shouldUpdateAbilities = true
+ }
+ } else {
+ info := channel.GetOtherInfo()
+ info["status_reason"] = reason
+ info["status_time"] = common.GetTimestamp()
+ channel.SetOtherInfo(info)
+ channel.Status = status
+ shouldUpdateAbilities = true
+ }
err = channel.Save()
if err != nil {
common.SysError("failed to update channel status: " + err.Error())
@@ -532,6 +792,8 @@ func (channel *Channel) GetSetting() dto.ChannelSettings {
err := json.Unmarshal([]byte(*channel.Setting), &setting)
if err != nil {
common.SysError("failed to unmarshal setting: " + err.Error())
+ channel.Setting = nil // 清空设置以避免后续错误
+ _ = channel.Save() // 保存修改
}
}
return setting
diff --git a/model/cache.go b/model/channel_cache.go
similarity index 63%
rename from model/cache.go
rename to model/channel_cache.go
index 3e5eb4c4476..b2451248979 100644
--- a/model/cache.go
+++ b/model/channel_cache.go
@@ -14,8 +14,8 @@ import (
"github.com/gin-gonic/gin"
)
-var group2model2channels map[string]map[string][]*Channel
-var channelsIDM map[int]*Channel
+var group2model2channels map[string]map[string][]int // enabled channel
+var channelsIDM map[int]*Channel // all channels include disabled
var channelSyncLock sync.RWMutex
func InitChannelCache() {
@@ -24,7 +24,7 @@ func InitChannelCache() {
}
newChannelId2channel := make(map[int]*Channel)
var channels []*Channel
- DB.Where("status = ?", common.ChannelStatusEnabled).Find(&channels)
+ DB.Find(&channels)
for _, channel := range channels {
newChannelId2channel[channel.Id] = channel
}
@@ -34,21 +34,22 @@ func InitChannelCache() {
for _, ability := range abilities {
groups[ability.Group] = true
}
- newGroup2model2channels := make(map[string]map[string][]*Channel)
- newChannelsIDM := make(map[int]*Channel)
+ newGroup2model2channels := make(map[string]map[string][]int)
for group := range groups {
- newGroup2model2channels[group] = make(map[string][]*Channel)
+ newGroup2model2channels[group] = make(map[string][]int)
}
for _, channel := range channels {
- newChannelsIDM[channel.Id] = channel
+ if channel.Status != common.ChannelStatusEnabled {
+ continue // skip disabled channels
+ }
groups := strings.Split(channel.Group, ",")
for _, group := range groups {
models := strings.Split(channel.Models, ",")
for _, model := range models {
if _, ok := newGroup2model2channels[group][model]; !ok {
- newGroup2model2channels[group][model] = make([]*Channel, 0)
+ newGroup2model2channels[group][model] = make([]int, 0)
}
- newGroup2model2channels[group][model] = append(newGroup2model2channels[group][model], channel)
+ newGroup2model2channels[group][model] = append(newGroup2model2channels[group][model], channel.Id)
}
}
}
@@ -57,7 +58,7 @@ func InitChannelCache() {
for group, model2channels := range newGroup2model2channels {
for model, channels := range model2channels {
sort.Slice(channels, func(i, j int) bool {
- return channels[i].GetPriority() > channels[j].GetPriority()
+ return newChannelId2channel[channels[i]].GetPriority() > newChannelId2channel[channels[j]].GetPriority()
})
newGroup2model2channels[group][model] = channels
}
@@ -65,7 +66,7 @@ func InitChannelCache() {
channelSyncLock.Lock()
group2model2channels = newGroup2model2channels
- channelsIDM = newChannelsIDM
+ channelsIDM = newChannelId2channel
channelSyncLock.Unlock()
common.SysLog("channels synced from database")
}
@@ -128,16 +129,27 @@ func getRandomSatisfiedChannel(group string, model string, retry int) (*Channel,
}
channelSyncLock.RLock()
+ defer channelSyncLock.RUnlock()
channels := group2model2channels[group][model]
- channelSyncLock.RUnlock()
if len(channels) == 0 {
return nil, errors.New("channel not found")
}
+ if len(channels) == 1 {
+ if channel, ok := channelsIDM[channels[0]]; ok {
+ return channel, nil
+ }
+ return nil, fmt.Errorf("数据库一致性错误,渠道# %d 不存在,请联系管理员修复", channels[0])
+ }
+
uniquePriorities := make(map[int]bool)
- for _, channel := range channels {
- uniquePriorities[int(channel.GetPriority())] = true
+ for _, channelId := range channels {
+ if channel, ok := channelsIDM[channelId]; ok {
+ uniquePriorities[int(channel.GetPriority())] = true
+ } else {
+ return nil, fmt.Errorf("数据库一致性错误,渠道# %d 不存在,请联系管理员修复", channelId)
+ }
}
var sortedUniquePriorities []int
for priority := range uniquePriorities {
@@ -152,9 +164,13 @@ func getRandomSatisfiedChannel(group string, model string, retry int) (*Channel,
// get the priority for the given retry number
var targetChannels []*Channel
- for _, channel := range channels {
- if channel.GetPriority() == targetPriority {
- targetChannels = append(targetChannels, channel)
+ for _, channelId := range channels {
+ if channel, ok := channelsIDM[channelId]; ok {
+ if channel.GetPriority() == targetPriority {
+ targetChannels = append(targetChannels, channel)
+ }
+ } else {
+ return nil, fmt.Errorf("数据库一致性错误,渠道# %d 不存在,请联系管理员修复", channelId)
}
}
@@ -188,11 +204,35 @@ func CacheGetChannel(id int) (*Channel, error) {
c, ok := channelsIDM[id]
if !ok {
- return nil, errors.New(fmt.Sprintf("当前渠道# %d,已不存在", id))
+ return nil, fmt.Errorf("渠道# %d,已不存在", id)
+ }
+ if c.Status != common.ChannelStatusEnabled {
+ return nil, fmt.Errorf("渠道# %d,已被禁用", id)
}
return c, nil
}
+func CacheGetChannelInfo(id int) (*ChannelInfo, error) {
+ if !common.MemoryCacheEnabled {
+ channel, err := GetChannelById(id, true)
+ if err != nil {
+ return nil, err
+ }
+ return &channel.ChannelInfo, nil
+ }
+ channelSyncLock.RLock()
+ defer channelSyncLock.RUnlock()
+
+ c, ok := channelsIDM[id]
+ if !ok {
+ return nil, fmt.Errorf("渠道# %d,已不存在", id)
+ }
+ if c.Status != common.ChannelStatusEnabled {
+ return nil, fmt.Errorf("渠道# %d,已被禁用", id)
+ }
+ return &c.ChannelInfo, nil
+}
+
func CacheUpdateChannelStatus(id int, status int) {
if !common.MemoryCacheEnabled {
return
@@ -203,3 +243,20 @@ func CacheUpdateChannelStatus(id int, status int) {
channel.Status = status
}
}
+
+func CacheUpdateChannel(channel *Channel) {
+ if !common.MemoryCacheEnabled {
+ return
+ }
+ channelSyncLock.Lock()
+ defer channelSyncLock.Unlock()
+ if channel == nil {
+ return
+ }
+
+ println("CacheUpdateChannel:", channel.Id, channel.Name, channel.Status, channel.ChannelInfo.MultiKeyPollingIndex)
+
+ println("before:", channelsIDM[channel.Id].ChannelInfo.MultiKeyPollingIndex)
+ channelsIDM[channel.Id] = channel
+ println("after :", channelsIDM[channel.Id].ChannelInfo.MultiKeyPollingIndex)
+}
diff --git a/model/log.go b/model/log.go
index e2d1ee5aa58..2070cd6f308 100644
--- a/model/log.go
+++ b/model/log.go
@@ -27,7 +27,7 @@ type Log struct {
PromptTokens int `json:"prompt_tokens" gorm:"default:0"`
CompletionTokens int `json:"completion_tokens" gorm:"default:0"`
UseTime int `json:"use_time" gorm:"default:0"`
- IsStream bool `json:"is_stream" gorm:"default:false"`
+ IsStream bool `json:"is_stream"`
ChannelId int `json:"channel" gorm:"index"`
ChannelName string `json:"channel_name" gorm:"->"`
TokenId int `json:"token_id" gorm:"default:0;index"`
@@ -49,7 +49,7 @@ func formatUserLogs(logs []*Log) {
for i := range logs {
logs[i].ChannelName = ""
var otherMap map[string]interface{}
- otherMap = common.StrToMap(logs[i].Other)
+ otherMap, _ = common.StrToMap(logs[i].Other)
if otherMap != nil {
// delete admin
delete(otherMap, "admin_info")
diff --git a/model/main.go b/model/main.go
index d46a21cf64b..013beacdab0 100644
--- a/model/main.go
+++ b/model/main.go
@@ -57,7 +57,7 @@ func initCol() {
}
}
// log sql type and database type
- common.SysLog("Using Log SQL Type: " + common.LogSqlType)
+ //common.SysLog("Using Log SQL Type: " + common.LogSqlType)
}
var DB *gorm.DB
@@ -225,12 +225,6 @@ func InitLogDB() (err error) {
if !common.IsMasterNode {
return nil
}
- //if common.UsingMySQL {
- // _, _ = sqlDB.Exec("DROP INDEX idx_channels_key ON channels;") // TODO: delete this line when most users have upgraded
- // _, _ = sqlDB.Exec("ALTER TABLE midjourneys MODIFY action VARCHAR(40);") // TODO: delete this line when most users have upgraded
- // _, _ = sqlDB.Exec("ALTER TABLE midjourneys MODIFY progress VARCHAR(30);") // TODO: delete this line when most users have upgraded
- // _, _ = sqlDB.Exec("ALTER TABLE midjourneys MODIFY status VARCHAR(20);") // TODO: delete this line when most users have upgraded
- //}
common.SysLog("database migration started")
err = migrateLOGDB()
return err
@@ -266,7 +260,6 @@ func migrateDB() error {
func migrateDBFast() error {
var wg sync.WaitGroup
- errChan := make(chan error, 12) // Buffer size matches number of migrations
migrations := []struct {
model interface{}
@@ -285,6 +278,8 @@ func migrateDBFast() error {
{&Task{}, "Task"},
{&Setup{}, "Setup"},
}
+ // 动态计算migration数量,确保errChan缓冲区足够大
+ errChan := make(chan error, len(migrations))
for _, m := range migrations {
wg.Add(1)
diff --git a/model/option.go b/model/option.go
index ea72e5ee8d9..05b99b41aff 100644
--- a/model/option.go
+++ b/model/option.go
@@ -74,7 +74,13 @@ func InitOptionMap() {
common.OptionMap["EpayId"] = ""
common.OptionMap["EpayKey"] = ""
common.OptionMap["Price"] = strconv.FormatFloat(setting.Price, 'f', -1, 64)
+ common.OptionMap["USDExchangeRate"] = strconv.FormatFloat(setting.USDExchangeRate, 'f', -1, 64)
common.OptionMap["MinTopUp"] = strconv.Itoa(setting.MinTopUp)
+ common.OptionMap["StripeMinTopUp"] = strconv.Itoa(setting.StripeMinTopUp)
+ common.OptionMap["StripeApiSecret"] = setting.StripeApiSecret
+ common.OptionMap["StripeWebhookSecret"] = setting.StripeWebhookSecret
+ common.OptionMap["StripePriceId"] = setting.StripePriceId
+ common.OptionMap["StripeUnitPrice"] = strconv.FormatFloat(setting.StripeUnitPrice, 'f', -1, 64)
common.OptionMap["TopupGroupRatio"] = common.TopupGroupRatio2JSONString()
common.OptionMap["Chats"] = setting.Chats2JsonString()
common.OptionMap["AutoGroups"] = setting.AutoGroups2JsonString()
@@ -306,8 +312,20 @@ func updateOptionMap(key string, value string) (err error) {
setting.EpayKey = value
case "Price":
setting.Price, _ = strconv.ParseFloat(value, 64)
+ case "USDExchangeRate":
+ setting.USDExchangeRate, _ = strconv.ParseFloat(value, 64)
case "MinTopUp":
setting.MinTopUp, _ = strconv.Atoi(value)
+ case "StripeApiSecret":
+ setting.StripeApiSecret = value
+ case "StripeWebhookSecret":
+ setting.StripeWebhookSecret = value
+ case "StripePriceId":
+ setting.StripePriceId = value
+ case "StripeUnitPrice":
+ setting.StripeUnitPrice, _ = strconv.ParseFloat(value, 64)
+ case "StripeMinTopUp":
+ setting.StripeMinTopUp, _ = strconv.Atoi(value)
case "TopupGroupRatio":
err = common.UpdateTopupGroupRatioByJSONString(value)
case "GitHubClientId":
diff --git a/model/pricing.go b/model/pricing.go
index 0c0216f135a..a280b5246a1 100644
--- a/model/pricing.go
+++ b/model/pricing.go
@@ -116,7 +116,7 @@ func updatePricing() {
pricing.ModelPrice = modelPrice
pricing.QuotaType = 1
} else {
- modelRatio, _ := ratio_setting.GetModelRatio(model)
+ modelRatio, _, _ := ratio_setting.GetModelRatio(model)
pricing.ModelRatio = modelRatio
pricing.CompletionRatio = ratio_setting.GetCompletionRatio(model)
pricing.QuotaType = 0
diff --git a/model/token.go b/model/token.go
index 7e68f1857ef..e85a445ec76 100644
--- a/model/token.go
+++ b/model/token.go
@@ -20,8 +20,8 @@ type Token struct {
AccessedTime int64 `json:"accessed_time" gorm:"bigint"`
ExpiredTime int64 `json:"expired_time" gorm:"bigint;default:-1"` // -1 means never expired
RemainQuota int `json:"remain_quota" gorm:"default:0"`
- UnlimitedQuota bool `json:"unlimited_quota" gorm:"default:false"`
- ModelLimitsEnabled bool `json:"model_limits_enabled" gorm:"default:false"`
+ UnlimitedQuota bool `json:"unlimited_quota"`
+ ModelLimitsEnabled bool `json:"model_limits_enabled"`
ModelLimits string `json:"model_limits" gorm:"type:varchar(1024);default:''"`
AllowIps *string `json:"allow_ips" gorm:"default:''"`
UsedQuota int `json:"used_quota" gorm:"default:0"` // used quota
diff --git a/model/topup.go b/model/topup.go
index 507b85186ce..c34c0ce62c7 100644
--- a/model/topup.go
+++ b/model/topup.go
@@ -1,13 +1,22 @@
package model
+import (
+ "errors"
+ "fmt"
+ "one-api/common"
+
+ "gorm.io/gorm"
+)
+
type TopUp struct {
- Id int `json:"id"`
- UserId int `json:"user_id" gorm:"index"`
- Amount int64 `json:"amount"`
- Money float64 `json:"money"`
- TradeNo string `json:"trade_no"`
- CreateTime int64 `json:"create_time"`
- Status string `json:"status"`
+ Id int `json:"id"`
+ UserId int `json:"user_id" gorm:"index"`
+ Amount int64 `json:"amount"`
+ Money float64 `json:"money"`
+ TradeNo string `json:"trade_no" gorm:"unique;type:varchar(255);index"`
+ CreateTime int64 `json:"create_time"`
+ CompleteTime int64 `json:"complete_time"`
+ Status string `json:"status"`
}
func (topUp *TopUp) Insert() error {
@@ -41,3 +50,51 @@ func GetTopUpByTradeNo(tradeNo string) *TopUp {
}
return topUp
}
+
+func Recharge(referenceId string, customerId string) (err error) {
+ if referenceId == "" {
+ return errors.New("未提供支付单号")
+ }
+
+ var quota float64
+ topUp := &TopUp{}
+
+ refCol := "`trade_no`"
+ if common.UsingPostgreSQL {
+ refCol = `"trade_no"`
+ }
+
+ err = DB.Transaction(func(tx *gorm.DB) error {
+ err := tx.Set("gorm:query_option", "FOR UPDATE").Where(refCol+" = ?", referenceId).First(topUp).Error
+ if err != nil {
+ return errors.New("充值订单不存在")
+ }
+
+ if topUp.Status != common.TopUpStatusPending {
+ return errors.New("充值订单状态错误")
+ }
+
+ topUp.CompleteTime = common.GetTimestamp()
+ topUp.Status = common.TopUpStatusSuccess
+ err = tx.Save(topUp).Error
+ if err != nil {
+ return err
+ }
+
+ quota = topUp.Money * common.QuotaPerUnit
+ err = tx.Model(&User{}).Where("id = ?", topUp.UserId).Updates(map[string]interface{}{"stripe_customer": customerId, "quota": gorm.Expr("quota + ?", quota)}).Error
+ if err != nil {
+ return err
+ }
+
+ return nil
+ })
+
+ if err != nil {
+ return errors.New("充值失败," + err.Error())
+ }
+
+ RecordLog(topUp.UserId, LogTypeTopup, fmt.Sprintf("使用在线充值成功,充值金额: %v,支付金额:%d", common.FormatQuota(int(quota)), topUp.Amount))
+
+ return nil
+}
diff --git a/model/user.go b/model/user.go
index 6bb5a8673da..6021f495c00 100644
--- a/model/user.go
+++ b/model/user.go
@@ -43,6 +43,7 @@ type User struct {
LinuxDOId string `json:"linux_do_id" gorm:"column:linux_do_id;index"`
Setting string `json:"setting" gorm:"type:text;column:setting"`
Remark string `json:"remark,omitempty" gorm:"type:varchar(255)" validate:"max=255"`
+ StripeCustomer string `json:"stripe_customer" gorm:"type:varchar(64);column:stripe_customer;index"`
}
func (user *User) ToBaseUser() *UserBase {
diff --git a/model/user_cache.go b/model/user_cache.go
index a62d97735ba..a631457c267 100644
--- a/model/user_cache.go
+++ b/model/user_cache.go
@@ -1,7 +1,6 @@
package model
import (
- "encoding/json"
"fmt"
"one-api/common"
"one-api/constant"
@@ -36,7 +35,7 @@ func (user *UserBase) WriteContext(c *gin.Context) {
func (user *UserBase) GetSetting() dto.UserSetting {
setting := dto.UserSetting{}
if user.Setting != "" {
- err := json.Unmarshal([]byte(user.Setting), &setting)
+ err := common.Unmarshal([]byte(user.Setting), &setting)
if err != nil {
common.SysError("failed to unmarshal setting: " + err.Error())
}
diff --git a/relay/audio_handler.go b/relay/audio_handler.go
index c1ce1a0224b..f39dbd8235a 100644
--- a/relay/audio_handler.go
+++ b/relay/audio_handler.go
@@ -3,7 +3,6 @@ package relay
import (
"errors"
"fmt"
- "github.com/gin-gonic/gin"
"net/http"
"one-api/common"
"one-api/dto"
@@ -12,7 +11,10 @@ import (
"one-api/relay/helper"
"one-api/service"
"one-api/setting"
+ "one-api/types"
"strings"
+
+ "github.com/gin-gonic/gin"
)
func getAndValidAudioRequest(c *gin.Context, info *relaycommon.RelayInfo) (*dto.AudioRequest, error) {
@@ -54,13 +56,13 @@ func getAndValidAudioRequest(c *gin.Context, info *relaycommon.RelayInfo) (*dto.
return audioRequest, nil
}
-func AudioHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode) {
+func AudioHelper(c *gin.Context) (newAPIError *types.NewAPIError) {
relayInfo := relaycommon.GenRelayInfoOpenAIAudio(c)
audioRequest, err := getAndValidAudioRequest(c, relayInfo)
if err != nil {
common.LogError(c, fmt.Sprintf("getAndValidAudioRequest failed: %s", err.Error()))
- return service.OpenAIErrorWrapper(err, "invalid_audio_request", http.StatusBadRequest)
+ return types.NewError(err, types.ErrorCodeInvalidRequest)
}
promptTokens := 0
@@ -73,7 +75,7 @@ func AudioHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode) {
priceData, err := helper.ModelPriceHelper(c, relayInfo, preConsumedTokens, 0)
if err != nil {
- return service.OpenAIErrorWrapperLocal(err, "model_price_error", http.StatusInternalServerError)
+ return types.NewError(err, types.ErrorCodeModelPriceError)
}
preConsumedQuota, userQuota, openaiErr := preConsumeQuota(c, priceData.ShouldPreConsumedQuota, relayInfo)
@@ -88,23 +90,23 @@ func AudioHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode) {
err = helper.ModelMappedHelper(c, relayInfo, audioRequest)
if err != nil {
- return service.OpenAIErrorWrapperLocal(err, "model_mapped_error", http.StatusInternalServerError)
+ return types.NewError(err, types.ErrorCodeChannelModelMappedError)
}
adaptor := GetAdaptor(relayInfo.ApiType)
if adaptor == nil {
- return service.OpenAIErrorWrapperLocal(fmt.Errorf("invalid api type: %d", relayInfo.ApiType), "invalid_api_type", http.StatusBadRequest)
+ return types.NewError(fmt.Errorf("invalid api type: %d", relayInfo.ApiType), types.ErrorCodeInvalidApiType)
}
adaptor.Init(relayInfo)
ioReader, err := adaptor.ConvertAudioRequest(c, relayInfo, *audioRequest)
if err != nil {
- return service.OpenAIErrorWrapperLocal(err, "convert_request_failed", http.StatusInternalServerError)
+ return types.NewError(err, types.ErrorCodeConvertRequestFailed)
}
resp, err := adaptor.DoRequest(c, relayInfo, ioReader)
if err != nil {
- return service.OpenAIErrorWrapper(err, "do_request_failed", http.StatusInternalServerError)
+ return types.NewError(err, types.ErrorCodeDoRequestFailed)
}
statusCodeMappingStr := c.GetString("status_code_mapping")
@@ -112,18 +114,18 @@ func AudioHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode) {
if resp != nil {
httpResp = resp.(*http.Response)
if httpResp.StatusCode != http.StatusOK {
- openaiErr = service.RelayErrorHandler(httpResp, false)
+ newAPIError = service.RelayErrorHandler(httpResp, false)
// reset status code 重置状态码
- service.ResetStatusCode(openaiErr, statusCodeMappingStr)
- return openaiErr
+ service.ResetStatusCode(newAPIError, statusCodeMappingStr)
+ return newAPIError
}
}
- usage, openaiErr := adaptor.DoResponse(c, httpResp, relayInfo)
- if openaiErr != nil {
+ usage, newAPIError := adaptor.DoResponse(c, httpResp, relayInfo)
+ if newAPIError != nil {
// reset status code 重置状态码
- service.ResetStatusCode(openaiErr, statusCodeMappingStr)
- return openaiErr
+ service.ResetStatusCode(newAPIError, statusCodeMappingStr)
+ return newAPIError
}
postConsumeQuota(c, relayInfo, usage.(*dto.Usage), preConsumedQuota, userQuota, priceData, "")
diff --git a/relay/channel/adapter.go b/relay/channel/adapter.go
index 2ff34e0176f..ab8836baa94 100644
--- a/relay/channel/adapter.go
+++ b/relay/channel/adapter.go
@@ -5,6 +5,7 @@ import (
"net/http"
"one-api/dto"
relaycommon "one-api/relay/common"
+ "one-api/types"
"github.com/gin-gonic/gin"
)
@@ -21,7 +22,7 @@ type Adaptor interface {
ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.ImageRequest) (any, error)
ConvertOpenAIResponsesRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.OpenAIResponsesRequest) (any, error)
DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error)
- DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *dto.OpenAIErrorWithStatusCode)
+ DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *types.NewAPIError)
GetModelList() []string
GetChannelName() string
ConvertClaudeRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.ClaudeRequest) (any, error)
diff --git a/relay/channel/ali/adaptor.go b/relay/channel/ali/adaptor.go
index 63525cc4487..d941a1bc71f 100644
--- a/relay/channel/ali/adaptor.go
+++ b/relay/channel/ali/adaptor.go
@@ -10,6 +10,7 @@ import (
"one-api/relay/channel/openai"
relaycommon "one-api/relay/common"
"one-api/relay/constant"
+ "one-api/types"
"github.com/gin-gonic/gin"
)
@@ -99,7 +100,7 @@ func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, request
return channel.DoApiRequest(a, c, info, requestBody)
}
-func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *dto.OpenAIErrorWithStatusCode) {
+func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *types.NewAPIError) {
switch info.RelayMode {
case constant.RelayModeImagesGenerations:
err, usage = aliImageHandler(c, resp, info)
@@ -109,9 +110,9 @@ func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycom
err, usage = RerankHandler(c, resp, info)
default:
if info.IsStream {
- err, usage = openai.OaiStreamHandler(c, resp, info)
+ usage, err = openai.OaiStreamHandler(c, info, resp)
} else {
- err, usage = openai.OpenaiHandler(c, resp, info)
+ usage, err = openai.OpenaiHandler(c, info, resp)
}
}
return
diff --git a/relay/channel/ali/image.go b/relay/channel/ali/image.go
index c84c7885758..0d430c629ad 100644
--- a/relay/channel/ali/image.go
+++ b/relay/channel/ali/image.go
@@ -4,15 +4,17 @@ import (
"encoding/json"
"errors"
"fmt"
- "github.com/gin-gonic/gin"
"io"
"net/http"
"one-api/common"
"one-api/dto"
relaycommon "one-api/relay/common"
"one-api/service"
+ "one-api/types"
"strings"
"time"
+
+ "github.com/gin-gonic/gin"
)
func oaiImage2Ali(request dto.ImageRequest) *AliImageRequest {
@@ -124,49 +126,46 @@ func responseAli2OpenAIImage(c *gin.Context, response *AliResponse, info *relayc
return &imageResponse
}
-func aliImageHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
+func aliImageHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (*types.NewAPIError, *dto.Usage) {
responseFormat := c.GetString("response_format")
var aliTaskResponse AliResponse
responseBody, err := io.ReadAll(resp.Body)
if err != nil {
- return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
+ return types.NewError(err, types.ErrorCodeReadResponseBodyFailed), nil
}
common.CloseResponseBodyGracefully(resp)
err = json.Unmarshal(responseBody, &aliTaskResponse)
if err != nil {
- return service.OpenAIErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
+ return types.NewError(err, types.ErrorCodeBadResponseBody), nil
}
if aliTaskResponse.Message != "" {
common.LogError(c, "ali_async_task_failed: "+aliTaskResponse.Message)
- return service.OpenAIErrorWrapper(errors.New(aliTaskResponse.Message), "ali_async_task_failed", http.StatusInternalServerError), nil
+ return types.NewError(errors.New(aliTaskResponse.Message), types.ErrorCodeBadResponse), nil
}
aliResponse, _, err := asyncTaskWait(info, aliTaskResponse.Output.TaskId)
if err != nil {
- return service.OpenAIErrorWrapper(err, "ali_async_task_wait_failed", http.StatusInternalServerError), nil
+ return types.NewError(err, types.ErrorCodeBadResponse), nil
}
if aliResponse.Output.TaskStatus != "SUCCEEDED" {
- return &dto.OpenAIErrorWithStatusCode{
- Error: dto.OpenAIError{
- Message: aliResponse.Output.Message,
- Type: "ali_error",
- Param: "",
- Code: aliResponse.Output.Code,
- },
- StatusCode: resp.StatusCode,
- }, nil
+ return types.WithOpenAIError(types.OpenAIError{
+ Message: aliResponse.Output.Message,
+ Type: "ali_error",
+ Param: "",
+ Code: aliResponse.Output.Code,
+ }, resp.StatusCode), nil
}
fullTextResponse := responseAli2OpenAIImage(c, aliResponse, info, responseFormat)
jsonResponse, err := json.Marshal(fullTextResponse)
if err != nil {
- return service.OpenAIErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil
+ return types.NewError(err, types.ErrorCodeBadResponseBody), nil
}
c.Writer.Header().Set("Content-Type", "application/json")
c.Writer.WriteHeader(resp.StatusCode)
- _, err = c.Writer.Write(jsonResponse)
- return nil, nil
+ c.Writer.Write(jsonResponse)
+ return nil, &dto.Usage{}
}
diff --git a/relay/channel/ali/rerank.go b/relay/channel/ali/rerank.go
index ebfe26deb10..59cb0a11a81 100644
--- a/relay/channel/ali/rerank.go
+++ b/relay/channel/ali/rerank.go
@@ -7,7 +7,7 @@ import (
"one-api/common"
"one-api/dto"
relaycommon "one-api/relay/common"
- "one-api/service"
+ "one-api/types"
"github.com/gin-gonic/gin"
)
@@ -31,29 +31,26 @@ func ConvertRerankRequest(request dto.RerankRequest) *AliRerankRequest {
}
}
-func RerankHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
+func RerankHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (*types.NewAPIError, *dto.Usage) {
responseBody, err := io.ReadAll(resp.Body)
if err != nil {
- return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
+ return types.NewError(err, types.ErrorCodeReadResponseBodyFailed), nil
}
common.CloseResponseBodyGracefully(resp)
var aliResponse AliRerankResponse
err = json.Unmarshal(responseBody, &aliResponse)
if err != nil {
- return service.OpenAIErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
+ return types.NewError(err, types.ErrorCodeBadResponseBody), nil
}
if aliResponse.Code != "" {
- return &dto.OpenAIErrorWithStatusCode{
- Error: dto.OpenAIError{
- Message: aliResponse.Message,
- Type: aliResponse.Code,
- Param: aliResponse.RequestId,
- Code: aliResponse.Code,
- },
- StatusCode: resp.StatusCode,
- }, nil
+ return types.WithOpenAIError(types.OpenAIError{
+ Message: aliResponse.Message,
+ Type: aliResponse.Code,
+ Param: aliResponse.RequestId,
+ Code: aliResponse.Code,
+ }, resp.StatusCode), nil
}
usage := dto.Usage{
@@ -68,14 +65,10 @@ func RerankHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayI
jsonResponse, err := json.Marshal(rerankResponse)
if err != nil {
- return service.OpenAIErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil
+ return types.NewError(err, types.ErrorCodeBadResponseBody), nil
}
c.Writer.Header().Set("Content-Type", "application/json")
c.Writer.WriteHeader(resp.StatusCode)
- _, err = c.Writer.Write(jsonResponse)
- if err != nil {
- return service.OpenAIErrorWrapper(err, "write_response_body_failed", http.StatusInternalServerError), nil
- }
-
+ c.Writer.Write(jsonResponse)
return nil, &usage
}
diff --git a/relay/channel/ali/text.go b/relay/channel/ali/text.go
index 149c9b4befc..6d90fa71391 100644
--- a/relay/channel/ali/text.go
+++ b/relay/channel/ali/text.go
@@ -8,9 +8,10 @@ import (
"one-api/common"
"one-api/dto"
"one-api/relay/helper"
- "one-api/service"
"strings"
+ "one-api/types"
+
"github.com/gin-gonic/gin"
)
@@ -38,11 +39,11 @@ func embeddingRequestOpenAI2Ali(request dto.EmbeddingRequest) *AliEmbeddingReque
}
}
-func aliEmbeddingHandler(c *gin.Context, resp *http.Response) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
- var fullTextResponse dto.OpenAIEmbeddingResponse
+func aliEmbeddingHandler(c *gin.Context, resp *http.Response) (*types.NewAPIError, *dto.Usage) {
+ var fullTextResponse dto.FlexibleEmbeddingResponse
err := json.NewDecoder(resp.Body).Decode(&fullTextResponse)
if err != nil {
- return service.OpenAIErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
+ return types.NewError(err, types.ErrorCodeBadResponseBody), nil
}
common.CloseResponseBodyGracefully(resp)
@@ -53,11 +54,11 @@ func aliEmbeddingHandler(c *gin.Context, resp *http.Response) (*dto.OpenAIErrorW
}
jsonResponse, err := json.Marshal(fullTextResponse)
if err != nil {
- return service.OpenAIErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil
+ return types.NewError(err, types.ErrorCodeBadResponseBody), nil
}
c.Writer.Header().Set("Content-Type", "application/json")
c.Writer.WriteHeader(resp.StatusCode)
- _, err = c.Writer.Write(jsonResponse)
+ c.Writer.Write(jsonResponse)
return nil, &fullTextResponse.Usage
}
@@ -119,7 +120,7 @@ func streamResponseAli2OpenAI(aliResponse *AliResponse) *dto.ChatCompletionsStre
return &response
}
-func aliStreamHandler(c *gin.Context, resp *http.Response) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
+func aliStreamHandler(c *gin.Context, resp *http.Response) (*types.NewAPIError, *dto.Usage) {
var usage dto.Usage
scanner := bufio.NewScanner(resp.Body)
scanner.Split(bufio.ScanLines)
@@ -174,32 +175,29 @@ func aliStreamHandler(c *gin.Context, resp *http.Response) (*dto.OpenAIErrorWith
return nil, &usage
}
-func aliHandler(c *gin.Context, resp *http.Response) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
+func aliHandler(c *gin.Context, resp *http.Response) (*types.NewAPIError, *dto.Usage) {
var aliResponse AliResponse
responseBody, err := io.ReadAll(resp.Body)
if err != nil {
- return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
+ return types.NewError(err, types.ErrorCodeReadResponseBodyFailed), nil
}
common.CloseResponseBodyGracefully(resp)
err = json.Unmarshal(responseBody, &aliResponse)
if err != nil {
- return service.OpenAIErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
+ return types.NewError(err, types.ErrorCodeBadResponseBody), nil
}
if aliResponse.Code != "" {
- return &dto.OpenAIErrorWithStatusCode{
- Error: dto.OpenAIError{
- Message: aliResponse.Message,
- Type: aliResponse.Code,
- Param: aliResponse.RequestId,
- Code: aliResponse.Code,
- },
- StatusCode: resp.StatusCode,
- }, nil
+ return types.WithOpenAIError(types.OpenAIError{
+ Message: aliResponse.Message,
+ Type: "ali_error",
+ Param: aliResponse.RequestId,
+ Code: aliResponse.Code,
+ }, resp.StatusCode), nil
}
fullTextResponse := responseAli2OpenAI(&aliResponse)
- jsonResponse, err := json.Marshal(fullTextResponse)
+ jsonResponse, err := common.Marshal(fullTextResponse)
if err != nil {
- return service.OpenAIErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil
+ return types.NewError(err, types.ErrorCodeBadResponseBody), nil
}
c.Writer.Header().Set("Content-Type", "application/json")
c.Writer.WriteHeader(resp.StatusCode)
diff --git a/relay/channel/api_request.go b/relay/channel/api_request.go
index 978872669be..ff7c63fab46 100644
--- a/relay/channel/api_request.go
+++ b/relay/channel/api_request.go
@@ -203,6 +203,9 @@ func sendPingData(c *gin.Context, mutex *sync.Mutex) error {
}
}
+func DoRequest(c *gin.Context, req *http.Request, info *common.RelayInfo) (*http.Response, error) {
+ return doRequest(c, req, info)
+}
func doRequest(c *gin.Context, req *http.Request, info *common.RelayInfo) (*http.Response, error) {
var client *http.Client
var err error
diff --git a/relay/channel/aws/adaptor.go b/relay/channel/aws/adaptor.go
index 9c879399f56..d3354f00d2b 100644
--- a/relay/channel/aws/adaptor.go
+++ b/relay/channel/aws/adaptor.go
@@ -8,6 +8,7 @@ import (
"one-api/relay/channel/claude"
relaycommon "one-api/relay/common"
"one-api/setting/model_setting"
+ "one-api/types"
"github.com/gin-gonic/gin"
)
@@ -84,7 +85,7 @@ func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, request
return nil, nil
}
-func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *dto.OpenAIErrorWithStatusCode) {
+func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *types.NewAPIError) {
if info.IsStream {
err, usage = awsStreamHandler(c, resp, info, a.RequestMode)
} else {
diff --git a/relay/channel/aws/relay-aws.go b/relay/channel/aws/relay-aws.go
index 3c9542c6056..0df19e07fec 100644
--- a/relay/channel/aws/relay-aws.go
+++ b/relay/channel/aws/relay-aws.go
@@ -3,19 +3,22 @@ package aws
import (
"encoding/json"
"fmt"
- "github.com/gin-gonic/gin"
- "github.com/pkg/errors"
"net/http"
"one-api/common"
"one-api/dto"
"one-api/relay/channel/claude"
relaycommon "one-api/relay/common"
+ "one-api/relay/helper"
+ "one-api/types"
"strings"
+ "github.com/gin-gonic/gin"
+ "github.com/pkg/errors"
+
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/bedrockruntime"
- "github.com/aws/aws-sdk-go-v2/service/bedrockruntime/types"
+ bedrockruntimeTypes "github.com/aws/aws-sdk-go-v2/service/bedrockruntime/types"
)
func newAwsClient(c *gin.Context, info *relaycommon.RelayInfo) (*bedrockruntime.Client, error) {
@@ -65,24 +68,21 @@ func awsModelCrossRegion(awsModelId, awsRegionPrefix string) string {
return modelPrefix + "." + awsModelId
}
-func awsModelID(requestModel string) (string, error) {
+func awsModelID(requestModel string) string {
if awsModelID, ok := awsModelIDMap[requestModel]; ok {
- return awsModelID, nil
+ return awsModelID
}
- return requestModel, nil
+ return requestModel
}
-func awsHandler(c *gin.Context, info *relaycommon.RelayInfo, requestMode int) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
+func awsHandler(c *gin.Context, info *relaycommon.RelayInfo, requestMode int) (*types.NewAPIError, *dto.Usage) {
awsCli, err := newAwsClient(c, info)
if err != nil {
- return wrapErr(errors.Wrap(err, "newAwsClient")), nil
+ return types.NewError(err, types.ErrorCodeChannelAwsClientError), nil
}
- awsModelId, err := awsModelID(c.GetString("request_model"))
- if err != nil {
- return wrapErr(errors.Wrap(err, "awsModelID")), nil
- }
+ awsModelId := awsModelID(c.GetString("request_model"))
awsRegionPrefix := awsRegionPrefix(awsCli.Options().Region)
canCrossRegion := awsModelCanCrossRegion(awsModelId, awsRegionPrefix)
@@ -98,42 +98,42 @@ func awsHandler(c *gin.Context, info *relaycommon.RelayInfo, requestMode int) (*
claudeReq_, ok := c.Get("converted_request")
if !ok {
- return wrapErr(errors.New("request not found")), nil
+ return types.NewError(errors.New("aws claude request not found"), types.ErrorCodeInvalidRequest), nil
}
claudeReq := claudeReq_.(*dto.ClaudeRequest)
awsClaudeReq := copyRequest(claudeReq)
awsReq.Body, err = json.Marshal(awsClaudeReq)
if err != nil {
- return wrapErr(errors.Wrap(err, "marshal request")), nil
+ return types.NewError(errors.Wrap(err, "marshal request"), types.ErrorCodeBadResponseBody), nil
}
awsResp, err := awsCli.InvokeModel(c.Request.Context(), awsReq)
if err != nil {
- return wrapErr(errors.Wrap(err, "InvokeModel")), nil
+ return types.NewError(errors.Wrap(err, "InvokeModel"), types.ErrorCodeChannelAwsClientError), nil
}
claudeInfo := &claude.ClaudeResponseInfo{
- ResponseId: fmt.Sprintf("chatcmpl-%s", common.GetUUID()),
+ ResponseId: helper.GetResponseID(c),
Created: common.GetTimestamp(),
Model: info.UpstreamModelName,
ResponseText: strings.Builder{},
Usage: &dto.Usage{},
}
- claude.HandleClaudeResponseData(c, info, claudeInfo, awsResp.Body, RequestModeMessage)
+ handlerErr := claude.HandleClaudeResponseData(c, info, claudeInfo, awsResp.Body, RequestModeMessage)
+ if handlerErr != nil {
+ return handlerErr, nil
+ }
return nil, claudeInfo.Usage
}
-func awsStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo, requestMode int) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
+func awsStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo, requestMode int) (*types.NewAPIError, *dto.Usage) {
awsCli, err := newAwsClient(c, info)
if err != nil {
- return wrapErr(errors.Wrap(err, "newAwsClient")), nil
+ return types.NewError(err, types.ErrorCodeChannelAwsClientError), nil
}
- awsModelId, err := awsModelID(c.GetString("request_model"))
- if err != nil {
- return wrapErr(errors.Wrap(err, "awsModelID")), nil
- }
+ awsModelId := awsModelID(c.GetString("request_model"))
awsRegionPrefix := awsRegionPrefix(awsCli.Options().Region)
canCrossRegion := awsModelCanCrossRegion(awsModelId, awsRegionPrefix)
@@ -149,25 +149,25 @@ func awsStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.Rel
claudeReq_, ok := c.Get("converted_request")
if !ok {
- return wrapErr(errors.New("request not found")), nil
+ return types.NewError(errors.New("aws claude request not found"), types.ErrorCodeInvalidRequest), nil
}
claudeReq := claudeReq_.(*dto.ClaudeRequest)
awsClaudeReq := copyRequest(claudeReq)
awsReq.Body, err = json.Marshal(awsClaudeReq)
if err != nil {
- return wrapErr(errors.Wrap(err, "marshal request")), nil
+ return types.NewError(errors.Wrap(err, "marshal request"), types.ErrorCodeBadResponseBody), nil
}
awsResp, err := awsCli.InvokeModelWithResponseStream(c.Request.Context(), awsReq)
if err != nil {
- return wrapErr(errors.Wrap(err, "InvokeModelWithResponseStream")), nil
+ return types.NewError(errors.Wrap(err, "InvokeModelWithResponseStream"), types.ErrorCodeChannelAwsClientError), nil
}
stream := awsResp.GetStream()
defer stream.Close()
claudeInfo := &claude.ClaudeResponseInfo{
- ResponseId: fmt.Sprintf("chatcmpl-%s", common.GetUUID()),
+ ResponseId: helper.GetResponseID(c),
Created: common.GetTimestamp(),
Model: info.UpstreamModelName,
ResponseText: strings.Builder{},
@@ -176,18 +176,18 @@ func awsStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.Rel
for event := range stream.Events() {
switch v := event.(type) {
- case *types.ResponseStreamMemberChunk:
+ case *bedrockruntimeTypes.ResponseStreamMemberChunk:
info.SetFirstResponseTime()
respErr := claude.HandleStreamResponseData(c, info, claudeInfo, string(v.Value.Bytes), RequestModeMessage)
if respErr != nil {
return respErr, nil
}
- case *types.UnknownUnionMember:
+ case *bedrockruntimeTypes.UnknownUnionMember:
fmt.Println("unknown tag:", v.Tag)
- return wrapErr(errors.New("unknown response type")), nil
+ return types.NewError(errors.New("unknown response type"), types.ErrorCodeInvalidRequest), nil
default:
fmt.Println("union is nil or unknown type")
- return wrapErr(errors.New("nil or unknown response type")), nil
+ return types.NewError(errors.New("nil or unknown response type"), types.ErrorCodeInvalidRequest), nil
}
}
diff --git a/relay/channel/baidu/adaptor.go b/relay/channel/baidu/adaptor.go
index 396c31abcfc..22443354b2a 100644
--- a/relay/channel/baidu/adaptor.go
+++ b/relay/channel/baidu/adaptor.go
@@ -9,6 +9,7 @@ import (
"one-api/relay/channel"
relaycommon "one-api/relay/common"
"one-api/relay/constant"
+ "one-api/types"
"strings"
"github.com/gin-gonic/gin"
@@ -140,15 +141,15 @@ func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, request
return channel.DoApiRequest(a, c, info, requestBody)
}
-func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *dto.OpenAIErrorWithStatusCode) {
+func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *types.NewAPIError) {
if info.IsStream {
- err, usage = baiduStreamHandler(c, resp)
+ err, usage = baiduStreamHandler(c, info, resp)
} else {
switch info.RelayMode {
case constant.RelayModeEmbeddings:
- err, usage = baiduEmbeddingHandler(c, resp)
+ err, usage = baiduEmbeddingHandler(c, info, resp)
default:
- err, usage = baiduHandler(c, resp)
+ err, usage = baiduHandler(c, info, resp)
}
}
return
diff --git a/relay/channel/baidu/relay-baidu.go b/relay/channel/baidu/relay-baidu.go
index 11492fe355c..06b48c20505 100644
--- a/relay/channel/baidu/relay-baidu.go
+++ b/relay/channel/baidu/relay-baidu.go
@@ -1,21 +1,23 @@
package baidu
import (
- "bufio"
"encoding/json"
"errors"
"fmt"
- "github.com/gin-gonic/gin"
"io"
"net/http"
"one-api/common"
"one-api/constant"
"one-api/dto"
+ relaycommon "one-api/relay/common"
"one-api/relay/helper"
"one-api/service"
+ "one-api/types"
"strings"
"sync"
"time"
+
+ "github.com/gin-gonic/gin"
)
// https://cloud.baidu.com/doc/WENXINWORKSHOP/s/flfmc9do2
@@ -110,92 +112,49 @@ func embeddingResponseBaidu2OpenAI(response *BaiduEmbeddingResponse) *dto.OpenAI
return &openAIEmbeddingResponse
}
-func baiduStreamHandler(c *gin.Context, resp *http.Response) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
- var usage dto.Usage
- scanner := bufio.NewScanner(resp.Body)
- scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
- if atEOF && len(data) == 0 {
- return 0, nil, nil
- }
- if i := strings.Index(string(data), "\n"); i >= 0 {
- return i + 1, data[0:i], nil
- }
- if atEOF {
- return len(data), data, nil
+func baiduStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Response) (*types.NewAPIError, *dto.Usage) {
+ usage := &dto.Usage{}
+ helper.StreamScannerHandler(c, resp, info, func(data string) bool {
+ var baiduResponse BaiduChatStreamResponse
+ err := common.Unmarshal([]byte(data), &baiduResponse)
+ if err != nil {
+ common.SysError("error unmarshalling stream response: " + err.Error())
+ return true
}
- return 0, nil, nil
- })
- dataChan := make(chan string)
- stopChan := make(chan bool)
- go func() {
- for scanner.Scan() {
- data := scanner.Text()
- if len(data) < 6 { // ignore blank line or wrong format
- continue
- }
- data = data[6:]
- dataChan <- data
+ if baiduResponse.Usage.TotalTokens != 0 {
+ usage.TotalTokens = baiduResponse.Usage.TotalTokens
+ usage.PromptTokens = baiduResponse.Usage.PromptTokens
+ usage.CompletionTokens = baiduResponse.Usage.TotalTokens - baiduResponse.Usage.PromptTokens
}
- stopChan <- true
- }()
- helper.SetEventStreamHeaders(c)
- c.Stream(func(w io.Writer) bool {
- select {
- case data := <-dataChan:
- var baiduResponse BaiduChatStreamResponse
- err := json.Unmarshal([]byte(data), &baiduResponse)
- if err != nil {
- common.SysError("error unmarshalling stream response: " + err.Error())
- return true
- }
- if baiduResponse.Usage.TotalTokens != 0 {
- usage.TotalTokens = baiduResponse.Usage.TotalTokens
- usage.PromptTokens = baiduResponse.Usage.PromptTokens
- usage.CompletionTokens = baiduResponse.Usage.TotalTokens - baiduResponse.Usage.PromptTokens
- }
- response := streamResponseBaidu2OpenAI(&baiduResponse)
- jsonResponse, err := json.Marshal(response)
- if err != nil {
- common.SysError("error marshalling stream response: " + err.Error())
- return true
- }
- c.Render(-1, common.CustomEvent{Data: "data: " + string(jsonResponse)})
- return true
- case <-stopChan:
- c.Render(-1, common.CustomEvent{Data: "data: [DONE]"})
- return false
+ response := streamResponseBaidu2OpenAI(&baiduResponse)
+ err = helper.ObjectData(c, response)
+ if err != nil {
+ common.SysError("error sending stream response: " + err.Error())
}
+ return true
})
common.CloseResponseBodyGracefully(resp)
- return nil, &usage
+ return nil, usage
}
-func baiduHandler(c *gin.Context, resp *http.Response) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
+func baiduHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Response) (*types.NewAPIError, *dto.Usage) {
var baiduResponse BaiduChatResponse
responseBody, err := io.ReadAll(resp.Body)
if err != nil {
- return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
+ return types.NewError(err, types.ErrorCodeBadResponseBody), nil
}
common.CloseResponseBodyGracefully(resp)
err = json.Unmarshal(responseBody, &baiduResponse)
if err != nil {
- return service.OpenAIErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
+ return types.NewError(err, types.ErrorCodeBadResponseBody), nil
}
if baiduResponse.ErrorMsg != "" {
- return &dto.OpenAIErrorWithStatusCode{
- Error: dto.OpenAIError{
- Message: baiduResponse.ErrorMsg,
- Type: "baidu_error",
- Param: "",
- Code: baiduResponse.ErrorCode,
- },
- StatusCode: resp.StatusCode,
- }, nil
+ return types.NewError(fmt.Errorf(baiduResponse.ErrorMsg), types.ErrorCodeBadResponseBody), nil
}
fullTextResponse := responseBaidu2OpenAI(&baiduResponse)
jsonResponse, err := json.Marshal(fullTextResponse)
if err != nil {
- return service.OpenAIErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil
+ return types.NewError(err, types.ErrorCodeBadResponseBody), nil
}
c.Writer.Header().Set("Content-Type", "application/json")
c.Writer.WriteHeader(resp.StatusCode)
@@ -203,32 +162,24 @@ func baiduHandler(c *gin.Context, resp *http.Response) (*dto.OpenAIErrorWithStat
return nil, &fullTextResponse.Usage
}
-func baiduEmbeddingHandler(c *gin.Context, resp *http.Response) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
+func baiduEmbeddingHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Response) (*types.NewAPIError, *dto.Usage) {
var baiduResponse BaiduEmbeddingResponse
responseBody, err := io.ReadAll(resp.Body)
if err != nil {
- return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
+ return types.NewError(err, types.ErrorCodeBadResponseBody), nil
}
common.CloseResponseBodyGracefully(resp)
err = json.Unmarshal(responseBody, &baiduResponse)
if err != nil {
- return service.OpenAIErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
+ return types.NewError(err, types.ErrorCodeBadResponseBody), nil
}
if baiduResponse.ErrorMsg != "" {
- return &dto.OpenAIErrorWithStatusCode{
- Error: dto.OpenAIError{
- Message: baiduResponse.ErrorMsg,
- Type: "baidu_error",
- Param: "",
- Code: baiduResponse.ErrorCode,
- },
- StatusCode: resp.StatusCode,
- }, nil
+ return types.NewError(fmt.Errorf(baiduResponse.ErrorMsg), types.ErrorCodeBadResponseBody), nil
}
fullTextResponse := embeddingResponseBaidu2OpenAI(&baiduResponse)
jsonResponse, err := json.Marshal(fullTextResponse)
if err != nil {
- return service.OpenAIErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil
+ return types.NewError(err, types.ErrorCodeBadResponseBody), nil
}
c.Writer.Header().Set("Content-Type", "application/json")
c.Writer.WriteHeader(resp.StatusCode)
diff --git a/relay/channel/baidu_v2/adaptor.go b/relay/channel/baidu_v2/adaptor.go
index 470f2a0c0dd..375fd5318f6 100644
--- a/relay/channel/baidu_v2/adaptor.go
+++ b/relay/channel/baidu_v2/adaptor.go
@@ -9,6 +9,7 @@ import (
"one-api/relay/channel"
"one-api/relay/channel/openai"
relaycommon "one-api/relay/common"
+ "one-api/types"
"strings"
"github.com/gin-gonic/gin"
@@ -92,11 +93,11 @@ func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, request
return channel.DoApiRequest(a, c, info, requestBody)
}
-func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *dto.OpenAIErrorWithStatusCode) {
+func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *types.NewAPIError) {
if info.IsStream {
- err, usage = openai.OaiStreamHandler(c, resp, info)
+ usage, err = openai.OaiStreamHandler(c, info, resp)
} else {
- err, usage = openai.OpenaiHandler(c, resp, info)
+ usage, err = openai.OpenaiHandler(c, info, resp)
}
return
}
diff --git a/relay/channel/claude/adaptor.go b/relay/channel/claude/adaptor.go
index 8389b9f103a..540742d644d 100644
--- a/relay/channel/claude/adaptor.go
+++ b/relay/channel/claude/adaptor.go
@@ -9,6 +9,7 @@ import (
"one-api/relay/channel"
relaycommon "one-api/relay/common"
"one-api/setting/model_setting"
+ "one-api/types"
"strings"
"github.com/gin-gonic/gin"
@@ -94,7 +95,7 @@ func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, request
return channel.DoApiRequest(a, c, info, requestBody)
}
-func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *dto.OpenAIErrorWithStatusCode) {
+func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *types.NewAPIError) {
if info.IsStream {
err, usage = ClaudeStreamHandler(c, resp, info, a.RequestMode)
} else {
diff --git a/relay/channel/claude/relay-claude.go b/relay/channel/claude/relay-claude.go
index a8607d8647a..f20b573d43e 100644
--- a/relay/channel/claude/relay-claude.go
+++ b/relay/channel/claude/relay-claude.go
@@ -12,11 +12,18 @@ import (
"one-api/relay/helper"
"one-api/service"
"one-api/setting/model_setting"
+ "one-api/types"
"strings"
"github.com/gin-gonic/gin"
)
+const (
+ WebSearchMaxUsesLow = 1
+ WebSearchMaxUsesMedium = 5
+ WebSearchMaxUsesHigh = 10
+)
+
func stopReasonClaude2OpenAI(reason string) string {
switch reason {
case "stop_sequence":
@@ -64,7 +71,7 @@ func RequestOpenAI2ClaudeComplete(textRequest dto.GeneralOpenAIRequest) *dto.Cla
}
func RequestOpenAI2ClaudeMessage(textRequest dto.GeneralOpenAIRequest) (*dto.ClaudeRequest, error) {
- claudeTools := make([]dto.Tool, 0, len(textRequest.Tools))
+ claudeTools := make([]any, 0, len(textRequest.Tools))
for _, tool := range textRequest.Tools {
if params, ok := tool.Function.Parameters.(map[string]any); ok {
@@ -84,10 +91,62 @@ func RequestOpenAI2ClaudeMessage(textRequest dto.GeneralOpenAIRequest) (*dto.Cla
}
claudeTool.InputSchema[s] = a
}
- claudeTools = append(claudeTools, claudeTool)
+ claudeTools = append(claudeTools, &claudeTool)
}
}
+ // Web search tool
+ // https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/web-search-tool
+ if textRequest.WebSearchOptions != nil {
+ webSearchTool := dto.ClaudeWebSearchTool{
+ Type: "web_search_20250305",
+ Name: "web_search",
+ }
+
+ // 处理 user_location
+ if textRequest.WebSearchOptions.UserLocation != nil {
+ anthropicUserLocation := &dto.ClaudeWebSearchUserLocation{
+ Type: "approximate", // 固定为 "approximate"
+ }
+
+ // 解析 UserLocation JSON
+ var userLocationMap map[string]interface{}
+ if err := json.Unmarshal(textRequest.WebSearchOptions.UserLocation, &userLocationMap); err == nil {
+ // 检查是否有 approximate 字段
+ if approximateData, ok := userLocationMap["approximate"].(map[string]interface{}); ok {
+ if timezone, ok := approximateData["timezone"].(string); ok && timezone != "" {
+ anthropicUserLocation.Timezone = timezone
+ }
+ if country, ok := approximateData["country"].(string); ok && country != "" {
+ anthropicUserLocation.Country = country
+ }
+ if region, ok := approximateData["region"].(string); ok && region != "" {
+ anthropicUserLocation.Region = region
+ }
+ if city, ok := approximateData["city"].(string); ok && city != "" {
+ anthropicUserLocation.City = city
+ }
+ }
+ }
+
+ webSearchTool.UserLocation = anthropicUserLocation
+ }
+
+ // 处理 search_context_size 转换为 max_uses
+ if textRequest.WebSearchOptions.SearchContextSize != "" {
+ switch textRequest.WebSearchOptions.SearchContextSize {
+ case "low":
+ webSearchTool.MaxUses = WebSearchMaxUsesLow
+ case "medium":
+ webSearchTool.MaxUses = WebSearchMaxUsesMedium
+ case "high":
+ webSearchTool.MaxUses = WebSearchMaxUsesHigh
+ }
+ }
+
+ claudeTools = append(claudeTools, &webSearchTool)
+ }
+
claudeRequest := dto.ClaudeRequest{
Model: textRequest.Model,
MaxTokens: textRequest.MaxTokens,
@@ -99,6 +158,14 @@ func RequestOpenAI2ClaudeMessage(textRequest dto.GeneralOpenAIRequest) (*dto.Cla
Tools: claudeTools,
}
+ // 处理 tool_choice 和 parallel_tool_calls
+ if textRequest.ToolChoice != nil || textRequest.ParallelTooCalls != nil {
+ claudeToolChoice := mapToolChoice(textRequest.ToolChoice, textRequest.ParallelTooCalls)
+ if claudeToolChoice != nil {
+ claudeRequest.ToolChoice = claudeToolChoice
+ }
+ }
+
if claudeRequest.MaxTokens == 0 {
claudeRequest.MaxTokens = uint(model_setting.GetClaudeSettings().GetDefaultMaxTokens(textRequest.Model))
}
@@ -123,9 +190,30 @@ func RequestOpenAI2ClaudeMessage(textRequest dto.GeneralOpenAIRequest) (*dto.Cla
claudeRequest.Model = strings.TrimSuffix(textRequest.Model, "-thinking")
}
+ if textRequest.ReasoningEffort != "" {
+ switch textRequest.ReasoningEffort {
+ case "low":
+ claudeRequest.Thinking = &dto.Thinking{
+ Type: "enabled",
+ BudgetTokens: common.GetPointer[int](1280),
+ }
+ case "medium":
+ claudeRequest.Thinking = &dto.Thinking{
+ Type: "enabled",
+ BudgetTokens: common.GetPointer[int](2048),
+ }
+ case "high":
+ claudeRequest.Thinking = &dto.Thinking{
+ Type: "enabled",
+ BudgetTokens: common.GetPointer[int](4096),
+ }
+ }
+ }
+
+ // 指定了 reasoning 参数,覆盖 budgetTokens
if textRequest.Reasoning != nil {
var reasoning openrouter.RequestReasoning
- if err := common.UnmarshalJson(textRequest.Reasoning, &reasoning); err != nil {
+ if err := common.Unmarshal(textRequest.Reasoning, &reasoning); err != nil {
return nil, err
}
@@ -517,22 +605,15 @@ func FormatClaudeResponseInfo(requestMode int, claudeResponse *dto.ClaudeRespons
return true
}
-func HandleStreamResponseData(c *gin.Context, info *relaycommon.RelayInfo, claudeInfo *ClaudeResponseInfo, data string, requestMode int) *dto.OpenAIErrorWithStatusCode {
+func HandleStreamResponseData(c *gin.Context, info *relaycommon.RelayInfo, claudeInfo *ClaudeResponseInfo, data string, requestMode int) *types.NewAPIError {
var claudeResponse dto.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)
+ return types.NewError(err, types.ErrorCodeBadResponseBody)
}
if claudeResponse.Error != nil && claudeResponse.Error.Type != "" {
- return &dto.OpenAIErrorWithStatusCode{
- Error: dto.OpenAIError{
- Code: "stream_response_error",
- Type: claudeResponse.Error.Type,
- Message: claudeResponse.Error.Message,
- },
- StatusCode: http.StatusInternalServerError,
- }
+ return types.WithClaudeError(*claudeResponse.Error, http.StatusInternalServerError)
}
if info.RelayFormat == relaycommon.RelayFormatClaude {
FormatClaudeResponseInfo(requestMode, &claudeResponse, nil, claudeInfo)
@@ -593,15 +674,15 @@ func HandleStreamFinalResponse(c *gin.Context, info *relaycommon.RelayInfo, clau
}
}
-func ClaudeStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo, requestMode int) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
+func ClaudeStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo, requestMode int) (*types.NewAPIError, *dto.Usage) {
claudeInfo := &ClaudeResponseInfo{
- ResponseId: fmt.Sprintf("chatcmpl-%s", common.GetUUID()),
+ ResponseId: helper.GetResponseID(c),
Created: common.GetTimestamp(),
Model: info.UpstreamModelName,
ResponseText: strings.Builder{},
Usage: &dto.Usage{},
}
- var err *dto.OpenAIErrorWithStatusCode
+ var err *types.NewAPIError
helper.StreamScannerHandler(c, resp, info, func(data string) bool {
err = HandleStreamResponseData(c, info, claudeInfo, data, requestMode)
if err != nil {
@@ -617,21 +698,14 @@ func ClaudeStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.
return nil, claudeInfo.Usage
}
-func HandleClaudeResponseData(c *gin.Context, info *relaycommon.RelayInfo, claudeInfo *ClaudeResponseInfo, data []byte, requestMode int) *dto.OpenAIErrorWithStatusCode {
+func HandleClaudeResponseData(c *gin.Context, info *relaycommon.RelayInfo, claudeInfo *ClaudeResponseInfo, data []byte, requestMode int) *types.NewAPIError {
var claudeResponse dto.ClaudeResponse
- err := common.UnmarshalJson(data, &claudeResponse)
+ err := common.Unmarshal(data, &claudeResponse)
if err != nil {
- return service.OpenAIErrorWrapper(err, "unmarshal_claude_response_failed", http.StatusInternalServerError)
+ return types.NewError(err, types.ErrorCodeBadResponseBody)
}
if claudeResponse.Error != nil && claudeResponse.Error.Type != "" {
- return &dto.OpenAIErrorWithStatusCode{
- Error: dto.OpenAIError{
- Message: claudeResponse.Error.Message,
- Type: claudeResponse.Error.Type,
- Code: claudeResponse.Error.Type,
- },
- StatusCode: http.StatusInternalServerError,
- }
+ return types.WithClaudeError(*claudeResponse.Error, http.StatusInternalServerError)
}
if requestMode == RequestModeCompletion {
completionTokens := service.CountTextToken(claudeResponse.Completion, info.OriginModelName)
@@ -652,21 +726,25 @@ func HandleClaudeResponseData(c *gin.Context, info *relaycommon.RelayInfo, claud
openaiResponse.Usage = *claudeInfo.Usage
responseData, err = json.Marshal(openaiResponse)
if err != nil {
- return service.OpenAIErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError)
+ return types.NewError(err, types.ErrorCodeBadResponseBody)
}
case relaycommon.RelayFormatClaude:
responseData = data
}
+ if claudeResponse.Usage.ServerToolUse != nil && claudeResponse.Usage.ServerToolUse.WebSearchRequests > 0 {
+ c.Set("claude_web_search_requests", claudeResponse.Usage.ServerToolUse.WebSearchRequests)
+ }
+
common.IOCopyBytesGracefully(c, nil, responseData)
return nil
}
-func ClaudeHandler(c *gin.Context, resp *http.Response, requestMode int, info *relaycommon.RelayInfo) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
+func ClaudeHandler(c *gin.Context, resp *http.Response, requestMode int, info *relaycommon.RelayInfo) (*types.NewAPIError, *dto.Usage) {
defer common.CloseResponseBodyGracefully(resp)
claudeInfo := &ClaudeResponseInfo{
- ResponseId: fmt.Sprintf("chatcmpl-%s", common.GetUUID()),
+ ResponseId: helper.GetResponseID(c),
Created: common.GetTimestamp(),
Model: info.UpstreamModelName,
ResponseText: strings.Builder{},
@@ -674,7 +752,7 @@ func ClaudeHandler(c *gin.Context, resp *http.Response, requestMode int, info *r
}
responseBody, err := io.ReadAll(resp.Body)
if err != nil {
- return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
+ return types.NewError(err, types.ErrorCodeBadResponseBody), nil
}
if common.DebugEnabled {
println("responseBody: ", string(responseBody))
@@ -685,3 +763,51 @@ func ClaudeHandler(c *gin.Context, resp *http.Response, requestMode int, info *r
}
return nil, claudeInfo.Usage
}
+
+func mapToolChoice(toolChoice any, parallelToolCalls *bool) *dto.ClaudeToolChoice {
+ var claudeToolChoice *dto.ClaudeToolChoice
+
+ // 处理 tool_choice 字符串值
+ if toolChoiceStr, ok := toolChoice.(string); ok {
+ switch toolChoiceStr {
+ case "auto":
+ claudeToolChoice = &dto.ClaudeToolChoice{
+ Type: "auto",
+ }
+ case "required":
+ claudeToolChoice = &dto.ClaudeToolChoice{
+ Type: "any",
+ }
+ case "none":
+ claudeToolChoice = &dto.ClaudeToolChoice{
+ Type: "none",
+ }
+ }
+ } else if toolChoiceMap, ok := toolChoice.(map[string]interface{}); ok {
+ // 处理 tool_choice 对象值
+ if function, ok := toolChoiceMap["function"].(map[string]interface{}); ok {
+ if toolName, ok := function["name"].(string); ok {
+ claudeToolChoice = &dto.ClaudeToolChoice{
+ Type: "tool",
+ Name: toolName,
+ }
+ }
+ }
+ }
+
+ // 处理 parallel_tool_calls
+ if parallelToolCalls != nil {
+ if claudeToolChoice == nil {
+ // 如果没有 tool_choice,但有 parallel_tool_calls,创建默认的 auto 类型
+ claudeToolChoice = &dto.ClaudeToolChoice{
+ Type: "auto",
+ }
+ }
+
+ // 设置 disable_parallel_tool_use
+ // 如果 parallel_tool_calls 为 true,则 disable_parallel_tool_use 为 false
+ claudeToolChoice.DisableParallelToolUse = !*parallelToolCalls
+ }
+
+ return claudeToolChoice
+}
diff --git a/relay/channel/cloudflare/adaptor.go b/relay/channel/cloudflare/adaptor.go
index 06f4ca345c7..6e59ad715b2 100644
--- a/relay/channel/cloudflare/adaptor.go
+++ b/relay/channel/cloudflare/adaptor.go
@@ -10,6 +10,7 @@ import (
"one-api/relay/channel"
relaycommon "one-api/relay/common"
"one-api/relay/constant"
+ "one-api/types"
"github.com/gin-gonic/gin"
)
@@ -94,20 +95,20 @@ func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInf
return nil, errors.New("not implemented")
}
-func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *dto.OpenAIErrorWithStatusCode) {
+func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *types.NewAPIError) {
switch info.RelayMode {
case constant.RelayModeEmbeddings:
fallthrough
case constant.RelayModeChatCompletions:
if info.IsStream {
- err, usage = cfStreamHandler(c, resp, info)
+ err, usage = cfStreamHandler(c, info, resp)
} else {
- err, usage = cfHandler(c, resp, info)
+ err, usage = cfHandler(c, info, resp)
}
case constant.RelayModeAudioTranslation:
fallthrough
case constant.RelayModeAudioTranscription:
- err, usage = cfSTTHandler(c, resp, info)
+ err, usage = cfSTTHandler(c, info, resp)
}
return
}
diff --git a/relay/channel/cloudflare/relay_cloudflare.go b/relay/channel/cloudflare/relay_cloudflare.go
index 1c3a26f7d69..5e8fe7f9215 100644
--- a/relay/channel/cloudflare/relay_cloudflare.go
+++ b/relay/channel/cloudflare/relay_cloudflare.go
@@ -3,7 +3,6 @@ package cloudflare
import (
"bufio"
"encoding/json"
- "github.com/gin-gonic/gin"
"io"
"net/http"
"one-api/common"
@@ -11,8 +10,11 @@ import (
relaycommon "one-api/relay/common"
"one-api/relay/helper"
"one-api/service"
+ "one-api/types"
"strings"
"time"
+
+ "github.com/gin-gonic/gin"
)
func convertCf2CompletionsRequest(textRequest dto.GeneralOpenAIRequest) *CfRequest {
@@ -25,7 +27,7 @@ func convertCf2CompletionsRequest(textRequest dto.GeneralOpenAIRequest) *CfReque
}
}
-func cfStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
+func cfStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Response) (*types.NewAPIError, *dto.Usage) {
scanner := bufio.NewScanner(resp.Body)
scanner.Split(bufio.ScanLines)
@@ -86,16 +88,16 @@ func cfStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.Rela
return nil, usage
}
-func cfHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
+func cfHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Response) (*types.NewAPIError, *dto.Usage) {
responseBody, err := io.ReadAll(resp.Body)
if err != nil {
- return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
+ return types.NewError(err, types.ErrorCodeBadResponseBody), nil
}
common.CloseResponseBodyGracefully(resp)
var response dto.TextResponse
err = json.Unmarshal(responseBody, &response)
if err != nil {
- return service.OpenAIErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
+ return types.NewError(err, types.ErrorCodeBadResponseBody), nil
}
response.Model = info.UpstreamModelName
var responseText string
@@ -107,7 +109,7 @@ func cfHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo)
response.Id = helper.GetResponseID(c)
jsonResponse, err := json.Marshal(response)
if err != nil {
- return service.OpenAIErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil
+ return types.NewError(err, types.ErrorCodeBadResponseBody), nil
}
c.Writer.Header().Set("Content-Type", "application/json")
c.Writer.WriteHeader(resp.StatusCode)
@@ -115,16 +117,16 @@ func cfHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo)
return nil, usage
}
-func cfSTTHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
+func cfSTTHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Response) (*types.NewAPIError, *dto.Usage) {
var cfResp CfAudioResponse
responseBody, err := io.ReadAll(resp.Body)
if err != nil {
- return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
+ return types.NewError(err, types.ErrorCodeBadResponseBody), nil
}
common.CloseResponseBodyGracefully(resp)
err = json.Unmarshal(responseBody, &cfResp)
if err != nil {
- return service.OpenAIErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
+ return types.NewError(err, types.ErrorCodeBadResponseBody), nil
}
audioResp := &dto.AudioResponse{
@@ -133,7 +135,7 @@ func cfSTTHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayIn
jsonResponse, err := json.Marshal(audioResp)
if err != nil {
- return service.OpenAIErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil
+ return types.NewError(err, types.ErrorCodeBadResponseBody), nil
}
c.Writer.Header().Set("Content-Type", "application/json")
c.Writer.WriteHeader(resp.StatusCode)
diff --git a/relay/channel/cohere/adaptor.go b/relay/channel/cohere/adaptor.go
index a93b10f680c..4f3a96c3270 100644
--- a/relay/channel/cohere/adaptor.go
+++ b/relay/channel/cohere/adaptor.go
@@ -9,6 +9,7 @@ import (
"one-api/relay/channel"
relaycommon "one-api/relay/common"
"one-api/relay/constant"
+ "one-api/types"
"github.com/gin-gonic/gin"
)
@@ -71,14 +72,14 @@ func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.Rela
return nil, errors.New("not implemented")
}
-func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *dto.OpenAIErrorWithStatusCode) {
+func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *types.NewAPIError) {
if info.RelayMode == constant.RelayModeRerank {
- err, usage = cohereRerankHandler(c, resp, info)
+ usage, err = cohereRerankHandler(c, resp, info)
} else {
if info.IsStream {
- err, usage = cohereStreamHandler(c, resp, info)
+ usage, err = cohereStreamHandler(c, info, resp) // TODO: fix this
} else {
- err, usage = cohereHandler(c, resp, info.UpstreamModelName, info.PromptTokens)
+ usage, err = cohereHandler(c, info, resp)
}
}
return
diff --git a/relay/channel/cohere/relay-cohere.go b/relay/channel/cohere/relay-cohere.go
index 4637740d13c..fcfb12b7521 100644
--- a/relay/channel/cohere/relay-cohere.go
+++ b/relay/channel/cohere/relay-cohere.go
@@ -3,7 +3,6 @@ package cohere
import (
"bufio"
"encoding/json"
- "github.com/gin-gonic/gin"
"io"
"net/http"
"one-api/common"
@@ -11,8 +10,11 @@ import (
relaycommon "one-api/relay/common"
"one-api/relay/helper"
"one-api/service"
+ "one-api/types"
"strings"
"time"
+
+ "github.com/gin-gonic/gin"
)
func requestOpenAI2Cohere(textRequest dto.GeneralOpenAIRequest) *CohereRequest {
@@ -76,7 +78,7 @@ func stopReasonCohere2OpenAI(reason string) string {
}
}
-func cohereStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
+func cohereStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Response) (*dto.Usage, *types.NewAPIError) {
responseId := helper.GetResponseID(c)
createdTime := common.GetTimestamp()
usage := &dto.Usage{}
@@ -164,20 +166,20 @@ func cohereStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.
if usage.PromptTokens == 0 {
usage = service.ResponseText2Usage(responseText, info.UpstreamModelName, info.PromptTokens)
}
- return nil, usage
+ return usage, nil
}
-func cohereHandler(c *gin.Context, resp *http.Response, modelName string, promptTokens int) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
+func cohereHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Response) (*dto.Usage, *types.NewAPIError) {
createdTime := common.GetTimestamp()
responseBody, err := io.ReadAll(resp.Body)
if err != nil {
- return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
+ return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
}
common.CloseResponseBodyGracefully(resp)
var cohereResp CohereResponseResult
err = json.Unmarshal(responseBody, &cohereResp)
if err != nil {
- return service.OpenAIErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
+ return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
}
usage := dto.Usage{}
usage.PromptTokens = cohereResp.Meta.BilledUnits.InputTokens
@@ -188,7 +190,7 @@ func cohereHandler(c *gin.Context, resp *http.Response, modelName string, prompt
openaiResp.Id = cohereResp.ResponseId
openaiResp.Created = createdTime
openaiResp.Object = "chat.completion"
- openaiResp.Model = modelName
+ openaiResp.Model = info.UpstreamModelName
openaiResp.Usage = usage
openaiResp.Choices = []dto.OpenAITextResponseChoice{
@@ -201,24 +203,24 @@ func cohereHandler(c *gin.Context, resp *http.Response, modelName string, prompt
jsonResponse, err := json.Marshal(openaiResp)
if err != nil {
- return service.OpenAIErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil
+ return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
}
c.Writer.Header().Set("Content-Type", "application/json")
c.Writer.WriteHeader(resp.StatusCode)
- _, err = c.Writer.Write(jsonResponse)
- return nil, &usage
+ _, _ = c.Writer.Write(jsonResponse)
+ return &usage, nil
}
-func cohereRerankHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
+func cohereRerankHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (*dto.Usage, *types.NewAPIError) {
responseBody, err := io.ReadAll(resp.Body)
if err != nil {
- return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
+ return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
}
common.CloseResponseBodyGracefully(resp)
var cohereResp CohereRerankResponseResult
err = json.Unmarshal(responseBody, &cohereResp)
if err != nil {
- return service.OpenAIErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
+ return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
}
usage := dto.Usage{}
if cohereResp.Meta.BilledUnits.InputTokens == 0 {
@@ -237,10 +239,10 @@ func cohereRerankHandler(c *gin.Context, resp *http.Response, info *relaycommon.
jsonResponse, err := json.Marshal(rerankResp)
if err != nil {
- return service.OpenAIErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil
+ return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
}
c.Writer.Header().Set("Content-Type", "application/json")
c.Writer.WriteHeader(resp.StatusCode)
_, err = c.Writer.Write(jsonResponse)
- return nil, &usage
+ return &usage, nil
}
diff --git a/relay/channel/coze/adaptor.go b/relay/channel/coze/adaptor.go
index 80441a512c4..fe5f5f00223 100644
--- a/relay/channel/coze/adaptor.go
+++ b/relay/channel/coze/adaptor.go
@@ -9,6 +9,7 @@ import (
"one-api/dto"
"one-api/relay/channel"
"one-api/relay/common"
+ "one-api/types"
"time"
"github.com/gin-gonic/gin"
@@ -95,11 +96,11 @@ func (a *Adaptor) DoRequest(c *gin.Context, info *common.RelayInfo, requestBody
}
// DoResponse implements channel.Adaptor.
-func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *common.RelayInfo) (usage any, err *dto.OpenAIErrorWithStatusCode) {
+func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *common.RelayInfo) (usage any, err *types.NewAPIError) {
if info.IsStream {
- err, usage = cozeChatStreamHandler(c, resp, info)
+ usage, err = cozeChatStreamHandler(c, info, resp)
} else {
- err, usage = cozeChatHandler(c, resp, info)
+ usage, err = cozeChatHandler(c, info, resp)
}
return
}
diff --git a/relay/channel/coze/relay-coze.go b/relay/channel/coze/relay-coze.go
index 618fe16f4f7..32cc69376a0 100644
--- a/relay/channel/coze/relay-coze.go
+++ b/relay/channel/coze/relay-coze.go
@@ -12,6 +12,7 @@ import (
relaycommon "one-api/relay/common"
"one-api/relay/helper"
"one-api/service"
+ "one-api/types"
"strings"
"github.com/gin-gonic/gin"
@@ -43,10 +44,10 @@ func convertCozeChatRequest(c *gin.Context, request dto.GeneralOpenAIRequest) *C
return cozeRequest
}
-func cozeChatHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
+func cozeChatHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Response) (*dto.Usage, *types.NewAPIError) {
responseBody, err := io.ReadAll(resp.Body)
if err != nil {
- return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
+ return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
}
common.CloseResponseBodyGracefully(resp)
// convert coze response to openai response
@@ -55,10 +56,10 @@ func cozeChatHandler(c *gin.Context, resp *http.Response, info *relaycommon.Rela
response.Model = info.UpstreamModelName
err = json.Unmarshal(responseBody, &cozeResponse)
if err != nil {
- return service.OpenAIErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
+ return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
}
if cozeResponse.Code != 0 {
- return service.OpenAIErrorWrapper(errors.New(cozeResponse.Msg), fmt.Sprintf("%d", cozeResponse.Code), http.StatusInternalServerError), nil
+ return nil, types.NewError(errors.New(cozeResponse.Msg), types.ErrorCodeBadResponseBody)
}
// 从上下文获取 usage
var usage dto.Usage
@@ -85,16 +86,16 @@ func cozeChatHandler(c *gin.Context, resp *http.Response, info *relaycommon.Rela
}
jsonResponse, err := json.Marshal(response)
if err != nil {
- return service.OpenAIErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil
+ return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
}
c.Writer.Header().Set("Content-Type", "application/json")
c.Writer.WriteHeader(resp.StatusCode)
_, _ = c.Writer.Write(jsonResponse)
- return nil, &usage
+ return &usage, nil
}
-func cozeChatStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
+func cozeChatStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Response) (*dto.Usage, *types.NewAPIError) {
scanner := bufio.NewScanner(resp.Body)
scanner.Split(bufio.ScanLines)
helper.SetEventStreamHeaders(c)
@@ -135,7 +136,7 @@ func cozeChatStreamHandler(c *gin.Context, resp *http.Response, info *relaycommo
}
if err := scanner.Err(); err != nil {
- return service.OpenAIErrorWrapper(err, "stream_scanner_error", http.StatusInternalServerError), nil
+ return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
}
helper.Done(c)
@@ -143,7 +144,7 @@ func cozeChatStreamHandler(c *gin.Context, resp *http.Response, info *relaycommo
usage = service.ResponseText2Usage(responseText, info.UpstreamModelName, c.GetInt("coze_input_count"))
}
- return nil, usage
+ return usage, nil
}
func handleCozeEvent(c *gin.Context, event string, data string, responseText *string, usage *dto.Usage, id string, info *relaycommon.RelayInfo) {
diff --git a/relay/channel/deepseek/adaptor.go b/relay/channel/deepseek/adaptor.go
index 76e7fa8de8c..edfc7fd3bcb 100644
--- a/relay/channel/deepseek/adaptor.go
+++ b/relay/channel/deepseek/adaptor.go
@@ -10,6 +10,7 @@ import (
"one-api/relay/channel/openai"
relaycommon "one-api/relay/common"
"one-api/relay/constant"
+ "one-api/types"
"strings"
"github.com/gin-gonic/gin"
@@ -81,11 +82,11 @@ func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, request
return channel.DoApiRequest(a, c, info, requestBody)
}
-func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *dto.OpenAIErrorWithStatusCode) {
+func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *types.NewAPIError) {
if info.IsStream {
- err, usage = openai.OaiStreamHandler(c, resp, info)
+ usage, err = openai.OaiStreamHandler(c, info, resp)
} else {
- err, usage = openai.OpenaiHandler(c, resp, info)
+ usage, err = openai.OpenaiHandler(c, info, resp)
}
return
}
diff --git a/relay/channel/dify/adaptor.go b/relay/channel/dify/adaptor.go
index 51dbee7167a..4ad167663b9 100644
--- a/relay/channel/dify/adaptor.go
+++ b/relay/channel/dify/adaptor.go
@@ -8,6 +8,7 @@ import (
"one-api/dto"
"one-api/relay/channel"
relaycommon "one-api/relay/common"
+ "one-api/types"
"github.com/gin-gonic/gin"
)
@@ -96,11 +97,11 @@ func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, request
return channel.DoApiRequest(a, c, info, requestBody)
}
-func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *dto.OpenAIErrorWithStatusCode) {
+func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *types.NewAPIError) {
if info.IsStream {
- err, usage = difyStreamHandler(c, resp, info)
+ return difyStreamHandler(c, info, resp)
} else {
- err, usage = difyHandler(c, resp, info)
+ return difyHandler(c, info, resp)
}
return
}
diff --git a/relay/channel/dify/relay-dify.go b/relay/channel/dify/relay-dify.go
index 3a2845b37f6..4733712719a 100644
--- a/relay/channel/dify/relay-dify.go
+++ b/relay/channel/dify/relay-dify.go
@@ -14,6 +14,7 @@ import (
relaycommon "one-api/relay/common"
"one-api/relay/helper"
"one-api/service"
+ "one-api/types"
"os"
"strings"
@@ -209,7 +210,7 @@ func streamResponseDify2OpenAI(difyResponse DifyChunkChatCompletionResponse) *dt
return &response
}
-func difyStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
+func difyStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Response) (*dto.Usage, *types.NewAPIError) {
var responseText string
usage := &dto.Usage{}
var nodeToken int
@@ -247,20 +248,20 @@ func difyStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.Re
usage = service.ResponseText2Usage(responseText, info.UpstreamModelName, info.PromptTokens)
}
usage.CompletionTokens += nodeToken
- return nil, usage
+ return usage, nil
}
-func difyHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
+func difyHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Response) (*dto.Usage, *types.NewAPIError) {
var difyResponse DifyChatCompletionResponse
responseBody, err := io.ReadAll(resp.Body)
if err != nil {
- return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
+ return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
}
common.CloseResponseBodyGracefully(resp)
err = json.Unmarshal(responseBody, &difyResponse)
if err != nil {
- return service.OpenAIErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
+ return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
}
fullTextResponse := dto.OpenAITextResponse{
Id: difyResponse.ConversationId,
@@ -279,10 +280,10 @@ func difyHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInf
fullTextResponse.Choices = append(fullTextResponse.Choices, choice)
jsonResponse, err := json.Marshal(fullTextResponse)
if err != nil {
- return service.OpenAIErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil
+ return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
}
c.Writer.Header().Set("Content-Type", "application/json")
c.Writer.WriteHeader(resp.StatusCode)
- _, err = c.Writer.Write(jsonResponse)
- return nil, &difyResponse.MetaData.Usage
+ c.Writer.Write(jsonResponse)
+ return &difyResponse.MetaData.Usage, nil
}
diff --git a/relay/channel/gemini/adaptor.go b/relay/channel/gemini/adaptor.go
index 968d9c9b9ef..71eb9ba4384 100644
--- a/relay/channel/gemini/adaptor.go
+++ b/relay/channel/gemini/adaptor.go
@@ -11,8 +11,8 @@ import (
"one-api/relay/channel"
relaycommon "one-api/relay/common"
"one-api/relay/constant"
- "one-api/service"
"one-api/setting/model_setting"
+ "one-api/types"
"strings"
"github.com/gin-gonic/gin"
@@ -168,30 +168,30 @@ func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, request
return channel.DoApiRequest(a, c, info, requestBody)
}
-func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *dto.OpenAIErrorWithStatusCode) {
+func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *types.NewAPIError) {
if info.RelayMode == constant.RelayModeGemini {
if info.IsStream {
- return GeminiTextGenerationStreamHandler(c, resp, info)
+ return GeminiTextGenerationStreamHandler(c, info, resp)
} else {
- return GeminiTextGenerationHandler(c, resp, info)
+ return GeminiTextGenerationHandler(c, info, resp)
}
}
if strings.HasPrefix(info.UpstreamModelName, "imagen") {
- return GeminiImageHandler(c, resp, info)
+ return GeminiImageHandler(c, info, resp)
}
// check if the model is an embedding model
if strings.HasPrefix(info.UpstreamModelName, "text-embedding") ||
strings.HasPrefix(info.UpstreamModelName, "embedding") ||
strings.HasPrefix(info.UpstreamModelName, "gemini-embedding") {
- return GeminiEmbeddingHandler(c, resp, info)
+ return GeminiEmbeddingHandler(c, info, resp)
}
if info.IsStream {
- err, usage = GeminiChatStreamHandler(c, resp, info)
+ return GeminiChatStreamHandler(c, info, resp)
} else {
- err, usage = GeminiChatHandler(c, resp, info)
+ return GeminiChatHandler(c, info, resp)
}
//if usage.(*dto.Usage).CompletionTokenDetails.ReasoningTokens > 100 {
@@ -205,23 +205,23 @@ func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycom
// }
//}
- return
+ return nil, types.NewError(errors.New("not implemented"), types.ErrorCodeBadResponseBody)
}
-func GeminiImageHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *dto.OpenAIErrorWithStatusCode) {
+func GeminiImageHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Response) (*dto.Usage, *types.NewAPIError) {
responseBody, readErr := io.ReadAll(resp.Body)
if readErr != nil {
- return nil, service.OpenAIErrorWrapper(readErr, "read_response_body_failed", http.StatusInternalServerError)
+ return nil, types.NewError(readErr, types.ErrorCodeBadResponseBody)
}
_ = resp.Body.Close()
var geminiResponse GeminiImageResponse
if jsonErr := json.Unmarshal(responseBody, &geminiResponse); jsonErr != nil {
- return nil, service.OpenAIErrorWrapper(jsonErr, "unmarshal_response_body_failed", http.StatusInternalServerError)
+ return nil, types.NewError(jsonErr, types.ErrorCodeBadResponseBody)
}
if len(geminiResponse.Predictions) == 0 {
- return nil, service.OpenAIErrorWrapper(errors.New("no images generated"), "no_images", http.StatusBadRequest)
+ return nil, types.NewError(errors.New("no images generated"), types.ErrorCodeBadResponseBody)
}
// convert to openai format response
@@ -241,7 +241,7 @@ func GeminiImageHandler(c *gin.Context, resp *http.Response, info *relaycommon.R
jsonResponse, jsonErr := json.Marshal(openAIResponse)
if jsonErr != nil {
- return nil, service.OpenAIErrorWrapper(jsonErr, "marshal_response_failed", http.StatusInternalServerError)
+ return nil, types.NewError(jsonErr, types.ErrorCodeBadResponseBody)
}
c.Writer.Header().Set("Content-Type", "application/json")
@@ -253,7 +253,7 @@ func GeminiImageHandler(c *gin.Context, resp *http.Response, info *relaycommon.R
const imageTokens = 258
generatedImages := len(openAIResponse.Data)
- usage = &dto.Usage{
+ usage := &dto.Usage{
PromptTokens: imageTokens * generatedImages, // each generated image has fixed 258 tokens
CompletionTokens: 0, // image generation does not calculate completion tokens
TotalTokens: imageTokens * generatedImages,
diff --git a/relay/channel/gemini/relay-gemini-native.go b/relay/channel/gemini/relay-gemini-native.go
index 52846c661c2..0870e3fabc3 100644
--- a/relay/channel/gemini/relay-gemini-native.go
+++ b/relay/channel/gemini/relay-gemini-native.go
@@ -8,18 +8,19 @@ import (
relaycommon "one-api/relay/common"
"one-api/relay/helper"
"one-api/service"
+ "one-api/types"
"strings"
"github.com/gin-gonic/gin"
)
-func GeminiTextGenerationHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (*dto.Usage, *dto.OpenAIErrorWithStatusCode) {
+func GeminiTextGenerationHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Response) (*dto.Usage, *types.NewAPIError) {
defer common.CloseResponseBodyGracefully(resp)
// 读取响应体
responseBody, err := io.ReadAll(resp.Body)
if err != nil {
- return nil, service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError)
+ return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
}
if common.DebugEnabled {
@@ -28,9 +29,9 @@ func GeminiTextGenerationHandler(c *gin.Context, resp *http.Response, info *rela
// 解析为 Gemini 原生响应格式
var geminiResponse GeminiChatResponse
- err = common.UnmarshalJson(responseBody, &geminiResponse)
+ err = common.Unmarshal(responseBody, &geminiResponse)
if err != nil {
- return nil, service.OpenAIErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError)
+ return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
}
// 计算使用量(基于 UsageMetadata)
@@ -51,9 +52,9 @@ func GeminiTextGenerationHandler(c *gin.Context, resp *http.Response, info *rela
}
// 直接返回 Gemini 原生格式的 JSON 响应
- jsonResponse, err := common.EncodeJson(geminiResponse)
+ jsonResponse, err := common.Marshal(geminiResponse)
if err != nil {
- return nil, service.OpenAIErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError)
+ return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
}
common.IOCopyBytesGracefully(c, resp, jsonResponse)
@@ -61,7 +62,7 @@ func GeminiTextGenerationHandler(c *gin.Context, resp *http.Response, info *rela
return &usage, nil
}
-func GeminiTextGenerationStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (*dto.Usage, *dto.OpenAIErrorWithStatusCode) {
+func GeminiTextGenerationStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Response) (*dto.Usage, *types.NewAPIError) {
var usage = &dto.Usage{}
var imageCount int
diff --git a/relay/channel/gemini/relay-gemini.go b/relay/channel/gemini/relay-gemini.go
index 1544e8cfe07..6f3babeb1f9 100644
--- a/relay/channel/gemini/relay-gemini.go
+++ b/relay/channel/gemini/relay-gemini.go
@@ -2,6 +2,7 @@ package gemini
import (
"encoding/json"
+ "errors"
"fmt"
"io"
"net/http"
@@ -12,6 +13,7 @@ import (
"one-api/relay/helper"
"one-api/service"
"one-api/setting/model_setting"
+ "one-api/types"
"strconv"
"strings"
"unicode/utf8"
@@ -792,7 +794,7 @@ func streamResponseGeminiChat2OpenAI(geminiResponse *GeminiChatResponse) (*dto.C
return &response, isStop, hasImage
}
-func GeminiChatStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
+func GeminiChatStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Response) (*dto.Usage, *types.NewAPIError) {
// responseText := ""
id := helper.GetResponseID(c)
createAt := common.GetTimestamp()
@@ -858,33 +860,25 @@ func GeminiChatStreamHandler(c *gin.Context, resp *http.Response, info *relaycom
}
helper.Done(c)
//resp.Body.Close()
- return nil, usage
+ return usage, nil
}
-func GeminiChatHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
+func GeminiChatHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Response) (*dto.Usage, *types.NewAPIError) {
responseBody, err := io.ReadAll(resp.Body)
if err != nil {
- return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
+ return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
}
common.CloseResponseBodyGracefully(resp)
if common.DebugEnabled {
println(string(responseBody))
}
var geminiResponse GeminiChatResponse
- err = common.UnmarshalJson(responseBody, &geminiResponse)
+ err = common.Unmarshal(responseBody, &geminiResponse)
if err != nil {
- return service.OpenAIErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
+ return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
}
if len(geminiResponse.Candidates) == 0 {
- return &dto.OpenAIErrorWithStatusCode{
- Error: dto.OpenAIError{
- Message: "No candidates returned",
- Type: "server_error",
- Param: "",
- Code: 500,
- },
- StatusCode: resp.StatusCode,
- }, nil
+ return nil, types.NewError(errors.New("no candidates returned"), types.ErrorCodeBadResponseBody)
}
fullTextResponse := responseGeminiChat2OpenAI(c, &geminiResponse)
fullTextResponse.Model = info.UpstreamModelName
@@ -908,25 +902,25 @@ func GeminiChatHandler(c *gin.Context, resp *http.Response, info *relaycommon.Re
fullTextResponse.Usage = usage
jsonResponse, err := json.Marshal(fullTextResponse)
if err != nil {
- return service.OpenAIErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil
+ return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
}
c.Writer.Header().Set("Content-Type", "application/json")
c.Writer.WriteHeader(resp.StatusCode)
- _, err = c.Writer.Write(jsonResponse)
- return nil, &usage
+ c.Writer.Write(jsonResponse)
+ return &usage, nil
}
-func GeminiEmbeddingHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *dto.OpenAIErrorWithStatusCode) {
+func GeminiEmbeddingHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Response) (*dto.Usage, *types.NewAPIError) {
defer common.CloseResponseBodyGracefully(resp)
responseBody, readErr := io.ReadAll(resp.Body)
if readErr != nil {
- return nil, service.OpenAIErrorWrapper(readErr, "read_response_body_failed", http.StatusInternalServerError)
+ return nil, types.NewError(readErr, types.ErrorCodeBadResponseBody)
}
var geminiResponse GeminiEmbeddingResponse
- if jsonErr := json.Unmarshal(responseBody, &geminiResponse); jsonErr != nil {
- return nil, service.OpenAIErrorWrapper(jsonErr, "unmarshal_response_body_failed", http.StatusInternalServerError)
+ if jsonErr := common.Unmarshal(responseBody, &geminiResponse); jsonErr != nil {
+ return nil, types.NewError(jsonErr, types.ErrorCodeBadResponseBody)
}
// convert to openai format response
@@ -947,16 +941,16 @@ func GeminiEmbeddingHandler(c *gin.Context, resp *http.Response, info *relaycomm
// Google has not yet clarified how embedding models will be billed
// refer to openai billing method to use input tokens billing
// https://platform.openai.com/docs/guides/embeddings#what-are-embeddings
- usage = &dto.Usage{
+ usage := &dto.Usage{
PromptTokens: info.PromptTokens,
CompletionTokens: 0,
TotalTokens: info.PromptTokens,
}
- openAIResponse.Usage = *usage.(*dto.Usage)
+ openAIResponse.Usage = *usage
- jsonResponse, jsonErr := common.EncodeJson(openAIResponse)
+ jsonResponse, jsonErr := common.Marshal(openAIResponse)
if jsonErr != nil {
- return nil, service.OpenAIErrorWrapper(jsonErr, "marshal_response_failed", http.StatusInternalServerError)
+ return nil, types.NewError(jsonErr, types.ErrorCodeBadResponseBody)
}
common.IOCopyBytesGracefully(c, resp, jsonResponse)
diff --git a/relay/channel/jimeng/adaptor.go b/relay/channel/jimeng/adaptor.go
new file mode 100644
index 00000000000..0b743879d85
--- /dev/null
+++ b/relay/channel/jimeng/adaptor.go
@@ -0,0 +1,136 @@
+package jimeng
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "github.com/gin-gonic/gin"
+ "io"
+ "net/http"
+ "one-api/dto"
+ "one-api/relay/channel"
+ "one-api/relay/channel/openai"
+ relaycommon "one-api/relay/common"
+ relayconstant "one-api/relay/constant"
+ "one-api/types"
+)
+
+type Adaptor struct {
+}
+
+func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) {
+ return nil, errors.New("not implemented")
+}
+
+func (a *Adaptor) Init(info *relaycommon.RelayInfo) {
+}
+
+func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
+ return fmt.Sprintf("%s/?Action=CVProcess&Version=2022-08-31", info.BaseUrl), nil
+}
+
+func (a *Adaptor) SetupRequestHeader(c *gin.Context, header *http.Header, info *relaycommon.RelayInfo) error {
+ return errors.New("not implemented")
+}
+
+func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) {
+ if request == nil {
+ return nil, errors.New("request is nil")
+ }
+ return request, nil
+}
+
+type LogoInfo struct {
+ AddLogo bool `json:"add_logo,omitempty"`
+ Position int `json:"position,omitempty"`
+ Language int `json:"language,omitempty"`
+ Opacity float64 `json:"opacity,omitempty"`
+ LogoTextContent string `json:"logo_text_content,omitempty"`
+}
+
+type imageRequestPayload struct {
+ ReqKey string `json:"req_key"` // Service identifier, fixed value: jimeng_high_aes_general_v21_L
+ Prompt string `json:"prompt"` // Prompt for image generation, supports both Chinese and English
+ Seed int64 `json:"seed,omitempty"` // Random seed, default -1 (random)
+ Width int `json:"width,omitempty"` // Image width, default 512, range [256, 768]
+ Height int `json:"height,omitempty"` // Image height, default 512, range [256, 768]
+ UsePreLLM bool `json:"use_pre_llm,omitempty"` // Enable text expansion, default true
+ UseSR bool `json:"use_sr,omitempty"` // Enable super resolution, default true
+ ReturnURL bool `json:"return_url,omitempty"` // Whether to return image URL (valid for 24 hours)
+ LogoInfo LogoInfo `json:"logo_info,omitempty"` // Watermark information
+ ImageUrls []string `json:"image_urls,omitempty"` // Image URLs for input
+ BinaryData []string `json:"binary_data_base64,omitempty"` // Base64 encoded binary data
+}
+
+func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.ImageRequest) (any, error) {
+ payload := imageRequestPayload{
+ ReqKey: request.Model,
+ Prompt: request.Prompt,
+ }
+ if request.ResponseFormat == "" || request.ResponseFormat == "url" {
+ payload.ReturnURL = true // Default to returning image URLs
+ }
+
+ if len(request.ExtraFields) > 0 {
+ if err := json.Unmarshal(request.ExtraFields, &payload); err != nil {
+ return nil, fmt.Errorf("failed to unmarshal extra fields: %w", err)
+ }
+ }
+
+ return payload, nil
+}
+
+func (a *Adaptor) ConvertRerankRequest(c *gin.Context, relayMode int, request dto.RerankRequest) (any, error) {
+ return nil, errors.New("not implemented")
+}
+
+func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.EmbeddingRequest) (any, error) {
+ return nil, errors.New("not implemented")
+}
+
+func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) {
+ return nil, errors.New("not implemented")
+}
+
+func (a *Adaptor) ConvertOpenAIResponsesRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.OpenAIResponsesRequest) (any, error) {
+ return nil, errors.New("not implemented")
+}
+
+func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error) {
+ fullRequestURL, err := a.GetRequestURL(info)
+ if err != nil {
+ return nil, fmt.Errorf("get request url failed: %w", err)
+ }
+ req, err := http.NewRequest(c.Request.Method, fullRequestURL, requestBody)
+ if err != nil {
+ return nil, fmt.Errorf("new request failed: %w", err)
+ }
+ err = Sign(c, req, info.ApiKey)
+ if err != nil {
+ return nil, fmt.Errorf("setup request header failed: %w", err)
+ }
+ resp, err := channel.DoRequest(c, req, info)
+ if err != nil {
+ return nil, fmt.Errorf("do request failed: %w", err)
+ }
+ return resp, nil
+}
+
+func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *types.NewAPIError) {
+ if info.RelayMode == relayconstant.RelayModeImagesGenerations {
+ usage, err = jimengImageHandler(c, resp, info)
+ } else if info.IsStream {
+ usage, err = openai.OaiStreamHandler(c, info, resp)
+ } else {
+ usage, err = openai.OpenaiHandler(c, info, resp)
+ }
+ return
+}
+
+func (a *Adaptor) GetModelList() []string {
+ return ModelList
+}
+
+func (a *Adaptor) GetChannelName() string {
+ return ChannelName
+}
diff --git a/relay/channel/jimeng/constants.go b/relay/channel/jimeng/constants.go
new file mode 100644
index 00000000000..0d1764e54d1
--- /dev/null
+++ b/relay/channel/jimeng/constants.go
@@ -0,0 +1,9 @@
+package jimeng
+
+const (
+ ChannelName = "jimeng"
+)
+
+var ModelList = []string{
+ "jimeng_high_aes_general_v21_L",
+}
diff --git a/relay/channel/jimeng/image.go b/relay/channel/jimeng/image.go
new file mode 100644
index 00000000000..3c6a1d991b8
--- /dev/null
+++ b/relay/channel/jimeng/image.go
@@ -0,0 +1,89 @@
+package jimeng
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "one-api/common"
+ "one-api/dto"
+ relaycommon "one-api/relay/common"
+ "one-api/types"
+
+ "github.com/gin-gonic/gin"
+)
+
+type ImageResponse struct {
+ Code int `json:"code"`
+ Message string `json:"message"`
+ Data struct {
+ BinaryDataBase64 []string `json:"binary_data_base64"`
+ ImageUrls []string `json:"image_urls"`
+ RephraseResult string `json:"rephraser_result"`
+ RequestID string `json:"request_id"`
+ // Other fields are omitted for brevity
+ } `json:"data"`
+ RequestID string `json:"request_id"`
+ Status int `json:"status"`
+ TimeElapsed string `json:"time_elapsed"`
+}
+
+func responseJimeng2OpenAIImage(_ *gin.Context, response *ImageResponse, info *relaycommon.RelayInfo) *dto.ImageResponse {
+ imageResponse := dto.ImageResponse{
+ Created: info.StartTime.Unix(),
+ }
+
+ for _, base64Data := range response.Data.BinaryDataBase64 {
+ imageResponse.Data = append(imageResponse.Data, dto.ImageData{
+ B64Json: base64Data,
+ })
+ }
+ for _, imageUrl := range response.Data.ImageUrls {
+ imageResponse.Data = append(imageResponse.Data, dto.ImageData{
+ Url: imageUrl,
+ })
+ }
+
+ return &imageResponse
+}
+
+// jimengImageHandler handles the Jimeng image generation response
+func jimengImageHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (*dto.Usage, *types.NewAPIError) {
+ var jimengResponse ImageResponse
+ responseBody, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return nil, types.NewError(err, types.ErrorCodeReadResponseBodyFailed)
+ }
+ common.CloseResponseBodyGracefully(resp)
+
+ err = json.Unmarshal(responseBody, &jimengResponse)
+ if err != nil {
+ return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
+ }
+
+ // Check if the response indicates an error
+ if jimengResponse.Code != 10000 {
+ return nil, types.WithOpenAIError(types.OpenAIError{
+ Message: jimengResponse.Message,
+ Type: "jimeng_error",
+ Param: "",
+ Code: fmt.Sprintf("%d", jimengResponse.Code),
+ }, resp.StatusCode)
+ }
+
+ // Convert Jimeng response to OpenAI format
+ fullTextResponse := responseJimeng2OpenAIImage(c, &jimengResponse, info)
+ jsonResponse, err := json.Marshal(fullTextResponse)
+ if err != nil {
+ return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
+ }
+
+ c.Writer.Header().Set("Content-Type", "application/json")
+ c.Writer.WriteHeader(resp.StatusCode)
+ _, err = c.Writer.Write(jsonResponse)
+ if err != nil {
+ return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
+ }
+
+ return &dto.Usage{}, nil
+}
diff --git a/relay/channel/jimeng/sign.go b/relay/channel/jimeng/sign.go
new file mode 100644
index 00000000000..c9db66301fb
--- /dev/null
+++ b/relay/channel/jimeng/sign.go
@@ -0,0 +1,176 @@
+package jimeng
+
+import (
+ "bytes"
+ "crypto/hmac"
+ "crypto/sha256"
+ "encoding/hex"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "github.com/gin-gonic/gin"
+ "io"
+ "net/http"
+ "net/url"
+ "one-api/common"
+ "sort"
+ "strings"
+ "time"
+)
+
+// SignRequestForJimeng 对即梦 API 请求进行签名,支持 http.Request 或 header+url+body 方式
+//func SignRequestForJimeng(req *http.Request, accessKey, secretKey string) error {
+// var bodyBytes []byte
+// var err error
+//
+// if req.Body != nil {
+// bodyBytes, err = io.ReadAll(req.Body)
+// if err != nil {
+// return fmt.Errorf("read request body failed: %w", err)
+// }
+// _ = req.Body.Close()
+// req.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) // rewind
+// } else {
+// bodyBytes = []byte{}
+// }
+//
+// return signJimengHeaders(&req.Header, req.Method, req.URL, bodyBytes, accessKey, secretKey)
+//}
+
+const HexPayloadHashKey = "HexPayloadHash"
+
+func SetPayloadHash(c *gin.Context, req any) error {
+ body, err := json.Marshal(req)
+ if err != nil {
+ return err
+ }
+ common.LogInfo(c, fmt.Sprintf("SetPayloadHash body: %s", body))
+ payloadHash := sha256.Sum256(body)
+ hexPayloadHash := hex.EncodeToString(payloadHash[:])
+ c.Set(HexPayloadHashKey, hexPayloadHash)
+ return nil
+}
+func getPayloadHash(c *gin.Context) string {
+ return c.GetString(HexPayloadHashKey)
+}
+
+func Sign(c *gin.Context, req *http.Request, apiKey string) error {
+ header := req.Header
+
+ var bodyBytes []byte
+ var err error
+
+ if req.Body != nil {
+ bodyBytes, err = io.ReadAll(req.Body)
+ if err != nil {
+ return err
+ }
+ _ = req.Body.Close()
+ req.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) // Rewind
+ }
+
+ payloadHash := sha256.Sum256(bodyBytes)
+ hexPayloadHash := hex.EncodeToString(payloadHash[:])
+
+ method := c.Request.Method
+ u := req.URL
+ keyParts := strings.Split(apiKey, "|")
+ if len(keyParts) != 2 {
+ return errors.New("invalid api key format for jimeng: expected 'ak|sk'")
+ }
+ accessKey := strings.TrimSpace(keyParts[0])
+ secretKey := strings.TrimSpace(keyParts[1])
+ t := time.Now().UTC()
+ xDate := t.Format("20060102T150405Z")
+ shortDate := t.Format("20060102")
+
+ host := u.Host
+ header.Set("Host", host)
+ header.Set("X-Date", xDate)
+ header.Set("X-Content-Sha256", hexPayloadHash)
+
+ // Sort and encode query parameters to create canonical query string
+ queryParams := u.Query()
+ sortedKeys := make([]string, 0, len(queryParams))
+ for k := range queryParams {
+ sortedKeys = append(sortedKeys, k)
+ }
+ sort.Strings(sortedKeys)
+ var queryParts []string
+ for _, k := range sortedKeys {
+ values := queryParams[k]
+ sort.Strings(values)
+ for _, v := range values {
+ queryParts = append(queryParts, fmt.Sprintf("%s=%s", url.QueryEscape(k), url.QueryEscape(v)))
+ }
+ }
+ canonicalQueryString := strings.Join(queryParts, "&")
+
+ headersToSign := map[string]string{
+ "host": host,
+ "x-date": xDate,
+ "x-content-sha256": hexPayloadHash,
+ }
+ if header.Get("Content-Type") == "" {
+ header.Set("Content-Type", "application/json")
+ }
+ headersToSign["content-type"] = header.Get("Content-Type")
+
+ var signedHeaderKeys []string
+ for k := range headersToSign {
+ signedHeaderKeys = append(signedHeaderKeys, k)
+ }
+ sort.Strings(signedHeaderKeys)
+
+ var canonicalHeaders strings.Builder
+ for _, k := range signedHeaderKeys {
+ canonicalHeaders.WriteString(k)
+ canonicalHeaders.WriteString(":")
+ canonicalHeaders.WriteString(strings.TrimSpace(headersToSign[k]))
+ canonicalHeaders.WriteString("\n")
+ }
+ signedHeaders := strings.Join(signedHeaderKeys, ";")
+
+ canonicalRequest := fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s",
+ method,
+ u.Path,
+ canonicalQueryString,
+ canonicalHeaders.String(),
+ signedHeaders,
+ hexPayloadHash,
+ )
+
+ hashedCanonicalRequest := sha256.Sum256([]byte(canonicalRequest))
+ hexHashedCanonicalRequest := hex.EncodeToString(hashedCanonicalRequest[:])
+
+ region := "cn-north-1"
+ serviceName := "cv"
+ credentialScope := fmt.Sprintf("%s/%s/%s/request", shortDate, region, serviceName)
+ stringToSign := fmt.Sprintf("HMAC-SHA256\n%s\n%s\n%s",
+ xDate,
+ credentialScope,
+ hexHashedCanonicalRequest,
+ )
+
+ kDate := hmacSHA256([]byte(secretKey), []byte(shortDate))
+ kRegion := hmacSHA256(kDate, []byte(region))
+ kService := hmacSHA256(kRegion, []byte(serviceName))
+ kSigning := hmacSHA256(kService, []byte("request"))
+ signature := hex.EncodeToString(hmacSHA256(kSigning, []byte(stringToSign)))
+
+ authorization := fmt.Sprintf("HMAC-SHA256 Credential=%s/%s, SignedHeaders=%s, Signature=%s",
+ accessKey,
+ credentialScope,
+ signedHeaders,
+ signature,
+ )
+ header.Set("Authorization", authorization)
+ return nil
+}
+
+// hmacSHA256 计算 HMAC-SHA256
+func hmacSHA256(key []byte, data []byte) []byte {
+ h := hmac.New(sha256.New, key)
+ h.Write(data)
+ return h.Sum(nil)
+}
diff --git a/relay/channel/jina/adaptor.go b/relay/channel/jina/adaptor.go
index 85b6a83f0d9..408a5c6e467 100644
--- a/relay/channel/jina/adaptor.go
+++ b/relay/channel/jina/adaptor.go
@@ -11,6 +11,7 @@ import (
relaycommon "one-api/relay/common"
"one-api/relay/common_handler"
"one-api/relay/constant"
+ "one-api/types"
"github.com/gin-gonic/gin"
)
@@ -73,11 +74,11 @@ func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.Rela
return request, nil
}
-func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *dto.OpenAIErrorWithStatusCode) {
+func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *types.NewAPIError) {
if info.RelayMode == constant.RelayModeRerank {
- err, usage = common_handler.RerankHandler(c, info, resp)
+ usage, err = common_handler.RerankHandler(c, info, resp)
} else if info.RelayMode == constant.RelayModeEmbeddings {
- err, usage = openai.OpenaiHandler(c, resp, info)
+ usage, err = openai.OpenaiHandler(c, info, resp)
}
return
}
diff --git a/relay/channel/mistral/adaptor.go b/relay/channel/mistral/adaptor.go
index 44f57e61cfe..434a1031c94 100644
--- a/relay/channel/mistral/adaptor.go
+++ b/relay/channel/mistral/adaptor.go
@@ -8,6 +8,7 @@ import (
"one-api/relay/channel"
"one-api/relay/channel/openai"
relaycommon "one-api/relay/common"
+ "one-api/types"
"github.com/gin-gonic/gin"
)
@@ -69,11 +70,11 @@ func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, request
return channel.DoApiRequest(a, c, info, requestBody)
}
-func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *dto.OpenAIErrorWithStatusCode) {
+func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *types.NewAPIError) {
if info.IsStream {
- err, usage = openai.OaiStreamHandler(c, resp, info)
+ usage, err = openai.OaiStreamHandler(c, info, resp)
} else {
- err, usage = openai.OpenaiHandler(c, resp, info)
+ usage, err = openai.OpenaiHandler(c, info, resp)
}
return
}
diff --git a/relay/channel/mokaai/adaptor.go b/relay/channel/mokaai/adaptor.go
index b889f225426..b0b54b0c5a5 100644
--- a/relay/channel/mokaai/adaptor.go
+++ b/relay/channel/mokaai/adaptor.go
@@ -9,6 +9,7 @@ import (
"one-api/relay/channel"
relaycommon "one-api/relay/common"
"one-api/relay/constant"
+ "one-api/types"
"strings"
"github.com/gin-gonic/gin"
@@ -84,11 +85,11 @@ func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, request
return channel.DoApiRequest(a, c, info, requestBody)
}
-func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *dto.OpenAIErrorWithStatusCode) {
+func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *types.NewAPIError) {
switch info.RelayMode {
case constant.RelayModeEmbeddings:
- err, usage = mokaEmbeddingHandler(c, resp)
+ return mokaEmbeddingHandler(c, info, resp)
default:
// err, usage = mokaHandler(c, resp)
diff --git a/relay/channel/mokaai/relay-mokaai.go b/relay/channel/mokaai/relay-mokaai.go
index 645475ddc95..78f96d6d6e0 100644
--- a/relay/channel/mokaai/relay-mokaai.go
+++ b/relay/channel/mokaai/relay-mokaai.go
@@ -2,12 +2,14 @@ package mokaai
import (
"encoding/json"
- "github.com/gin-gonic/gin"
"io"
"net/http"
"one-api/common"
"one-api/dto"
- "one-api/service"
+ relaycommon "one-api/relay/common"
+ "one-api/types"
+
+ "github.com/gin-gonic/gin"
)
func embeddingRequestOpenAI2Moka(request dto.GeneralOpenAIRequest) *dto.EmbeddingRequest {
@@ -48,16 +50,16 @@ func embeddingResponseMoka2OpenAI(response *dto.EmbeddingResponse) *dto.OpenAIEm
return &openAIEmbeddingResponse
}
-func mokaEmbeddingHandler(c *gin.Context, resp *http.Response) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
+func mokaEmbeddingHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Response) (*dto.Usage, *types.NewAPIError) {
var baiduResponse dto.EmbeddingResponse
responseBody, err := io.ReadAll(resp.Body)
if err != nil {
- return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
+ return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
}
common.CloseResponseBodyGracefully(resp)
err = json.Unmarshal(responseBody, &baiduResponse)
if err != nil {
- return service.OpenAIErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
+ return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
}
// if baiduResponse.ErrorMsg != "" {
// return &dto.OpenAIErrorWithStatusCode{
@@ -69,12 +71,12 @@ func mokaEmbeddingHandler(c *gin.Context, resp *http.Response) (*dto.OpenAIError
// }, nil
// }
fullTextResponse := embeddingResponseMoka2OpenAI(&baiduResponse)
- jsonResponse, err := json.Marshal(fullTextResponse)
+ jsonResponse, err := common.Marshal(fullTextResponse)
if err != nil {
- return service.OpenAIErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil
+ return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
}
c.Writer.Header().Set("Content-Type", "application/json")
c.Writer.WriteHeader(resp.StatusCode)
- _, err = c.Writer.Write(jsonResponse)
- return nil, &fullTextResponse.Usage
+ common.IOCopyBytesGracefully(c, resp, jsonResponse)
+ return &fullTextResponse.Usage, nil
}
diff --git a/relay/channel/ollama/adaptor.go b/relay/channel/ollama/adaptor.go
index 1806931102e..b9e304fcc27 100644
--- a/relay/channel/ollama/adaptor.go
+++ b/relay/channel/ollama/adaptor.go
@@ -9,6 +9,7 @@ import (
"one-api/relay/channel/openai"
relaycommon "one-api/relay/common"
relayconstant "one-api/relay/constant"
+ "one-api/types"
"github.com/gin-gonic/gin"
)
@@ -74,14 +75,14 @@ func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, request
return channel.DoApiRequest(a, c, info, requestBody)
}
-func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *dto.OpenAIErrorWithStatusCode) {
+func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *types.NewAPIError) {
if info.IsStream {
- err, usage = openai.OaiStreamHandler(c, resp, info)
+ usage, err = openai.OaiStreamHandler(c, info, resp)
} else {
if info.RelayMode == relayconstant.RelayModeEmbeddings {
- err, usage = ollamaEmbeddingHandler(c, resp, info.PromptTokens, info.UpstreamModelName, info.RelayMode)
+ usage, err = ollamaEmbeddingHandler(c, info, resp)
} else {
- err, usage = openai.OpenaiHandler(c, resp, info)
+ usage, err = openai.OpenaiHandler(c, info, resp)
}
}
return
diff --git a/relay/channel/ollama/relay-ollama.go b/relay/channel/ollama/relay-ollama.go
index bf7501e58a8..295349e3154 100644
--- a/relay/channel/ollama/relay-ollama.go
+++ b/relay/channel/ollama/relay-ollama.go
@@ -1,15 +1,17 @@
package ollama
import (
- "encoding/json"
"fmt"
- "github.com/gin-gonic/gin"
"io"
"net/http"
"one-api/common"
"one-api/dto"
+ relaycommon "one-api/relay/common"
"one-api/service"
+ "one-api/types"
"strings"
+
+ "github.com/gin-gonic/gin"
)
func requestOpenAI2Ollama(request dto.GeneralOpenAIRequest) (*OllamaRequest, error) {
@@ -82,19 +84,19 @@ func requestOpenAI2Embeddings(request dto.EmbeddingRequest) *OllamaEmbeddingRequ
}
}
-func ollamaEmbeddingHandler(c *gin.Context, resp *http.Response, promptTokens int, model string, relayMode int) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
+func ollamaEmbeddingHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Response) (*dto.Usage, *types.NewAPIError) {
var ollamaEmbeddingResponse OllamaEmbeddingResponse
responseBody, err := io.ReadAll(resp.Body)
if err != nil {
- return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
+ return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
}
common.CloseResponseBodyGracefully(resp)
- err = json.Unmarshal(responseBody, &ollamaEmbeddingResponse)
+ err = common.Unmarshal(responseBody, &ollamaEmbeddingResponse)
if err != nil {
- return service.OpenAIErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
+ return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
}
if ollamaEmbeddingResponse.Error != "" {
- return service.OpenAIErrorWrapper(err, "ollama_error", resp.StatusCode), nil
+ return nil, types.NewError(fmt.Errorf("ollama error: %s", ollamaEmbeddingResponse.Error), types.ErrorCodeBadResponseBody)
}
flattenedEmbeddings := flattenEmbeddings(ollamaEmbeddingResponse.Embedding)
data := make([]dto.OpenAIEmbeddingResponseItem, 0, 1)
@@ -103,22 +105,22 @@ func ollamaEmbeddingHandler(c *gin.Context, resp *http.Response, promptTokens in
Object: "embedding",
})
usage := &dto.Usage{
- TotalTokens: promptTokens,
+ TotalTokens: info.PromptTokens,
CompletionTokens: 0,
- PromptTokens: promptTokens,
+ PromptTokens: info.PromptTokens,
}
embeddingResponse := &dto.OpenAIEmbeddingResponse{
Object: "list",
Data: data,
- Model: model,
+ Model: info.UpstreamModelName,
Usage: *usage,
}
- doResponseBody, err := json.Marshal(embeddingResponse)
+ doResponseBody, err := common.Marshal(embeddingResponse)
if err != nil {
- return service.OpenAIErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil
+ return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
}
common.IOCopyBytesGracefully(c, resp, doResponseBody)
- return nil, usage
+ return usage, nil
}
func flattenEmbeddings(embeddings [][]float64) []float64 {
diff --git a/relay/channel/openai/adaptor.go b/relay/channel/openai/adaptor.go
index 367dbc47107..efd228781d0 100644
--- a/relay/channel/openai/adaptor.go
+++ b/relay/channel/openai/adaptor.go
@@ -22,6 +22,7 @@ import (
"one-api/relay/common_handler"
relayconstant "one-api/relay/constant"
"one-api/service"
+ "one-api/types"
"path/filepath"
"strings"
@@ -34,9 +35,9 @@ type Adaptor struct {
}
func (a *Adaptor) ConvertClaudeRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.ClaudeRequest) (any, error) {
- if !strings.Contains(request.Model, "claude") {
- return nil, fmt.Errorf("you are using openai channel type with path /v1/messages, only claude model supported convert, but got %s", request.Model)
- }
+ //if !strings.Contains(request.Model, "claude") {
+ // return nil, fmt.Errorf("you are using openai channel type with path /v1/messages, only claude model supported convert, but got %s", request.Model)
+ //}
aiRequest, err := service.ClaudeToOpenAIRequest(*request, info)
if err != nil {
return nil, err
@@ -421,31 +422,31 @@ func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, request
}
}
-func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *dto.OpenAIErrorWithStatusCode) {
+func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *types.NewAPIError) {
switch info.RelayMode {
case relayconstant.RelayModeRealtime:
err, usage = OpenaiRealtimeHandler(c, info)
case relayconstant.RelayModeAudioSpeech:
- err, usage = OpenaiTTSHandler(c, resp, info)
+ usage = OpenaiTTSHandler(c, resp, info)
case relayconstant.RelayModeAudioTranslation:
fallthrough
case relayconstant.RelayModeAudioTranscription:
err, usage = OpenaiSTTHandler(c, resp, info, a.ResponseFormat)
case relayconstant.RelayModeImagesGenerations, relayconstant.RelayModeImagesEdits:
- err, usage = OpenaiHandlerWithUsage(c, resp, info)
+ usage, err = OpenaiHandlerWithUsage(c, info, resp)
case relayconstant.RelayModeRerank:
- err, usage = common_handler.RerankHandler(c, info, resp)
+ usage, err = common_handler.RerankHandler(c, info, resp)
case relayconstant.RelayModeResponses:
if info.IsStream {
- err, usage = OaiResponsesStreamHandler(c, resp, info)
+ usage, err = OaiResponsesStreamHandler(c, info, resp)
} else {
- err, usage = OaiResponsesHandler(c, resp, info)
+ usage, err = OaiResponsesHandler(c, info, resp)
}
default:
if info.IsStream {
- err, usage = OaiStreamHandler(c, resp, info)
+ usage, err = OaiStreamHandler(c, info, resp)
} else {
- err, usage = OpenaiHandler(c, resp, info)
+ usage, err = OpenaiHandler(c, info, resp)
}
}
return
diff --git a/relay/channel/openai/relay-openai.go b/relay/channel/openai/relay-openai.go
index 6aa7327432d..bfe8bcd395b 100644
--- a/relay/channel/openai/relay-openai.go
+++ b/relay/channel/openai/relay-openai.go
@@ -17,6 +17,8 @@ import (
"path/filepath"
"strings"
+ "one-api/types"
+
"github.com/bytedance/gopkg/util/gopool"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
@@ -104,10 +106,10 @@ func sendStreamData(c *gin.Context, info *relaycommon.RelayInfo, data string, fo
return helper.ObjectData(c, lastStreamResponse)
}
-func OaiStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
+func OaiStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Response) (*dto.Usage, *types.NewAPIError) {
if resp == nil || resp.Body == nil {
common.LogError(c, "invalid response or response body")
- return service.OpenAIErrorWrapper(fmt.Errorf("invalid response"), "invalid_response", http.StatusInternalServerError), nil
+ return nil, types.NewError(fmt.Errorf("invalid response"), types.ErrorCodeBadResponse)
}
defer common.CloseResponseBodyGracefully(resp)
@@ -177,26 +179,23 @@ func OaiStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.Rel
handleFinalResponse(c, info, lastStreamData, responseId, createAt, model, systemFingerprint, usage, containStreamUsage)
- return nil, usage
+ return usage, nil
}
-func OpenaiHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
+func OpenaiHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Response) (*dto.Usage, *types.NewAPIError) {
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
+ return nil, types.NewError(err, types.ErrorCodeReadResponseBodyFailed)
}
- err = common.UnmarshalJson(responseBody, &simpleResponse)
+ err = common.Unmarshal(responseBody, &simpleResponse)
if err != nil {
- return service.OpenAIErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
+ return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
}
if simpleResponse.Error != nil && simpleResponse.Error.Type != "" {
- return &dto.OpenAIErrorWithStatusCode{
- Error: *simpleResponse.Error,
- StatusCode: resp.StatusCode,
- }, nil
+ return nil, types.WithOpenAIError(*simpleResponse.Error, resp.StatusCode)
}
forceFormat := false
@@ -220,28 +219,28 @@ func OpenaiHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayI
switch info.RelayFormat {
case relaycommon.RelayFormatOpenAI:
if forceFormat {
- responseBody, err = common.EncodeJson(simpleResponse)
+ responseBody, err = common.Marshal(simpleResponse)
if err != nil {
- return service.OpenAIErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil
+ return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
}
} else {
break
}
case relaycommon.RelayFormatClaude:
claudeResp := service.ResponseOpenAI2Claude(&simpleResponse, info)
- claudeRespStr, err := common.EncodeJson(claudeResp)
+ claudeRespStr, err := common.Marshal(claudeResp)
if err != nil {
- return service.OpenAIErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil
+ return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
}
responseBody = claudeRespStr
}
common.IOCopyBytesGracefully(c, resp, responseBody)
- return nil, &simpleResponse.Usage
+ return &simpleResponse.Usage, nil
}
-func OpenaiTTSHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
+func OpenaiTTSHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) *dto.Usage {
// the status code has been judged before, if there is a body reading failure,
// it should be regarded as a non-recoverable error, so it should not return err for external retry.
// Analogous to nginx's load balancing, it will only retry if it can't be requested or
@@ -261,20 +260,20 @@ func OpenaiTTSHandler(c *gin.Context, resp *http.Response, info *relaycommon.Rel
if err != nil {
common.LogError(c, err.Error())
}
- return nil, usage
+ return usage
}
-func OpenaiSTTHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo, responseFormat string) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
+func OpenaiSTTHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo, responseFormat string) (*types.NewAPIError, *dto.Usage) {
defer common.CloseResponseBodyGracefully(resp)
// count tokens by audio file duration
audioTokens, err := countAudioTokens(c)
if err != nil {
- return service.OpenAIErrorWrapper(err, "count_audio_tokens_failed", http.StatusInternalServerError), nil
+ return types.NewError(err, types.ErrorCodeCountTokenFailed), nil
}
responseBody, err := io.ReadAll(resp.Body)
if err != nil {
- return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
+ return types.NewError(err, types.ErrorCodeReadResponseBodyFailed), nil
}
// 写入新的 response body
common.IOCopyBytesGracefully(c, resp, responseBody)
@@ -328,9 +327,9 @@ func countAudioTokens(c *gin.Context) (int, error) {
return int(math.Round(math.Ceil(duration) / 60.0 * 1000)), nil // 1 minute 相当于 1k tokens
}
-func OpenaiRealtimeHandler(c *gin.Context, info *relaycommon.RelayInfo) (*dto.OpenAIErrorWithStatusCode, *dto.RealtimeUsage) {
+func OpenaiRealtimeHandler(c *gin.Context, info *relaycommon.RelayInfo) (*types.NewAPIError, *dto.RealtimeUsage) {
if info == nil || info.ClientWs == nil || info.TargetWs == nil {
- return service.OpenAIErrorWrapper(fmt.Errorf("invalid websocket connection"), "invalid_connection", http.StatusBadRequest), nil
+ return types.NewError(fmt.Errorf("invalid websocket connection"), types.ErrorCodeBadResponse), nil
}
info.IsStream = true
@@ -368,7 +367,7 @@ func OpenaiRealtimeHandler(c *gin.Context, info *relaycommon.RelayInfo) (*dto.Op
}
realtimeEvent := &dto.RealtimeEvent{}
- err = common.UnmarshalJson(message, realtimeEvent)
+ err = common.Unmarshal(message, realtimeEvent)
if err != nil {
errChan <- fmt.Errorf("error unmarshalling message: %v", err)
return
@@ -428,7 +427,7 @@ func OpenaiRealtimeHandler(c *gin.Context, info *relaycommon.RelayInfo) (*dto.Op
}
info.SetFirstResponseTime()
realtimeEvent := &dto.RealtimeEvent{}
- err = common.UnmarshalJson(message, realtimeEvent)
+ err = common.Unmarshal(message, realtimeEvent)
if err != nil {
errChan <- fmt.Errorf("error unmarshalling message: %v", err)
return
@@ -553,18 +552,18 @@ func preConsumeUsage(ctx *gin.Context, info *relaycommon.RelayInfo, usage *dto.R
return err
}
-func OpenaiHandlerWithUsage(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
+func OpenaiHandlerWithUsage(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Response) (*dto.Usage, *types.NewAPIError) {
defer common.CloseResponseBodyGracefully(resp)
responseBody, err := io.ReadAll(resp.Body)
if err != nil {
- return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
+ return nil, types.NewError(err, types.ErrorCodeReadResponseBodyFailed)
}
var usageResp dto.SimpleResponse
- err = common.UnmarshalJson(responseBody, &usageResp)
+ err = common.Unmarshal(responseBody, &usageResp)
if err != nil {
- return service.OpenAIErrorWrapper(err, "parse_response_body_failed", http.StatusInternalServerError), nil
+ return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
}
// 写入新的 response body
@@ -584,5 +583,5 @@ func OpenaiHandlerWithUsage(c *gin.Context, resp *http.Response, info *relaycomm
usageResp.PromptTokensDetails.ImageTokens += usageResp.InputTokensDetails.ImageTokens
usageResp.PromptTokensDetails.TextTokens += usageResp.InputTokensDetails.TextTokens
}
- return nil, &usageResp.Usage
+ return &usageResp.Usage, nil
}
diff --git a/relay/channel/openai/relay_responses.go b/relay/channel/openai/relay_responses.go
index 7f426c33e1b..d9dd96b9005 100644
--- a/relay/channel/openai/relay_responses.go
+++ b/relay/channel/openai/relay_responses.go
@@ -9,33 +9,27 @@ import (
relaycommon "one-api/relay/common"
"one-api/relay/helper"
"one-api/service"
+ "one-api/types"
"strings"
"github.com/gin-gonic/gin"
)
-func OaiResponsesHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
+func OaiResponsesHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Response) (*dto.Usage, *types.NewAPIError) {
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
+ return nil, types.NewError(err, types.ErrorCodeReadResponseBodyFailed)
}
- err = common.UnmarshalJson(responseBody, &responsesResponse)
+ err = common.Unmarshal(responseBody, &responsesResponse)
if err != nil {
- return service.OpenAIErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
+ return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
}
if responsesResponse.Error != nil {
- return &dto.OpenAIErrorWithStatusCode{
- Error: dto.OpenAIError{
- Message: responsesResponse.Error.Message,
- Type: "openai_error",
- Code: responsesResponse.Error.Code,
- },
- StatusCode: resp.StatusCode,
- }, nil
+ return nil, types.WithOpenAIError(*responsesResponse.Error, resp.StatusCode)
}
// 写入新的 response body
@@ -48,15 +42,15 @@ func OaiResponsesHandler(c *gin.Context, resp *http.Response, info *relaycommon.
usage.TotalTokens = responsesResponse.Usage.TotalTokens
// 解析 Tools 用量
for _, tool := range responsesResponse.Tools {
- info.ResponsesUsageInfo.BuiltInTools[tool.Type].CallCount++
+ info.ResponsesUsageInfo.BuiltInTools[common.Interface2String(tool["type"])].CallCount++
}
- return nil, &usage
+ return &usage, nil
}
-func OaiResponsesStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
+func OaiResponsesStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Response) (*dto.Usage, *types.NewAPIError) {
if resp == nil || resp.Body == nil {
common.LogError(c, "invalid response or response body")
- return service.OpenAIErrorWrapper(fmt.Errorf("invalid response"), "invalid_response", http.StatusInternalServerError), nil
+ return nil, types.NewError(fmt.Errorf("invalid response"), types.ErrorCodeBadResponse)
}
var usage = &dto.Usage{}
@@ -99,5 +93,5 @@ func OaiResponsesStreamHandler(c *gin.Context, resp *http.Response, info *relayc
}
}
- return nil, usage
+ return usage, nil
}
diff --git a/relay/channel/palm/adaptor.go b/relay/channel/palm/adaptor.go
index aee4a3076d8..a60dc4b2802 100644
--- a/relay/channel/palm/adaptor.go
+++ b/relay/channel/palm/adaptor.go
@@ -9,6 +9,7 @@ import (
"one-api/relay/channel"
relaycommon "one-api/relay/common"
"one-api/service"
+ "one-api/types"
"github.com/gin-gonic/gin"
)
@@ -70,13 +71,13 @@ func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, request
return channel.DoApiRequest(a, c, info, requestBody)
}
-func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *dto.OpenAIErrorWithStatusCode) {
+func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *types.NewAPIError) {
if info.IsStream {
var responseText string
err, responseText = palmStreamHandler(c, resp)
usage = service.ResponseText2Usage(responseText, info.UpstreamModelName, info.PromptTokens)
} else {
- err, usage = palmHandler(c, resp, info.PromptTokens, info.UpstreamModelName)
+ usage, err = palmHandler(c, info, resp)
}
return
}
diff --git a/relay/channel/palm/relay-palm.go b/relay/channel/palm/relay-palm.go
index 44c60713c21..4db31573949 100644
--- a/relay/channel/palm/relay-palm.go
+++ b/relay/channel/palm/relay-palm.go
@@ -2,14 +2,17 @@ package palm
import (
"encoding/json"
- "github.com/gin-gonic/gin"
"io"
"net/http"
"one-api/common"
"one-api/constant"
"one-api/dto"
+ relaycommon "one-api/relay/common"
"one-api/relay/helper"
"one-api/service"
+ "one-api/types"
+
+ "github.com/gin-gonic/gin"
)
// https://developers.generativeai.google/api/rest/generativelanguage/models/generateMessage#request-body
@@ -70,7 +73,7 @@ func streamResponsePaLM2OpenAI(palmResponse *PaLMChatResponse) *dto.ChatCompleti
return &response
}
-func palmStreamHandler(c *gin.Context, resp *http.Response) (*dto.OpenAIErrorWithStatusCode, string) {
+func palmStreamHandler(c *gin.Context, resp *http.Response) (*types.NewAPIError, string) {
responseText := ""
responseId := helper.GetResponseID(c)
createdTime := common.GetTimestamp()
@@ -121,42 +124,39 @@ func palmStreamHandler(c *gin.Context, resp *http.Response) (*dto.OpenAIErrorWit
return nil, responseText
}
-func palmHandler(c *gin.Context, resp *http.Response, promptTokens int, model string) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
+func palmHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Response) (*dto.Usage, *types.NewAPIError) {
responseBody, err := io.ReadAll(resp.Body)
if err != nil {
- return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
+ return nil, types.NewError(err, types.ErrorCodeReadResponseBodyFailed)
}
common.CloseResponseBodyGracefully(resp)
var palmResponse PaLMChatResponse
err = json.Unmarshal(responseBody, &palmResponse)
if err != nil {
- return service.OpenAIErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
+ return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
}
if palmResponse.Error.Code != 0 || len(palmResponse.Candidates) == 0 {
- return &dto.OpenAIErrorWithStatusCode{
- Error: dto.OpenAIError{
- Message: palmResponse.Error.Message,
- Type: palmResponse.Error.Status,
- Param: "",
- Code: palmResponse.Error.Code,
- },
- StatusCode: resp.StatusCode,
- }, nil
+ return nil, types.WithOpenAIError(types.OpenAIError{
+ Message: palmResponse.Error.Message,
+ Type: palmResponse.Error.Status,
+ Param: "",
+ Code: palmResponse.Error.Code,
+ }, resp.StatusCode)
}
fullTextResponse := responsePaLM2OpenAI(&palmResponse)
- completionTokens := service.CountTextToken(palmResponse.Candidates[0].Content, model)
+ completionTokens := service.CountTextToken(palmResponse.Candidates[0].Content, info.UpstreamModelName)
usage := dto.Usage{
- PromptTokens: promptTokens,
+ PromptTokens: info.PromptTokens,
CompletionTokens: completionTokens,
- TotalTokens: promptTokens + completionTokens,
+ TotalTokens: info.PromptTokens + completionTokens,
}
fullTextResponse.Usage = usage
- jsonResponse, err := json.Marshal(fullTextResponse)
+ jsonResponse, err := common.Marshal(fullTextResponse)
if err != nil {
- return service.OpenAIErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil
+ return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
}
c.Writer.Header().Set("Content-Type", "application/json")
c.Writer.WriteHeader(resp.StatusCode)
- _, err = c.Writer.Write(jsonResponse)
- return nil, &usage
+ common.IOCopyBytesGracefully(c, resp, jsonResponse)
+ return &usage, nil
}
diff --git a/relay/channel/perplexity/adaptor.go b/relay/channel/perplexity/adaptor.go
index ca20650348e..19830aca435 100644
--- a/relay/channel/perplexity/adaptor.go
+++ b/relay/channel/perplexity/adaptor.go
@@ -9,6 +9,7 @@ import (
"one-api/relay/channel"
"one-api/relay/channel/openai"
relaycommon "one-api/relay/common"
+ "one-api/types"
"github.com/gin-gonic/gin"
)
@@ -73,11 +74,11 @@ func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, request
return channel.DoApiRequest(a, c, info, requestBody)
}
-func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *dto.OpenAIErrorWithStatusCode) {
+func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *types.NewAPIError) {
if info.IsStream {
- err, usage = openai.OaiStreamHandler(c, resp, info)
+ usage, err = openai.OaiStreamHandler(c, info, resp)
} else {
- err, usage = openai.OpenaiHandler(c, resp, info)
+ usage, err = openai.OpenaiHandler(c, info, resp)
}
return
}
diff --git a/relay/channel/siliconflow/adaptor.go b/relay/channel/siliconflow/adaptor.go
index 89236ea3298..63c1c84d756 100644
--- a/relay/channel/siliconflow/adaptor.go
+++ b/relay/channel/siliconflow/adaptor.go
@@ -10,6 +10,7 @@ import (
"one-api/relay/channel/openai"
relaycommon "one-api/relay/common"
"one-api/relay/constant"
+ "one-api/types"
"github.com/gin-gonic/gin"
)
@@ -76,20 +77,20 @@ func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.Rela
return request, nil
}
-func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *dto.OpenAIErrorWithStatusCode) {
+func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *types.NewAPIError) {
switch info.RelayMode {
case constant.RelayModeRerank:
- err, usage = siliconflowRerankHandler(c, resp)
+ usage, err = siliconflowRerankHandler(c, info, resp)
case constant.RelayModeCompletions:
fallthrough
case constant.RelayModeChatCompletions:
if info.IsStream {
- err, usage = openai.OaiStreamHandler(c, resp, info)
+ usage, err = openai.OaiStreamHandler(c, info, resp)
} else {
- err, usage = openai.OpenaiHandler(c, resp, info)
+ usage, err = openai.OpenaiHandler(c, info, resp)
}
case constant.RelayModeEmbeddings:
- err, usage = openai.OpenaiHandler(c, resp, info)
+ usage, err = openai.OpenaiHandler(c, info, resp)
}
return
}
diff --git a/relay/channel/siliconflow/relay-siliconflow.go b/relay/channel/siliconflow/relay-siliconflow.go
index a52ebfdaef4..fabaf9c634e 100644
--- a/relay/channel/siliconflow/relay-siliconflow.go
+++ b/relay/channel/siliconflow/relay-siliconflow.go
@@ -2,24 +2,26 @@ package siliconflow
import (
"encoding/json"
- "github.com/gin-gonic/gin"
"io"
"net/http"
"one-api/common"
"one-api/dto"
- "one-api/service"
+ relaycommon "one-api/relay/common"
+ "one-api/types"
+
+ "github.com/gin-gonic/gin"
)
-func siliconflowRerankHandler(c *gin.Context, resp *http.Response) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
+func siliconflowRerankHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Response) (*dto.Usage, *types.NewAPIError) {
responseBody, err := io.ReadAll(resp.Body)
if err != nil {
- return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
+ return nil, types.NewError(err, types.ErrorCodeReadResponseBodyFailed)
}
common.CloseResponseBodyGracefully(resp)
var siliconflowResp SFRerankResponse
err = json.Unmarshal(responseBody, &siliconflowResp)
if err != nil {
- return service.OpenAIErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
+ return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
}
usage := &dto.Usage{
PromptTokens: siliconflowResp.Meta.Tokens.InputTokens,
@@ -33,10 +35,10 @@ func siliconflowRerankHandler(c *gin.Context, resp *http.Response) (*dto.OpenAIE
jsonResponse, err := json.Marshal(rerankResp)
if err != nil {
- return service.OpenAIErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil
+ return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
}
c.Writer.Header().Set("Content-Type", "application/json")
c.Writer.WriteHeader(resp.StatusCode)
- _, err = c.Writer.Write(jsonResponse)
- return nil, usage
+ common.IOCopyBytesGracefully(c, resp, jsonResponse)
+ return usage, nil
}
diff --git a/relay/channel/tencent/adaptor.go b/relay/channel/tencent/adaptor.go
index 7ea3aae7948..520276a7eca 100644
--- a/relay/channel/tencent/adaptor.go
+++ b/relay/channel/tencent/adaptor.go
@@ -6,10 +6,11 @@ import (
"io"
"net/http"
"one-api/common"
+ "one-api/constant"
"one-api/dto"
"one-api/relay/channel"
relaycommon "one-api/relay/common"
- "one-api/service"
+ "one-api/types"
"strconv"
"strings"
@@ -63,7 +64,7 @@ func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayIn
if request == nil {
return nil, errors.New("request is nil")
}
- apiKey := c.Request.Header.Get("Authorization")
+ apiKey := common.GetContextKeyString(c, constant.ContextKeyChannelKey)
apiKey = strings.TrimPrefix(apiKey, "Bearer ")
appId, secretId, secretKey, err := parseTencentConfig(apiKey)
a.AppID = appId
@@ -94,13 +95,11 @@ func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, request
return channel.DoApiRequest(a, c, info, requestBody)
}
-func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *dto.OpenAIErrorWithStatusCode) {
+func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *types.NewAPIError) {
if info.IsStream {
- var responseText string
- err, responseText = tencentStreamHandler(c, resp)
- usage = service.ResponseText2Usage(responseText, info.UpstreamModelName, info.PromptTokens)
+ usage, err = tencentStreamHandler(c, info, resp)
} else {
- err, usage = tencentHandler(c, resp)
+ usage, err = tencentHandler(c, info, resp)
}
return
}
diff --git a/relay/channel/tencent/relay-tencent.go b/relay/channel/tencent/relay-tencent.go
index a7106a88c8d..c3d96c49a3f 100644
--- a/relay/channel/tencent/relay-tencent.go
+++ b/relay/channel/tencent/relay-tencent.go
@@ -8,17 +8,20 @@ import (
"encoding/json"
"errors"
"fmt"
- "github.com/gin-gonic/gin"
"io"
"net/http"
"one-api/common"
"one-api/constant"
"one-api/dto"
+ relaycommon "one-api/relay/common"
"one-api/relay/helper"
"one-api/service"
+ "one-api/types"
"strconv"
"strings"
"time"
+
+ "github.com/gin-gonic/gin"
)
// https://cloud.tencent.com/document/product/1729/97732
@@ -86,7 +89,7 @@ func streamResponseTencent2OpenAI(TencentResponse *TencentChatResponse) *dto.Cha
return &response
}
-func tencentStreamHandler(c *gin.Context, resp *http.Response) (*dto.OpenAIErrorWithStatusCode, string) {
+func tencentStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Response) (*dto.Usage, *types.NewAPIError) {
var responseText string
scanner := bufio.NewScanner(resp.Body)
scanner.Split(bufio.ScanLines)
@@ -126,38 +129,35 @@ func tencentStreamHandler(c *gin.Context, resp *http.Response) (*dto.OpenAIError
common.CloseResponseBodyGracefully(resp)
- return nil, responseText
+ return service.ResponseText2Usage(responseText, info.UpstreamModelName, info.PromptTokens), nil
}
-func tencentHandler(c *gin.Context, resp *http.Response) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
+func tencentHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Response) (*dto.Usage, *types.NewAPIError) {
var tencentSb TencentChatResponseSB
responseBody, err := io.ReadAll(resp.Body)
if err != nil {
- return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
+ return nil, types.NewError(err, types.ErrorCodeReadResponseBodyFailed)
}
common.CloseResponseBodyGracefully(resp)
err = json.Unmarshal(responseBody, &tencentSb)
if err != nil {
- return service.OpenAIErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
+ return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
}
if tencentSb.Response.Error.Code != 0 {
- return &dto.OpenAIErrorWithStatusCode{
- Error: dto.OpenAIError{
- Message: tencentSb.Response.Error.Message,
- Code: tencentSb.Response.Error.Code,
- },
- StatusCode: resp.StatusCode,
- }, nil
+ return nil, types.WithOpenAIError(types.OpenAIError{
+ Message: tencentSb.Response.Error.Message,
+ Code: tencentSb.Response.Error.Code,
+ }, resp.StatusCode)
}
fullTextResponse := responseTencent2OpenAI(&tencentSb.Response)
- jsonResponse, err := json.Marshal(fullTextResponse)
+ jsonResponse, err := common.Marshal(fullTextResponse)
if err != nil {
- return service.OpenAIErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil
+ return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
}
c.Writer.Header().Set("Content-Type", "application/json")
c.Writer.WriteHeader(resp.StatusCode)
- _, err = c.Writer.Write(jsonResponse)
- return nil, &fullTextResponse.Usage
+ common.IOCopyBytesGracefully(c, resp, jsonResponse)
+ return &fullTextResponse.Usage, nil
}
func parseTencentConfig(config string) (appId int64, secretId string, secretKey string, err error) {
diff --git a/relay/channel/vertex/adaptor.go b/relay/channel/vertex/adaptor.go
index e568f6518e7..fa895de0882 100644
--- a/relay/channel/vertex/adaptor.go
+++ b/relay/channel/vertex/adaptor.go
@@ -14,6 +14,7 @@ import (
relaycommon "one-api/relay/common"
"one-api/relay/constant"
"one-api/setting/model_setting"
+ "one-api/types"
"strings"
"github.com/gin-gonic/gin"
@@ -208,19 +209,19 @@ func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, request
return channel.DoApiRequest(a, c, info, requestBody)
}
-func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *dto.OpenAIErrorWithStatusCode) {
+func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *types.NewAPIError) {
if info.IsStream {
switch a.RequestMode {
case RequestModeClaude:
err, usage = claude.ClaudeStreamHandler(c, resp, info, claude.RequestModeMessage)
case RequestModeGemini:
if info.RelayMode == constant.RelayModeGemini {
- usage, err = gemini.GeminiTextGenerationStreamHandler(c, resp, info)
+ usage, err = gemini.GeminiTextGenerationStreamHandler(c, info, resp)
} else {
- err, usage = gemini.GeminiChatStreamHandler(c, resp, info)
+ usage, err = gemini.GeminiChatStreamHandler(c, info, resp)
}
case RequestModeLlama:
- err, usage = openai.OaiStreamHandler(c, resp, info)
+ usage, err = openai.OaiStreamHandler(c, info, resp)
}
} else {
switch a.RequestMode {
@@ -228,12 +229,12 @@ func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycom
err, usage = claude.ClaudeHandler(c, resp, claude.RequestModeMessage, info)
case RequestModeGemini:
if info.RelayMode == constant.RelayModeGemini {
- usage, err = gemini.GeminiTextGenerationHandler(c, resp, info)
+ usage, err = gemini.GeminiTextGenerationHandler(c, info, resp)
} else {
- err, usage = gemini.GeminiChatHandler(c, resp, info)
+ usage, err = gemini.GeminiChatHandler(c, info, resp)
}
case RequestModeLlama:
- err, usage = openai.OpenaiHandler(c, resp, info)
+ usage, err = openai.OpenaiHandler(c, info, resp)
}
}
return
diff --git a/relay/channel/vertex/relay-vertex.go b/relay/channel/vertex/relay-vertex.go
index d2596320259..5ed876654f4 100644
--- a/relay/channel/vertex/relay-vertex.go
+++ b/relay/channel/vertex/relay-vertex.go
@@ -4,8 +4,11 @@ import "one-api/common"
func GetModelRegion(other string, localModelName string) string {
// if other is json string
- if common.IsJsonStr(other) {
- m := common.StrToMap(other)
+ if common.IsJsonObject(other) {
+ m, err := common.StrToMap(other)
+ if err != nil {
+ return other // return original if parsing fails
+ }
if m[localModelName] != nil {
return m[localModelName].(string)
} else {
diff --git a/relay/channel/volcengine/adaptor.go b/relay/channel/volcengine/adaptor.go
index 782339346bb..af15d636700 100644
--- a/relay/channel/volcengine/adaptor.go
+++ b/relay/channel/volcengine/adaptor.go
@@ -13,6 +13,7 @@ import (
"one-api/relay/channel/openai"
relaycommon "one-api/relay/common"
"one-api/relay/constant"
+ "one-api/types"
"path/filepath"
"strings"
@@ -225,18 +226,18 @@ func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, request
return channel.DoApiRequest(a, c, info, requestBody)
}
-func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *dto.OpenAIErrorWithStatusCode) {
+func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *types.NewAPIError) {
switch info.RelayMode {
case constant.RelayModeChatCompletions:
if info.IsStream {
- err, usage = openai.OaiStreamHandler(c, resp, info)
+ usage, err = openai.OaiStreamHandler(c, info, resp)
} else {
- err, usage = openai.OpenaiHandler(c, resp, info)
+ usage, err = openai.OpenaiHandler(c, info, resp)
}
case constant.RelayModeEmbeddings:
- err, usage = openai.OpenaiHandler(c, resp, info)
+ usage, err = openai.OpenaiHandler(c, info, resp)
case constant.RelayModeImagesGenerations, constant.RelayModeImagesEdits:
- err, usage = openai.OpenaiHandlerWithUsage(c, resp, info)
+ usage, err = openai.OpenaiHandlerWithUsage(c, info, resp)
}
return
}
diff --git a/relay/channel/xai/adaptor.go b/relay/channel/xai/adaptor.go
index b589641554b..8d880137e51 100644
--- a/relay/channel/xai/adaptor.go
+++ b/relay/channel/xai/adaptor.go
@@ -8,6 +8,7 @@ import (
"one-api/relay/channel"
"one-api/relay/channel/openai"
relaycommon "one-api/relay/common"
+ "one-api/types"
"strings"
"one-api/relay/constant"
@@ -56,6 +57,15 @@ func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayIn
if request == nil {
return nil, errors.New("request is nil")
}
+ if strings.HasSuffix(info.UpstreamModelName, "-search") {
+ info.UpstreamModelName = strings.TrimSuffix(info.UpstreamModelName, "-search")
+ request.Model = info.UpstreamModelName
+ toMap := request.ToMap()
+ toMap["search_parameters"] = map[string]any{
+ "mode": "on",
+ }
+ return toMap, nil
+ }
if strings.HasPrefix(request.Model, "grok-3-mini") {
if request.MaxCompletionTokens == 0 && request.MaxTokens != 0 {
request.MaxCompletionTokens = request.MaxTokens
@@ -95,15 +105,15 @@ func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, request
return channel.DoApiRequest(a, c, info, requestBody)
}
-func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *dto.OpenAIErrorWithStatusCode) {
+func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *types.NewAPIError) {
switch info.RelayMode {
case constant.RelayModeImagesGenerations, constant.RelayModeImagesEdits:
- err, usage = openai.OpenaiHandlerWithUsage(c, resp, info)
+ usage, err = openai.OpenaiHandlerWithUsage(c, info, resp)
default:
if info.IsStream {
- err, usage = xAIStreamHandler(c, resp, info)
+ usage, err = xAIStreamHandler(c, info, resp)
} else {
- err, usage = xAIHandler(c, resp, info)
+ usage, err = xAIHandler(c, info, resp)
}
}
return
diff --git a/relay/channel/xai/constants.go b/relay/channel/xai/constants.go
index 685fe3bba66..311b4bb6e29 100644
--- a/relay/channel/xai/constants.go
+++ b/relay/channel/xai/constants.go
@@ -1,6 +1,8 @@
package xai
var ModelList = []string{
+ // grok-4
+ "grok-4", "grok-4-0709", "grok-4-0709-search",
// grok-3
"grok-3-beta", "grok-3-mini-beta",
// grok-3 mini
diff --git a/relay/channel/xai/dto.go b/relay/channel/xai/dto.go
index b8098475dfa..107a980a877 100644
--- a/relay/channel/xai/dto.go
+++ b/relay/channel/xai/dto.go
@@ -4,24 +4,24 @@ import "one-api/dto"
// ChatCompletionResponse represents the response from XAI chat completion API
type ChatCompletionResponse struct {
- Id string `json:"id"`
- Object string `json:"object"`
- Created int64 `json:"created"`
- Model string `json:"model"`
- Choices []dto.ChatCompletionsStreamResponseChoice
- Usage *dto.Usage `json:"usage"`
- SystemFingerprint string `json:"system_fingerprint"`
+ Id string `json:"id"`
+ Object string `json:"object"`
+ Created int64 `json:"created"`
+ Model string `json:"model"`
+ Choices []dto.OpenAITextResponseChoice `json:"choices"`
+ Usage *dto.Usage `json:"usage"`
+ SystemFingerprint string `json:"system_fingerprint"`
}
// quality, size or style are not supported by xAI API at the moment.
type ImageRequest struct {
- Model string `json:"model"`
- Prompt string `json:"prompt" binding:"required"`
- N int `json:"n,omitempty"`
+ Model string `json:"model"`
+ Prompt string `json:"prompt" binding:"required"`
+ N int `json:"n,omitempty"`
// Size string `json:"size,omitempty"`
// Quality string `json:"quality,omitempty"`
- ResponseFormat string `json:"response_format,omitempty"`
+ ResponseFormat string `json:"response_format,omitempty"`
// Style string `json:"style,omitempty"`
// User string `json:"user,omitempty"`
// ExtraFields json.RawMessage `json:"extra_fields,omitempty"`
-}
\ No newline at end of file
+}
diff --git a/relay/channel/xai/text.go b/relay/channel/xai/text.go
index 4a030e48bd1..4d098102afc 100644
--- a/relay/channel/xai/text.go
+++ b/relay/channel/xai/text.go
@@ -10,6 +10,7 @@ import (
relaycommon "one-api/relay/common"
"one-api/relay/helper"
"one-api/service"
+ "one-api/types"
"strings"
"github.com/gin-gonic/gin"
@@ -34,7 +35,7 @@ func streamResponseXAI2OpenAI(xAIResp *dto.ChatCompletionsStreamResponse, usage
return openAIResp
}
-func xAIStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
+func xAIStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Response) (*dto.Usage, *types.NewAPIError) {
usage := &dto.Usage{}
var responseTextBuilder strings.Builder
var toolCount int
@@ -74,30 +75,33 @@ func xAIStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.Rel
helper.Done(c)
common.CloseResponseBodyGracefully(resp)
- return nil, usage
+ return usage, nil
}
-func xAIHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
+func xAIHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Response) (*dto.Usage, *types.NewAPIError) {
defer common.CloseResponseBodyGracefully(resp)
responseBody, err := io.ReadAll(resp.Body)
- var response *dto.SimpleResponse
- err = common.UnmarshalJson(responseBody, &response)
if err != nil {
- common.SysError("error unmarshalling stream response: " + err.Error())
- return nil, nil
+ return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
+ }
+ var xaiResponse ChatCompletionResponse
+ err = common.Unmarshal(responseBody, &xaiResponse)
+ if err != nil {
+ return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
+ }
+ if xaiResponse.Usage != nil {
+ xaiResponse.Usage.CompletionTokens = xaiResponse.Usage.TotalTokens - xaiResponse.Usage.PromptTokens
+ xaiResponse.Usage.CompletionTokenDetails.TextTokens = xaiResponse.Usage.CompletionTokens - xaiResponse.Usage.CompletionTokenDetails.ReasoningTokens
}
- response.Usage.CompletionTokens = response.Usage.TotalTokens - response.Usage.PromptTokens
- response.Usage.CompletionTokenDetails.TextTokens = response.Usage.CompletionTokens - response.Usage.CompletionTokenDetails.ReasoningTokens
// new body
- encodeJson, err := common.EncodeJson(response)
+ encodeJson, err := common.Marshal(xaiResponse)
if err != nil {
- common.SysError("error marshalling stream response: " + err.Error())
- return nil, nil
+ return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
}
common.IOCopyBytesGracefully(c, resp, encodeJson)
- return nil, &response.Usage
+ return xaiResponse.Usage, nil
}
diff --git a/relay/channel/xunfei/adaptor.go b/relay/channel/xunfei/adaptor.go
index 7591e0e75f8..0d218adafb3 100644
--- a/relay/channel/xunfei/adaptor.go
+++ b/relay/channel/xunfei/adaptor.go
@@ -7,7 +7,7 @@ import (
"one-api/dto"
"one-api/relay/channel"
relaycommon "one-api/relay/common"
- "one-api/service"
+ "one-api/types"
"strings"
"github.com/gin-gonic/gin"
@@ -74,18 +74,18 @@ func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, request
return dummyResp, nil
}
-func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *dto.OpenAIErrorWithStatusCode) {
+func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *types.NewAPIError) {
splits := strings.Split(info.ApiKey, "|")
if len(splits) != 3 {
- return nil, service.OpenAIErrorWrapper(errors.New("invalid auth"), "invalid_auth", http.StatusBadRequest)
+ return nil, types.NewError(errors.New("invalid auth"), types.ErrorCodeChannelInvalidKey)
}
if a.request == nil {
- return nil, service.OpenAIErrorWrapper(errors.New("request is nil"), "request_is_nil", http.StatusBadRequest)
+ return nil, types.NewError(errors.New("request is nil"), types.ErrorCodeInvalidRequest)
}
if info.IsStream {
- err, usage = xunfeiStreamHandler(c, *a.request, splits[0], splits[1], splits[2])
+ usage, err = xunfeiStreamHandler(c, *a.request, splits[0], splits[1], splits[2])
} else {
- err, usage = xunfeiHandler(c, *a.request, splits[0], splits[1], splits[2])
+ usage, err = xunfeiHandler(c, *a.request, splits[0], splits[1], splits[2])
}
return
}
diff --git a/relay/channel/xunfei/relay-xunfei.go b/relay/channel/xunfei/relay-xunfei.go
index c6ef722c885..373ad605426 100644
--- a/relay/channel/xunfei/relay-xunfei.go
+++ b/relay/channel/xunfei/relay-xunfei.go
@@ -6,18 +6,18 @@ import (
"encoding/base64"
"encoding/json"
"fmt"
- "github.com/gin-gonic/gin"
- "github.com/gorilla/websocket"
"io"
- "net/http"
"net/url"
"one-api/common"
"one-api/constant"
"one-api/dto"
"one-api/relay/helper"
- "one-api/service"
+ "one-api/types"
"strings"
"time"
+
+ "github.com/gin-gonic/gin"
+ "github.com/gorilla/websocket"
)
// https://console.xfyun.cn/services/cbm
@@ -126,11 +126,11 @@ func buildXunfeiAuthUrl(hostUrl string, apiKey, apiSecret string) string {
return callUrl
}
-func xunfeiStreamHandler(c *gin.Context, textRequest dto.GeneralOpenAIRequest, appId string, apiSecret string, apiKey string) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
+func xunfeiStreamHandler(c *gin.Context, textRequest dto.GeneralOpenAIRequest, appId string, apiSecret string, apiKey string) (*dto.Usage, *types.NewAPIError) {
domain, authUrl := getXunfeiAuthUrl(c, apiKey, apiSecret, textRequest.Model)
dataChan, stopChan, err := xunfeiMakeRequest(textRequest, domain, authUrl, appId)
if err != nil {
- return service.OpenAIErrorWrapper(err, "make xunfei request err", http.StatusInternalServerError), nil
+ return nil, types.NewError(err, types.ErrorCodeDoRequestFailed)
}
helper.SetEventStreamHeaders(c)
var usage dto.Usage
@@ -153,14 +153,14 @@ func xunfeiStreamHandler(c *gin.Context, textRequest dto.GeneralOpenAIRequest, a
return false
}
})
- return nil, &usage
+ return &usage, nil
}
-func xunfeiHandler(c *gin.Context, textRequest dto.GeneralOpenAIRequest, appId string, apiSecret string, apiKey string) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
+func xunfeiHandler(c *gin.Context, textRequest dto.GeneralOpenAIRequest, appId string, apiSecret string, apiKey string) (*dto.Usage, *types.NewAPIError) {
domain, authUrl := getXunfeiAuthUrl(c, apiKey, apiSecret, textRequest.Model)
dataChan, stopChan, err := xunfeiMakeRequest(textRequest, domain, authUrl, appId)
if err != nil {
- return service.OpenAIErrorWrapper(err, "make xunfei request err", http.StatusInternalServerError), nil
+ return nil, types.NewError(err, types.ErrorCodeDoRequestFailed)
}
var usage dto.Usage
var content string
@@ -191,11 +191,11 @@ func xunfeiHandler(c *gin.Context, textRequest dto.GeneralOpenAIRequest, appId s
response := responseXunfei2OpenAI(&xunfeiResponse)
jsonResponse, err := json.Marshal(response)
if err != nil {
- return service.OpenAIErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil
+ return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
}
c.Writer.Header().Set("Content-Type", "application/json")
_, _ = c.Writer.Write(jsonResponse)
- return nil, &usage
+ return &usage, nil
}
func xunfeiMakeRequest(textRequest dto.GeneralOpenAIRequest, domain, authUrl, appId string) (chan XunfeiChatResponse, chan bool, error) {
diff --git a/relay/channel/zhipu/adaptor.go b/relay/channel/zhipu/adaptor.go
index b4d8fb304bd..433444289cf 100644
--- a/relay/channel/zhipu/adaptor.go
+++ b/relay/channel/zhipu/adaptor.go
@@ -8,6 +8,7 @@ import (
"one-api/dto"
"one-api/relay/channel"
relaycommon "one-api/relay/common"
+ "one-api/types"
"github.com/gin-gonic/gin"
)
@@ -77,11 +78,11 @@ func (a *Adaptor) ConvertOpenAIResponsesRequest(c *gin.Context, info *relaycommo
return nil, errors.New("not implemented")
}
-func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *dto.OpenAIErrorWithStatusCode) {
+func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *types.NewAPIError) {
if info.IsStream {
- err, usage = zhipuStreamHandler(c, resp)
+ usage, err = zhipuStreamHandler(c, info, resp)
} else {
- err, usage = zhipuHandler(c, resp)
+ usage, err = zhipuHandler(c, info, resp)
}
return
}
diff --git a/relay/channel/zhipu/relay-zhipu.go b/relay/channel/zhipu/relay-zhipu.go
index 91cd384b8b8..916a200de45 100644
--- a/relay/channel/zhipu/relay-zhipu.go
+++ b/relay/channel/zhipu/relay-zhipu.go
@@ -3,18 +3,20 @@ package zhipu
import (
"bufio"
"encoding/json"
- "github.com/gin-gonic/gin"
- "github.com/golang-jwt/jwt"
"io"
"net/http"
"one-api/common"
"one-api/constant"
"one-api/dto"
+ relaycommon "one-api/relay/common"
"one-api/relay/helper"
- "one-api/service"
+ "one-api/types"
"strings"
"sync"
"time"
+
+ "github.com/gin-gonic/gin"
+ "github.com/golang-jwt/jwt"
)
// https://open.bigmodel.cn/doc/api#chatglm_std
@@ -150,7 +152,7 @@ func streamMetaResponseZhipu2OpenAI(zhipuResponse *ZhipuStreamMetaResponse) (*dt
return &response, &zhipuResponse.Usage
}
-func zhipuStreamHandler(c *gin.Context, resp *http.Response) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
+func zhipuStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Response) (*dto.Usage, *types.NewAPIError) {
var usage *dto.Usage
scanner := bufio.NewScanner(resp.Body)
scanner.Split(bufio.ScanLines)
@@ -211,38 +213,33 @@ func zhipuStreamHandler(c *gin.Context, resp *http.Response) (*dto.OpenAIErrorWi
}
})
common.CloseResponseBodyGracefully(resp)
- return nil, usage
+ return usage, nil
}
-func zhipuHandler(c *gin.Context, resp *http.Response) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
+func zhipuHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Response) (*dto.Usage, *types.NewAPIError) {
var zhipuResponse ZhipuResponse
responseBody, err := io.ReadAll(resp.Body)
if err != nil {
- return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
+ return nil, types.NewError(err, types.ErrorCodeReadResponseBodyFailed)
}
common.CloseResponseBodyGracefully(resp)
err = json.Unmarshal(responseBody, &zhipuResponse)
if err != nil {
- return service.OpenAIErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
+ return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
}
if !zhipuResponse.Success {
- return &dto.OpenAIErrorWithStatusCode{
- Error: dto.OpenAIError{
- Message: zhipuResponse.Msg,
- Type: "zhipu_error",
- Param: "",
- Code: zhipuResponse.Code,
- },
- StatusCode: resp.StatusCode,
- }, nil
+ return nil, types.WithOpenAIError(types.OpenAIError{
+ Message: zhipuResponse.Msg,
+ Code: zhipuResponse.Code,
+ }, resp.StatusCode)
}
fullTextResponse := responseZhipu2OpenAI(&zhipuResponse)
jsonResponse, err := json.Marshal(fullTextResponse)
if err != nil {
- return service.OpenAIErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil
+ return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
}
c.Writer.Header().Set("Content-Type", "application/json")
c.Writer.WriteHeader(resp.StatusCode)
_, err = c.Writer.Write(jsonResponse)
- return nil, &fullTextResponse.Usage
+ return &fullTextResponse.Usage, nil
}
diff --git a/relay/channel/zhipu_4v/adaptor.go b/relay/channel/zhipu_4v/adaptor.go
index 222cdff8d95..edd7a534504 100644
--- a/relay/channel/zhipu_4v/adaptor.go
+++ b/relay/channel/zhipu_4v/adaptor.go
@@ -10,6 +10,7 @@ import (
"one-api/relay/channel/openai"
relaycommon "one-api/relay/common"
relayconstant "one-api/relay/constant"
+ "one-api/types"
"github.com/gin-gonic/gin"
)
@@ -80,11 +81,11 @@ func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, request
return channel.DoApiRequest(a, c, info, requestBody)
}
-func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *dto.OpenAIErrorWithStatusCode) {
+func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *types.NewAPIError) {
if info.IsStream {
- err, usage = openai.OaiStreamHandler(c, resp, info)
+ usage, err = openai.OaiStreamHandler(c, info, resp)
} else {
- err, usage = openai.OpenaiHandler(c, resp, info)
+ usage, err = openai.OpenaiHandler(c, info, resp)
}
return
}
diff --git a/relay/claude_handler.go b/relay/claude_handler.go
index 42139ddfce5..5f38960e52b 100644
--- a/relay/claude_handler.go
+++ b/relay/claude_handler.go
@@ -2,10 +2,8 @@ package relay
import (
"bytes"
- "encoding/json"
"errors"
"fmt"
- "github.com/gin-gonic/gin"
"io"
"net/http"
"one-api/common"
@@ -14,7 +12,10 @@ import (
"one-api/relay/helper"
"one-api/service"
"one-api/setting/model_setting"
+ "one-api/types"
"strings"
+
+ "github.com/gin-gonic/gin"
)
func getAndValidateClaudeRequest(c *gin.Context) (textRequest *dto.ClaudeRequest, err error) {
@@ -32,14 +33,14 @@ func getAndValidateClaudeRequest(c *gin.Context) (textRequest *dto.ClaudeRequest
return textRequest, nil
}
-func ClaudeHelper(c *gin.Context) (claudeError *dto.ClaudeErrorWithStatusCode) {
+func ClaudeHelper(c *gin.Context) (newAPIError *types.NewAPIError) {
relayInfo := relaycommon.GenRelayInfoClaude(c)
// get & validate textRequest 获取并验证文本请求
textRequest, err := getAndValidateClaudeRequest(c)
if err != nil {
- return service.ClaudeErrorWrapperLocal(err, "invalid_claude_request", http.StatusBadRequest)
+ return types.NewError(err, types.ErrorCodeInvalidRequest)
}
if textRequest.Stream {
@@ -48,35 +49,35 @@ func ClaudeHelper(c *gin.Context) (claudeError *dto.ClaudeErrorWithStatusCode) {
err = helper.ModelMappedHelper(c, relayInfo, textRequest)
if err != nil {
- return service.ClaudeErrorWrapperLocal(err, "model_mapped_error", http.StatusInternalServerError)
+ return types.NewError(err, types.ErrorCodeChannelModelMappedError)
}
promptTokens, err := getClaudePromptTokens(textRequest, relayInfo)
// count messages token error 计算promptTokens错误
if err != nil {
- return service.ClaudeErrorWrapperLocal(err, "count_token_messages_failed", http.StatusInternalServerError)
+ return types.NewError(err, types.ErrorCodeCountTokenFailed)
}
priceData, err := helper.ModelPriceHelper(c, relayInfo, promptTokens, int(textRequest.MaxTokens))
if err != nil {
- return service.ClaudeErrorWrapperLocal(err, "model_price_error", http.StatusInternalServerError)
+ return types.NewError(err, types.ErrorCodeModelPriceError)
}
// pre-consume quota 预消耗配额
- preConsumedQuota, userQuota, openaiErr := preConsumeQuota(c, priceData.ShouldPreConsumedQuota, relayInfo)
+ preConsumedQuota, userQuota, newAPIError := preConsumeQuota(c, priceData.ShouldPreConsumedQuota, relayInfo)
- if openaiErr != nil {
- return service.OpenAIErrorToClaudeError(openaiErr)
+ if newAPIError != nil {
+ return newAPIError
}
defer func() {
- if openaiErr != nil {
+ if newAPIError != nil {
returnPreConsumedQuota(c, relayInfo, userQuota, preConsumedQuota)
}
}()
adaptor := GetAdaptor(relayInfo.ApiType)
if adaptor == nil {
- return service.ClaudeErrorWrapperLocal(fmt.Errorf("invalid api type: %d", relayInfo.ApiType), "invalid_api_type", http.StatusBadRequest)
+ return types.NewError(fmt.Errorf("invalid api type: %d", relayInfo.ApiType), types.ErrorCodeInvalidApiType)
}
adaptor.Init(relayInfo)
var requestBody io.Reader
@@ -109,14 +110,14 @@ func ClaudeHelper(c *gin.Context) (claudeError *dto.ClaudeErrorWithStatusCode) {
convertedRequest, err := adaptor.ConvertClaudeRequest(c, relayInfo, textRequest)
if err != nil {
- return service.ClaudeErrorWrapperLocal(err, "convert_request_failed", http.StatusInternalServerError)
+ return types.NewError(err, types.ErrorCodeConvertRequestFailed)
}
- jsonData, err := json.Marshal(convertedRequest)
+ jsonData, err := common.Marshal(convertedRequest)
if common.DebugEnabled {
println("requestBody: ", string(jsonData))
}
if err != nil {
- return service.ClaudeErrorWrapperLocal(err, "json_marshal_failed", http.StatusInternalServerError)
+ return types.NewError(err, types.ErrorCodeConvertRequestFailed)
}
requestBody = bytes.NewBuffer(jsonData)
@@ -124,26 +125,26 @@ func ClaudeHelper(c *gin.Context) (claudeError *dto.ClaudeErrorWithStatusCode) {
var httpResp *http.Response
resp, err := adaptor.DoRequest(c, relayInfo, requestBody)
if err != nil {
- return service.ClaudeErrorWrapper(err, "do_request_failed", http.StatusInternalServerError)
+ return types.NewOpenAIError(err, types.ErrorCodeDoRequestFailed, http.StatusInternalServerError)
}
if resp != nil {
httpResp = resp.(*http.Response)
relayInfo.IsStream = relayInfo.IsStream || strings.HasPrefix(httpResp.Header.Get("Content-Type"), "text/event-stream")
if httpResp.StatusCode != http.StatusOK {
- openaiErr = service.RelayErrorHandler(httpResp, false)
+ newAPIError = service.RelayErrorHandler(httpResp, false)
// reset status code 重置状态码
- service.ResetStatusCode(openaiErr, statusCodeMappingStr)
- return service.OpenAIErrorToClaudeError(openaiErr)
+ service.ResetStatusCode(newAPIError, statusCodeMappingStr)
+ return newAPIError
}
}
- usage, openaiErr := adaptor.DoResponse(c, httpResp, relayInfo)
+ usage, newAPIError := adaptor.DoResponse(c, httpResp, relayInfo)
//log.Printf("usage: %v", usage)
- if openaiErr != nil {
+ if newAPIError != nil {
// reset status code 重置状态码
- service.ResetStatusCode(openaiErr, statusCodeMappingStr)
- return service.OpenAIErrorToClaudeError(openaiErr)
+ service.ResetStatusCode(newAPIError, statusCodeMappingStr)
+ return newAPIError
}
service.PostClaudeConsumeQuota(c, relayInfo, usage.(*dto.Usage), preConsumedQuota, userQuota, priceData, "")
return nil
diff --git a/relay/common/relay_info.go b/relay/common/relay_info.go
index 2f5f5d38b80..45fde019cd3 100644
--- a/relay/common/relay_info.go
+++ b/relay/common/relay_info.go
@@ -180,16 +180,18 @@ func GenRelayInfoResponses(c *gin.Context, req *dto.OpenAIResponsesRequest) *Rel
}
if len(req.Tools) > 0 {
for _, tool := range req.Tools {
- info.ResponsesUsageInfo.BuiltInTools[tool.Type] = &BuildInToolInfo{
- ToolName: tool.Type,
+ toolType := common.Interface2String(tool["type"])
+ info.ResponsesUsageInfo.BuiltInTools[toolType] = &BuildInToolInfo{
+ ToolName: toolType,
CallCount: 0,
}
- switch tool.Type {
+ switch toolType {
case dto.BuildInToolWebSearchPreview:
- if tool.SearchContextSize == "" {
- tool.SearchContextSize = "medium"
+ searchContextSize := common.Interface2String(tool["search_context_size"])
+ if searchContextSize == "" {
+ searchContextSize = "medium"
}
- info.ResponsesUsageInfo.BuiltInTools[tool.Type].SearchContextSize = tool.SearchContextSize
+ info.ResponsesUsageInfo.BuiltInTools[toolType].SearchContextSize = searchContextSize
}
}
}
@@ -213,7 +215,7 @@ func GenRelayInfoImage(c *gin.Context) *RelayInfo {
func GenRelayInfo(c *gin.Context) *RelayInfo {
channelType := common.GetContextKeyInt(c, constant.ContextKeyChannelType)
channelId := common.GetContextKeyInt(c, constant.ContextKeyChannelId)
- paramOverride := common.GetContextKeyStringMap(c, constant.ContextKeyParamOverride)
+ paramOverride := common.GetContextKeyStringMap(c, constant.ContextKeyChannelParamOverride)
tokenId := common.GetContextKeyInt(c, constant.ContextKeyTokenId)
tokenKey := common.GetContextKeyString(c, constant.ContextKeyTokenKey)
@@ -229,7 +231,7 @@ func GenRelayInfo(c *gin.Context) *RelayInfo {
UserEmail: common.GetContextKeyString(c, constant.ContextKeyUserEmail),
isFirstResponse: true,
RelayMode: relayconstant.Path2RelayMode(c.Request.URL.Path),
- BaseUrl: common.GetContextKeyString(c, constant.ContextKeyBaseUrl),
+ BaseUrl: common.GetContextKeyString(c, constant.ContextKeyChannelBaseUrl),
RequestURLPath: c.Request.URL.String(),
ChannelType: channelType,
ChannelId: channelId,
@@ -247,7 +249,7 @@ func GenRelayInfo(c *gin.Context) *RelayInfo {
IsModelMapped: false,
ApiType: apiType,
ApiVersion: c.GetString("api_version"),
- ApiKey: strings.TrimPrefix(c.Request.Header.Get("Authorization"), "Bearer "),
+ ApiKey: common.GetContextKeyString(c, constant.ContextKeyChannelKey),
Organization: c.GetString("channel_organization"),
ChannelCreateTime: c.GetInt64("channel_create_time"),
diff --git a/relay/common_handler/rerank.go b/relay/common_handler/rerank.go
index 0df219e36f7..ce823b3abc5 100644
--- a/relay/common_handler/rerank.go
+++ b/relay/common_handler/rerank.go
@@ -1,7 +1,6 @@
package common_handler
import (
- "github.com/gin-gonic/gin"
"io"
"net/http"
"one-api/common"
@@ -9,13 +8,15 @@ import (
"one-api/dto"
"one-api/relay/channel/xinference"
relaycommon "one-api/relay/common"
- "one-api/service"
+ "one-api/types"
+
+ "github.com/gin-gonic/gin"
)
-func RerankHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Response) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
+func RerankHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Response) (*dto.Usage, *types.NewAPIError) {
responseBody, err := io.ReadAll(resp.Body)
if err != nil {
- return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
+ return nil, types.NewError(err, types.ErrorCodeReadResponseBodyFailed)
}
common.CloseResponseBodyGracefully(resp)
if common.DebugEnabled {
@@ -24,9 +25,9 @@ func RerankHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Respo
var jinaResp dto.RerankResponse
if info.ChannelType == constant.ChannelTypeXinference {
var xinRerankResponse xinference.XinRerankResponse
- err = common.UnmarshalJson(responseBody, &xinRerankResponse)
+ err = common.Unmarshal(responseBody, &xinRerankResponse)
if err != nil {
- return service.OpenAIErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
+ return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
}
jinaRespResults := make([]dto.RerankResponseResult, len(xinRerankResponse.Results))
for i, result := range xinRerankResponse.Results {
@@ -59,14 +60,14 @@ func RerankHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Respo
},
}
} else {
- err = common.UnmarshalJson(responseBody, &jinaResp)
+ err = common.Unmarshal(responseBody, &jinaResp)
if err != nil {
- return service.OpenAIErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
+ return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
}
jinaResp.Usage.PromptTokens = jinaResp.Usage.TotalTokens
}
c.Writer.Header().Set("Content-Type", "application/json")
c.JSON(http.StatusOK, jinaResp)
- return nil, &jinaResp.Usage
+ return &jinaResp.Usage, nil
}
diff --git a/relay/embedding_handler.go b/relay/embedding_handler.go
index 849c70da349..be11bb2b821 100644
--- a/relay/embedding_handler.go
+++ b/relay/embedding_handler.go
@@ -4,7 +4,6 @@ import (
"bytes"
"encoding/json"
"fmt"
- "github.com/gin-gonic/gin"
"net/http"
"one-api/common"
"one-api/dto"
@@ -12,6 +11,9 @@ import (
relayconstant "one-api/relay/constant"
"one-api/relay/helper"
"one-api/service"
+ "one-api/types"
+
+ "github.com/gin-gonic/gin"
)
func getEmbeddingPromptToken(embeddingRequest dto.EmbeddingRequest) int {
@@ -32,24 +34,24 @@ func validateEmbeddingRequest(c *gin.Context, info *relaycommon.RelayInfo, embed
return nil
}
-func EmbeddingHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode) {
+func EmbeddingHelper(c *gin.Context) (newAPIError *types.NewAPIError) {
relayInfo := relaycommon.GenRelayInfoEmbedding(c)
var embeddingRequest *dto.EmbeddingRequest
err := common.UnmarshalBodyReusable(c, &embeddingRequest)
if err != nil {
common.LogError(c, fmt.Sprintf("getAndValidateTextRequest failed: %s", err.Error()))
- return service.OpenAIErrorWrapperLocal(err, "invalid_text_request", http.StatusBadRequest)
+ return types.NewError(err, types.ErrorCodeInvalidRequest)
}
err = validateEmbeddingRequest(c, relayInfo, *embeddingRequest)
if err != nil {
- return service.OpenAIErrorWrapperLocal(err, "invalid_embedding_request", http.StatusBadRequest)
+ return types.NewError(err, types.ErrorCodeInvalidRequest)
}
err = helper.ModelMappedHelper(c, relayInfo, embeddingRequest)
if err != nil {
- return service.OpenAIErrorWrapperLocal(err, "model_mapped_error", http.StatusInternalServerError)
+ return types.NewError(err, types.ErrorCodeChannelModelMappedError)
}
promptToken := getEmbeddingPromptToken(*embeddingRequest)
@@ -57,57 +59,57 @@ func EmbeddingHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode)
priceData, err := helper.ModelPriceHelper(c, relayInfo, promptToken, 0)
if err != nil {
- return service.OpenAIErrorWrapperLocal(err, "model_price_error", http.StatusInternalServerError)
+ return types.NewError(err, types.ErrorCodeModelPriceError)
}
// pre-consume quota 预消耗配额
- preConsumedQuota, userQuota, openaiErr := preConsumeQuota(c, priceData.ShouldPreConsumedQuota, relayInfo)
- if openaiErr != nil {
- return openaiErr
+ preConsumedQuota, userQuota, newAPIError := preConsumeQuota(c, priceData.ShouldPreConsumedQuota, relayInfo)
+ if newAPIError != nil {
+ return newAPIError
}
defer func() {
- if openaiErr != nil {
+ if newAPIError != nil {
returnPreConsumedQuota(c, relayInfo, userQuota, preConsumedQuota)
}
}()
adaptor := GetAdaptor(relayInfo.ApiType)
if adaptor == nil {
- return service.OpenAIErrorWrapperLocal(fmt.Errorf("invalid api type: %d", relayInfo.ApiType), "invalid_api_type", http.StatusBadRequest)
+ return types.NewError(fmt.Errorf("invalid api type: %d", relayInfo.ApiType), types.ErrorCodeInvalidApiType)
}
adaptor.Init(relayInfo)
convertedRequest, err := adaptor.ConvertEmbeddingRequest(c, relayInfo, *embeddingRequest)
if err != nil {
- return service.OpenAIErrorWrapperLocal(err, "convert_request_failed", http.StatusInternalServerError)
+ return types.NewError(err, types.ErrorCodeConvertRequestFailed)
}
jsonData, err := json.Marshal(convertedRequest)
if err != nil {
- return service.OpenAIErrorWrapperLocal(err, "json_marshal_failed", http.StatusInternalServerError)
+ return types.NewError(err, types.ErrorCodeConvertRequestFailed)
}
requestBody := bytes.NewBuffer(jsonData)
statusCodeMappingStr := c.GetString("status_code_mapping")
resp, err := adaptor.DoRequest(c, relayInfo, requestBody)
if err != nil {
- return service.OpenAIErrorWrapper(err, "do_request_failed", http.StatusInternalServerError)
+ return types.NewOpenAIError(err, types.ErrorCodeDoRequestFailed, http.StatusInternalServerError)
}
var httpResp *http.Response
if resp != nil {
httpResp = resp.(*http.Response)
if httpResp.StatusCode != http.StatusOK {
- openaiErr = service.RelayErrorHandler(httpResp, false)
+ newAPIError = service.RelayErrorHandler(httpResp, false)
// reset status code 重置状态码
- service.ResetStatusCode(openaiErr, statusCodeMappingStr)
- return openaiErr
+ service.ResetStatusCode(newAPIError, statusCodeMappingStr)
+ return newAPIError
}
}
- usage, openaiErr := adaptor.DoResponse(c, httpResp, relayInfo)
- if openaiErr != nil {
+ usage, newAPIError := adaptor.DoResponse(c, httpResp, relayInfo)
+ if newAPIError != nil {
// reset status code 重置状态码
- service.ResetStatusCode(openaiErr, statusCodeMappingStr)
- return openaiErr
+ service.ResetStatusCode(newAPIError, statusCodeMappingStr)
+ return newAPIError
}
postConsumeQuota(c, relayInfo, usage.(*dto.Usage), preConsumedQuota, userQuota, priceData, "")
return nil
diff --git a/relay/gemini_handler.go b/relay/gemini_handler.go
index 9185ce624e8..e448b491356 100644
--- a/relay/gemini_handler.go
+++ b/relay/gemini_handler.go
@@ -14,6 +14,7 @@ import (
"one-api/service"
"one-api/setting"
"one-api/setting/model_setting"
+ "one-api/types"
"strings"
"github.com/gin-gonic/gin"
@@ -104,11 +105,11 @@ func trimModelThinking(modelName string) string {
return modelName
}
-func GeminiHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode) {
+func GeminiHelper(c *gin.Context) (newAPIError *types.NewAPIError) {
req, err := getAndValidateGeminiRequest(c)
if err != nil {
common.LogError(c, fmt.Sprintf("getAndValidateGeminiRequest error: %s", err.Error()))
- return service.OpenAIErrorWrapperLocal(err, "invalid_gemini_request", http.StatusBadRequest)
+ return types.NewError(err, types.ErrorCodeInvalidRequest)
}
relayInfo := relaycommon.GenRelayInfoGemini(c)
@@ -120,14 +121,14 @@ func GeminiHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode) {
sensitiveWords, err := checkGeminiInputSensitive(req)
if err != nil {
common.LogWarn(c, fmt.Sprintf("user sensitive words detected: %s", strings.Join(sensitiveWords, ", ")))
- return service.OpenAIErrorWrapperLocal(err, "check_request_sensitive_error", http.StatusBadRequest)
+ return types.NewError(err, types.ErrorCodeSensitiveWordsDetected)
}
}
// model mapped 模型映射
err = helper.ModelMappedHelper(c, relayInfo, req)
if err != nil {
- return service.OpenAIErrorWrapperLocal(err, "model_mapped_error", http.StatusBadRequest)
+ return types.NewError(err, types.ErrorCodeChannelModelMappedError)
}
if value, exists := c.Get("prompt_tokens"); exists {
@@ -158,23 +159,23 @@ func GeminiHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode) {
priceData, err := helper.ModelPriceHelper(c, relayInfo, relayInfo.PromptTokens, int(req.GenerationConfig.MaxOutputTokens))
if err != nil {
- return service.OpenAIErrorWrapperLocal(err, "model_price_error", http.StatusInternalServerError)
+ return types.NewError(err, types.ErrorCodeModelPriceError)
}
// pre consume quota
- preConsumedQuota, userQuota, openaiErr := preConsumeQuota(c, priceData.ShouldPreConsumedQuota, relayInfo)
- if openaiErr != nil {
- return openaiErr
+ preConsumedQuota, userQuota, newAPIError := preConsumeQuota(c, priceData.ShouldPreConsumedQuota, relayInfo)
+ if newAPIError != nil {
+ return newAPIError
}
defer func() {
- if openaiErr != nil {
+ if newAPIError != nil {
returnPreConsumedQuota(c, relayInfo, userQuota, preConsumedQuota)
}
}()
adaptor := GetAdaptor(relayInfo.ApiType)
if adaptor == nil {
- return service.OpenAIErrorWrapperLocal(fmt.Errorf("invalid api type: %d", relayInfo.ApiType), "invalid_api_type", http.StatusBadRequest)
+ return types.NewError(fmt.Errorf("invalid api type: %d", relayInfo.ApiType), types.ErrorCodeInvalidApiType)
}
adaptor.Init(relayInfo)
@@ -195,7 +196,7 @@ func GeminiHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode) {
requestBody, err := json.Marshal(req)
if err != nil {
- return service.OpenAIErrorWrapperLocal(err, "marshal_text_request_failed", http.StatusInternalServerError)
+ return types.NewError(err, types.ErrorCodeConvertRequestFailed)
}
if common.DebugEnabled {
@@ -205,7 +206,7 @@ func GeminiHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode) {
resp, err := adaptor.DoRequest(c, relayInfo, bytes.NewReader(requestBody))
if err != nil {
common.LogError(c, "Do gemini request failed: "+err.Error())
- return service.OpenAIErrorWrapper(err, "do_request_failed", http.StatusInternalServerError)
+ return types.NewError(err, types.ErrorCodeDoRequestFailed)
}
statusCodeMappingStr := c.GetString("status_code_mapping")
@@ -215,10 +216,10 @@ func GeminiHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode) {
httpResp = resp.(*http.Response)
relayInfo.IsStream = relayInfo.IsStream || strings.HasPrefix(httpResp.Header.Get("Content-Type"), "text/event-stream")
if httpResp.StatusCode != http.StatusOK {
- openaiErr = service.RelayErrorHandler(httpResp, false)
+ newAPIError = service.RelayErrorHandler(httpResp, false)
// reset status code 重置状态码
- service.ResetStatusCode(openaiErr, statusCodeMappingStr)
- return openaiErr
+ service.ResetStatusCode(newAPIError, statusCodeMappingStr)
+ return newAPIError
}
}
diff --git a/relay/helper/common.go b/relay/helper/common.go
index 35d983f7635..5d23b512389 100644
--- a/relay/helper/common.go
+++ b/relay/helper/common.go
@@ -4,27 +4,29 @@ import (
"encoding/json"
"errors"
"fmt"
- "github.com/gin-gonic/gin"
- "github.com/gorilla/websocket"
"net/http"
"one-api/common"
"one-api/dto"
+ "one-api/types"
+
+ "github.com/gin-gonic/gin"
+ "github.com/gorilla/websocket"
)
func SetEventStreamHeaders(c *gin.Context) {
- // 检查是否已经设置过头部
- if _, exists := c.Get("event_stream_headers_set"); exists {
- return
- }
-
- c.Writer.Header().Set("Content-Type", "text/event-stream")
- c.Writer.Header().Set("Cache-Control", "no-cache")
- c.Writer.Header().Set("Connection", "keep-alive")
- c.Writer.Header().Set("Transfer-Encoding", "chunked")
- c.Writer.Header().Set("X-Accel-Buffering", "no")
-
- // 设置标志,表示头部已经设置过
- c.Set("event_stream_headers_set", true)
+ // 检查是否已经设置过头部
+ if _, exists := c.Get("event_stream_headers_set"); exists {
+ return
+ }
+
+ c.Writer.Header().Set("Content-Type", "text/event-stream")
+ c.Writer.Header().Set("Cache-Control", "no-cache")
+ c.Writer.Header().Set("Connection", "keep-alive")
+ c.Writer.Header().Set("Transfer-Encoding", "chunked")
+ c.Writer.Header().Set("X-Accel-Buffering", "no")
+
+ // 设置标志,表示头部已经设置过
+ c.Set("event_stream_headers_set", true)
}
func ClaudeData(c *gin.Context, resp dto.ClaudeResponse) error {
@@ -85,7 +87,7 @@ func ObjectData(c *gin.Context, object interface{}) error {
if object == nil {
return errors.New("object is nil")
}
- jsonData, err := json.Marshal(object)
+ jsonData, err := common.Marshal(object)
if err != nil {
return fmt.Errorf("error marshalling object: %w", err)
}
@@ -118,7 +120,7 @@ func WssObject(c *gin.Context, ws *websocket.Conn, object interface{}) error {
return ws.WriteMessage(1, jsonData)
}
-func WssError(c *gin.Context, ws *websocket.Conn, openaiError dto.OpenAIError) {
+func WssError(c *gin.Context, ws *websocket.Conn, openaiError types.OpenAIError) {
errorObj := &dto.RealtimeEvent{
Type: "error",
EventId: GetLocalRealtimeID(c),
diff --git a/relay/helper/price.go b/relay/helper/price.go
index 9995db2fd2b..e80578e57cf 100644
--- a/relay/helper/price.go
+++ b/relay/helper/price.go
@@ -79,14 +79,15 @@ func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens
preConsumedTokens = promptTokens + maxTokens
}
var success bool
- modelRatio, success = ratio_setting.GetModelRatio(info.OriginModelName)
+ var matchName string
+ modelRatio, success, matchName = ratio_setting.GetModelRatio(info.OriginModelName)
if !success {
acceptUnsetRatio := false
if info.UserSetting.AcceptUnsetRatioModel {
acceptUnsetRatio = true
}
if !acceptUnsetRatio {
- return PriceData{}, fmt.Errorf("模型 %s 倍率或价格未配置,请联系管理员设置或开始自用模式;Model %s ratio or price not set, please set or start self-use mode", info.OriginModelName, info.OriginModelName)
+ return PriceData{}, fmt.Errorf("模型 %s 倍率或价格未配置,请联系管理员设置或开始自用模式;Model %s ratio or price not set, please set or start self-use mode", matchName, matchName)
}
}
completionRatio = ratio_setting.GetCompletionRatio(info.OriginModelName)
@@ -152,7 +153,7 @@ func ContainPriceOrRatio(modelName string) bool {
if ok {
return true
}
- _, ok = ratio_setting.GetModelRatio(modelName)
+ _, ok, _ = ratio_setting.GetModelRatio(modelName)
if ok {
return true
}
diff --git a/relay/image_handler.go b/relay/image_handler.go
index 5decb497681..8e059863083 100644
--- a/relay/image_handler.go
+++ b/relay/image_handler.go
@@ -16,6 +16,7 @@ import (
"one-api/relay/helper"
"one-api/service"
"one-api/setting"
+ "one-api/types"
"strings"
"github.com/gin-gonic/gin"
@@ -107,23 +108,23 @@ func getAndValidImageRequest(c *gin.Context, info *relaycommon.RelayInfo) (*dto.
return imageRequest, nil
}
-func ImageHelper(c *gin.Context) *dto.OpenAIErrorWithStatusCode {
+func ImageHelper(c *gin.Context) (newAPIError *types.NewAPIError) {
relayInfo := relaycommon.GenRelayInfoImage(c)
imageRequest, err := getAndValidImageRequest(c, relayInfo)
if err != nil {
common.LogError(c, fmt.Sprintf("getAndValidImageRequest failed: %s", err.Error()))
- return service.OpenAIErrorWrapper(err, "invalid_image_request", http.StatusBadRequest)
+ return types.NewError(err, types.ErrorCodeInvalidRequest)
}
err = helper.ModelMappedHelper(c, relayInfo, imageRequest)
if err != nil {
- return service.OpenAIErrorWrapperLocal(err, "model_mapped_error", http.StatusInternalServerError)
+ return types.NewError(err, types.ErrorCodeChannelModelMappedError)
}
priceData, err := helper.ModelPriceHelper(c, relayInfo, len(imageRequest.Prompt), 0)
if err != nil {
- return service.OpenAIErrorWrapperLocal(err, "model_price_error", http.StatusInternalServerError)
+ return types.NewError(err, types.ErrorCodeModelPriceError)
}
var preConsumedQuota int
var quota int
@@ -132,35 +133,37 @@ func ImageHelper(c *gin.Context) *dto.OpenAIErrorWithStatusCode {
// modelRatio 16 = modelPrice $0.04
// per 1 modelRatio = $0.04 / 16
// priceData.ModelPrice = 0.0025 * priceData.ModelRatio
- var openaiErr *dto.OpenAIErrorWithStatusCode
- preConsumedQuota, userQuota, openaiErr = preConsumeQuota(c, priceData.ShouldPreConsumedQuota, relayInfo)
- if openaiErr != nil {
- return openaiErr
+ preConsumedQuota, userQuota, newAPIError = preConsumeQuota(c, priceData.ShouldPreConsumedQuota, relayInfo)
+ if newAPIError != nil {
+ return newAPIError
}
defer func() {
- if openaiErr != nil {
+ if newAPIError != nil {
returnPreConsumedQuota(c, relayInfo, userQuota, preConsumedQuota)
}
}()
} else {
sizeRatio := 1.0
- // Size
- if imageRequest.Size == "256x256" {
- sizeRatio = 0.4
- } else if imageRequest.Size == "512x512" {
- sizeRatio = 0.45
- } else if imageRequest.Size == "1024x1024" {
- sizeRatio = 1
- } else if imageRequest.Size == "1024x1792" || imageRequest.Size == "1792x1024" {
- sizeRatio = 2
- }
-
qualityRatio := 1.0
- if imageRequest.Model == "dall-e-3" && imageRequest.Quality == "hd" {
- qualityRatio = 2.0
- if imageRequest.Size == "1024x1792" || imageRequest.Size == "1792x1024" {
- qualityRatio = 1.5
+
+ if strings.HasPrefix(imageRequest.Model, "dall-e") {
+ // Size
+ if imageRequest.Size == "256x256" {
+ sizeRatio = 0.4
+ } else if imageRequest.Size == "512x512" {
+ sizeRatio = 0.45
+ } else if imageRequest.Size == "1024x1024" {
+ sizeRatio = 1
+ } else if imageRequest.Size == "1024x1792" || imageRequest.Size == "1792x1024" {
+ sizeRatio = 2
+ }
+
+ if imageRequest.Model == "dall-e-3" && imageRequest.Quality == "hd" {
+ qualityRatio = 2.0
+ if imageRequest.Size == "1024x1792" || imageRequest.Size == "1792x1024" {
+ qualityRatio = 1.5
+ }
}
}
@@ -169,16 +172,16 @@ func ImageHelper(c *gin.Context) *dto.OpenAIErrorWithStatusCode {
quota = int(priceData.ModelPrice * priceData.GroupRatioInfo.GroupRatio * common.QuotaPerUnit)
userQuota, err = model.GetUserQuota(relayInfo.UserId, false)
if err != nil {
- return service.OpenAIErrorWrapperLocal(err, "get_user_quota_failed", http.StatusInternalServerError)
+ return types.NewError(err, types.ErrorCodeQueryDataError)
}
if userQuota-quota < 0 {
- return service.OpenAIErrorWrapperLocal(fmt.Errorf("image pre-consumed quota failed, user quota: %s, need quota: %s", common.FormatQuota(userQuota), common.FormatQuota(quota)), "insufficient_user_quota", http.StatusForbidden)
+ return types.NewError(fmt.Errorf("image pre-consumed quota failed, user quota: %s, need quota: %s", common.FormatQuota(userQuota), common.FormatQuota(quota)), types.ErrorCodeInsufficientUserQuota)
}
}
adaptor := GetAdaptor(relayInfo.ApiType)
if adaptor == nil {
- return service.OpenAIErrorWrapperLocal(fmt.Errorf("invalid api type: %d", relayInfo.ApiType), "invalid_api_type", http.StatusBadRequest)
+ return types.NewError(fmt.Errorf("invalid api type: %d", relayInfo.ApiType), types.ErrorCodeInvalidApiType)
}
adaptor.Init(relayInfo)
@@ -186,14 +189,14 @@ func ImageHelper(c *gin.Context) *dto.OpenAIErrorWithStatusCode {
convertedRequest, err := adaptor.ConvertImageRequest(c, relayInfo, *imageRequest)
if err != nil {
- return service.OpenAIErrorWrapperLocal(err, "convert_request_failed", http.StatusInternalServerError)
+ return types.NewError(err, types.ErrorCodeConvertRequestFailed)
}
if relayInfo.RelayMode == relayconstant.RelayModeImagesEdits {
requestBody = convertedRequest.(io.Reader)
} else {
jsonData, err := json.Marshal(convertedRequest)
if err != nil {
- return service.OpenAIErrorWrapperLocal(err, "json_marshal_failed", http.StatusInternalServerError)
+ return types.NewError(err, types.ErrorCodeConvertRequestFailed)
}
requestBody = bytes.NewBuffer(jsonData)
}
@@ -206,25 +209,25 @@ func ImageHelper(c *gin.Context) *dto.OpenAIErrorWithStatusCode {
resp, err := adaptor.DoRequest(c, relayInfo, requestBody)
if err != nil {
- return service.OpenAIErrorWrapper(err, "do_request_failed", http.StatusInternalServerError)
+ return types.NewOpenAIError(err, types.ErrorCodeDoRequestFailed, http.StatusInternalServerError)
}
var httpResp *http.Response
if resp != nil {
httpResp = resp.(*http.Response)
relayInfo.IsStream = relayInfo.IsStream || strings.HasPrefix(httpResp.Header.Get("Content-Type"), "text/event-stream")
if httpResp.StatusCode != http.StatusOK {
- openaiErr := service.RelayErrorHandler(httpResp, false)
+ newAPIError = service.RelayErrorHandler(httpResp, false)
// reset status code 重置状态码
- service.ResetStatusCode(openaiErr, statusCodeMappingStr)
- return openaiErr
+ service.ResetStatusCode(newAPIError, statusCodeMappingStr)
+ return newAPIError
}
}
- usage, openaiErr := adaptor.DoResponse(c, httpResp, relayInfo)
- if openaiErr != nil {
+ usage, newAPIError := adaptor.DoResponse(c, httpResp, relayInfo)
+ if newAPIError != nil {
// reset status code 重置状态码
- service.ResetStatusCode(openaiErr, statusCodeMappingStr)
- return openaiErr
+ service.ResetStatusCode(newAPIError, statusCodeMappingStr)
+ return newAPIError
}
if usage.(*dto.Usage).TotalTokens == 0 {
diff --git a/relay/relay-mj.go b/relay/relay-mj.go
index f23f8152c1e..e7f316b9894 100644
--- a/relay/relay-mj.go
+++ b/relay/relay-mj.go
@@ -575,7 +575,7 @@ func RelayMidjourneySubmit(c *gin.Context, relayMode int) *dto.MidjourneyRespons
common.SysError("get_channel_null: " + err.Error())
}
if channel.GetAutoBan() && common.AutomaticDisableChannelEnabled {
- model.UpdateChannelStatusById(midjourneyTask.ChannelId, 2, "No available account instance")
+ model.UpdateChannelStatus(midjourneyTask.ChannelId, "", 2, "No available account instance")
}
}
if midjResponse.Code != 1 && midjResponse.Code != 21 && midjResponse.Code != 22 {
diff --git a/relay/relay-text.go b/relay/relay-text.go
index 86b6c530047..603270741ed 100644
--- a/relay/relay-text.go
+++ b/relay/relay-text.go
@@ -19,6 +19,7 @@ import (
"one-api/setting"
"one-api/setting/model_setting"
"one-api/setting/operation_setting"
+ "one-api/types"
"strings"
"time"
@@ -84,7 +85,7 @@ func getAndValidateTextRequest(c *gin.Context, relayInfo *relaycommon.RelayInfo)
return textRequest, nil
}
-func TextHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode) {
+func TextHelper(c *gin.Context) (newAPIError *types.NewAPIError) {
relayInfo := relaycommon.GenRelayInfo(c)
@@ -92,8 +93,7 @@ func TextHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode) {
textRequest, err := getAndValidateTextRequest(c, relayInfo)
if err != nil {
- common.LogError(c, fmt.Sprintf("getAndValidateTextRequest failed: %s", err.Error()))
- return service.OpenAIErrorWrapperLocal(err, "invalid_text_request", http.StatusBadRequest)
+ return types.NewError(err, types.ErrorCodeInvalidRequest)
}
if textRequest.WebSearchOptions != nil {
@@ -104,13 +104,13 @@ func TextHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode) {
words, err := checkRequestSensitive(textRequest, relayInfo)
if err != nil {
common.LogWarn(c, fmt.Sprintf("user sensitive words detected: %s", strings.Join(words, ", ")))
- return service.OpenAIErrorWrapperLocal(err, "sensitive_words_detected", http.StatusBadRequest)
+ return types.NewError(err, types.ErrorCodeSensitiveWordsDetected)
}
}
err = helper.ModelMappedHelper(c, relayInfo, textRequest)
if err != nil {
- return service.OpenAIErrorWrapperLocal(err, "model_mapped_error", http.StatusInternalServerError)
+ return types.NewError(err, types.ErrorCodeChannelModelMappedError)
}
// 获取 promptTokens,如果上下文中已经存在,则直接使用
@@ -122,23 +122,23 @@ func TextHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode) {
promptTokens, err = getPromptTokens(textRequest, relayInfo)
// count messages token error 计算promptTokens错误
if err != nil {
- return service.OpenAIErrorWrapper(err, "count_token_messages_failed", http.StatusInternalServerError)
+ return types.NewError(err, types.ErrorCodeCountTokenFailed)
}
c.Set("prompt_tokens", promptTokens)
}
priceData, err := helper.ModelPriceHelper(c, relayInfo, promptTokens, int(math.Max(float64(textRequest.MaxTokens), float64(textRequest.MaxCompletionTokens))))
if err != nil {
- return service.OpenAIErrorWrapperLocal(err, "model_price_error", http.StatusInternalServerError)
+ return types.NewError(err, types.ErrorCodeModelPriceError)
}
// pre-consume quota 预消耗配额
- preConsumedQuota, userQuota, openaiErr := preConsumeQuota(c, priceData.ShouldPreConsumedQuota, relayInfo)
- if openaiErr != nil {
- return openaiErr
+ preConsumedQuota, userQuota, newApiErr := preConsumeQuota(c, priceData.ShouldPreConsumedQuota, relayInfo)
+ if newApiErr != nil {
+ return newApiErr
}
defer func() {
- if openaiErr != nil {
+ if newApiErr != nil {
returnPreConsumedQuota(c, relayInfo, userQuota, preConsumedQuota)
}
}()
@@ -166,7 +166,7 @@ func TextHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode) {
adaptor := GetAdaptor(relayInfo.ApiType)
if adaptor == nil {
- return service.OpenAIErrorWrapperLocal(fmt.Errorf("invalid api type: %d", relayInfo.ApiType), "invalid_api_type", http.StatusBadRequest)
+ return types.NewError(fmt.Errorf("invalid api type: %d", relayInfo.ApiType), types.ErrorCodeInvalidApiType)
}
adaptor.Init(relayInfo)
var requestBody io.Reader
@@ -174,32 +174,29 @@ func TextHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode) {
if model_setting.GetGlobalSettings().PassThroughRequestEnabled {
body, err := common.GetRequestBody(c)
if err != nil {
- return service.OpenAIErrorWrapperLocal(err, "get_request_body_failed", http.StatusInternalServerError)
+ return types.NewErrorWithStatusCode(err, types.ErrorCodeReadRequestBodyFailed, http.StatusBadRequest)
}
requestBody = bytes.NewBuffer(body)
} else {
convertedRequest, err := adaptor.ConvertOpenAIRequest(c, relayInfo, textRequest)
if err != nil {
- return service.OpenAIErrorWrapperLocal(err, "convert_request_failed", http.StatusInternalServerError)
+ return types.NewError(err, types.ErrorCodeConvertRequestFailed)
}
jsonData, err := json.Marshal(convertedRequest)
if err != nil {
- return service.OpenAIErrorWrapperLocal(err, "json_marshal_failed", http.StatusInternalServerError)
+ return types.NewError(err, types.ErrorCodeConvertRequestFailed)
}
// apply param override
if len(relayInfo.ParamOverride) > 0 {
reqMap := make(map[string]interface{})
- err = json.Unmarshal(jsonData, &reqMap)
- if err != nil {
- return service.OpenAIErrorWrapperLocal(err, "param_override_unmarshal_failed", http.StatusInternalServerError)
- }
+ _ = common.Unmarshal(jsonData, &reqMap)
for key, value := range relayInfo.ParamOverride {
reqMap[key] = value
}
- jsonData, err = json.Marshal(reqMap)
+ jsonData, err = common.Marshal(reqMap)
if err != nil {
- return service.OpenAIErrorWrapperLocal(err, "param_override_marshal_failed", http.StatusInternalServerError)
+ return types.NewError(err, types.ErrorCodeChannelParamOverrideInvalid)
}
}
@@ -213,7 +210,7 @@ func TextHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode) {
resp, err := adaptor.DoRequest(c, relayInfo, requestBody)
if err != nil {
- return service.OpenAIErrorWrapper(err, "do_request_failed", http.StatusInternalServerError)
+ return types.NewOpenAIError(err, types.ErrorCodeDoRequestFailed, http.StatusInternalServerError)
}
statusCodeMappingStr := c.GetString("status_code_mapping")
@@ -222,18 +219,18 @@ func TextHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode) {
httpResp = resp.(*http.Response)
relayInfo.IsStream = relayInfo.IsStream || strings.HasPrefix(httpResp.Header.Get("Content-Type"), "text/event-stream")
if httpResp.StatusCode != http.StatusOK {
- openaiErr = service.RelayErrorHandler(httpResp, false)
+ newApiErr = service.RelayErrorHandler(httpResp, false)
// reset status code 重置状态码
- service.ResetStatusCode(openaiErr, statusCodeMappingStr)
- return openaiErr
+ service.ResetStatusCode(newApiErr, statusCodeMappingStr)
+ return newApiErr
}
}
- usage, openaiErr := adaptor.DoResponse(c, httpResp, relayInfo)
- if openaiErr != nil {
+ usage, newApiErr := adaptor.DoResponse(c, httpResp, relayInfo)
+ if newApiErr != nil {
// reset status code 重置状态码
- service.ResetStatusCode(openaiErr, statusCodeMappingStr)
- return openaiErr
+ service.ResetStatusCode(newApiErr, statusCodeMappingStr)
+ return newApiErr
}
if strings.HasPrefix(relayInfo.OriginModelName, "gpt-4o-audio") {
@@ -281,16 +278,16 @@ func checkRequestSensitive(textRequest *dto.GeneralOpenAIRequest, info *relaycom
}
// 预扣费并返回用户剩余配额
-func preConsumeQuota(c *gin.Context, preConsumedQuota int, relayInfo *relaycommon.RelayInfo) (int, int, *dto.OpenAIErrorWithStatusCode) {
+func preConsumeQuota(c *gin.Context, preConsumedQuota int, relayInfo *relaycommon.RelayInfo) (int, int, *types.NewAPIError) {
userQuota, err := model.GetUserQuota(relayInfo.UserId, false)
if err != nil {
- return 0, 0, service.OpenAIErrorWrapperLocal(err, "get_user_quota_failed", http.StatusInternalServerError)
+ return 0, 0, types.NewError(err, types.ErrorCodeQueryDataError)
}
if userQuota <= 0 {
- return 0, 0, service.OpenAIErrorWrapperLocal(errors.New("user quota is not enough"), "insufficient_user_quota", http.StatusForbidden)
+ return 0, 0, types.NewErrorWithStatusCode(errors.New("user quota is not enough"), types.ErrorCodeInsufficientUserQuota, http.StatusForbidden)
}
if userQuota-preConsumedQuota < 0 {
- return 0, 0, service.OpenAIErrorWrapperLocal(fmt.Errorf("chat pre-consumed quota failed, user quota: %s, need quota: %s", common.FormatQuota(userQuota), common.FormatQuota(preConsumedQuota)), "insufficient_user_quota", http.StatusForbidden)
+ return 0, 0, types.NewErrorWithStatusCode(fmt.Errorf("pre-consume quota failed, user quota: %s, need quota: %s", common.FormatQuota(userQuota), common.FormatQuota(preConsumedQuota)), types.ErrorCodeInsufficientUserQuota, http.StatusForbidden)
}
relayInfo.UserQuota = userQuota
if userQuota > 100*preConsumedQuota {
@@ -314,11 +311,11 @@ func preConsumeQuota(c *gin.Context, preConsumedQuota int, relayInfo *relaycommo
if preConsumedQuota > 0 {
err := service.PreConsumeTokenQuota(relayInfo, preConsumedQuota)
if err != nil {
- return 0, 0, service.OpenAIErrorWrapperLocal(err, "pre_consume_token_quota_failed", http.StatusForbidden)
+ return 0, 0, types.NewErrorWithStatusCode(err, types.ErrorCodePreConsumeTokenQuotaFailed, http.StatusForbidden)
}
err = model.DecreaseUserQuota(relayInfo.UserId, preConsumedQuota)
if err != nil {
- return 0, 0, service.OpenAIErrorWrapperLocal(err, "decrease_user_quota_failed", http.StatusInternalServerError)
+ return 0, 0, types.NewError(err, types.ErrorCodeUpdateDataError)
}
}
return preConsumedQuota, userQuota, nil
@@ -382,6 +379,7 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo,
// openai web search 工具计费
var dWebSearchQuota decimal.Decimal
var webSearchPrice float64
+ // response api 格式工具计费
if relayInfo.ResponsesUsageInfo != nil {
if webSearchTool, exists := relayInfo.ResponsesUsageInfo.BuiltInTools[dto.BuildInToolWebSearchPreview]; exists && webSearchTool.CallCount > 0 {
// 计算 web search 调用的配额 (配额 = 价格 * 调用次数 / 1000 * 分组倍率)
@@ -404,6 +402,17 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo,
extraContent += fmt.Sprintf("Web Search 调用 1 次,上下文大小 %s,调用花费 %s",
searchContextSize, dWebSearchQuota.String())
}
+ // claude web search tool 计费
+ var dClaudeWebSearchQuota decimal.Decimal
+ var claudeWebSearchPrice float64
+ claudeWebSearchCallCount := ctx.GetInt("claude_web_search_requests")
+ if claudeWebSearchCallCount > 0 {
+ claudeWebSearchPrice = operation_setting.GetClaudeWebSearchPricePerThousand()
+ dClaudeWebSearchQuota = decimal.NewFromFloat(claudeWebSearchPrice).
+ Div(decimal.NewFromInt(1000)).Mul(dGroupRatio).Mul(dQuotaPerUnit).Mul(decimal.NewFromInt(int64(claudeWebSearchCallCount)))
+ extraContent += fmt.Sprintf("Claude Web Search 调用 %d 次,调用花费 %s",
+ claudeWebSearchCallCount, dClaudeWebSearchQuota.String())
+ }
// file search tool 计费
var dFileSearchQuota decimal.Decimal
var fileSearchPrice float64
@@ -527,6 +536,10 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo,
other["web_search_call_count"] = 1
other["web_search_price"] = webSearchPrice
}
+ } else if !dClaudeWebSearchQuota.IsZero() {
+ other["web_search"] = true
+ other["web_search_call_count"] = claudeWebSearchCallCount
+ other["web_search_price"] = claudeWebSearchPrice
}
if !dFileSearchQuota.IsZero() && relayInfo.ResponsesUsageInfo != nil {
if fileSearchTool, exists := relayInfo.ResponsesUsageInfo.BuiltInTools[dto.BuildInToolFileSearch]; exists {
diff --git a/relay/relay_adaptor.go b/relay/relay_adaptor.go
index 00e59eacb5a..2ce12a872a1 100644
--- a/relay/relay_adaptor.go
+++ b/relay/relay_adaptor.go
@@ -15,6 +15,7 @@ import (
"one-api/relay/channel/deepseek"
"one-api/relay/channel/dify"
"one-api/relay/channel/gemini"
+ "one-api/relay/channel/jimeng"
"one-api/relay/channel/jina"
"one-api/relay/channel/mistral"
"one-api/relay/channel/mokaai"
@@ -23,7 +24,7 @@ import (
"one-api/relay/channel/palm"
"one-api/relay/channel/perplexity"
"one-api/relay/channel/siliconflow"
- "one-api/relay/channel/task/jimeng"
+ taskjimeng "one-api/relay/channel/task/jimeng"
"one-api/relay/channel/task/kling"
"one-api/relay/channel/task/suno"
"one-api/relay/channel/tencent"
@@ -93,6 +94,8 @@ func GetAdaptor(apiType int) channel.Adaptor {
return &xai.Adaptor{}
case constant.APITypeCoze:
return &coze.Adaptor{}
+ case constant.APITypeJimeng:
+ return &jimeng.Adaptor{}
}
return nil
}
@@ -106,7 +109,7 @@ func GetTaskAdaptor(platform commonconstant.TaskPlatform) channel.TaskAdaptor {
case commonconstant.TaskPlatformKling:
return &kling.TaskAdaptor{}
case commonconstant.TaskPlatformJimeng:
- return &jimeng.TaskAdaptor{}
+ return &taskjimeng.TaskAdaptor{}
}
return nil
}
diff --git a/relay/relay_task.go b/relay/relay_task.go
index ce6b93ce4db..25f63d40e08 100644
--- a/relay/relay_task.go
+++ b/relay/relay_task.go
@@ -37,9 +37,9 @@ func RelayTaskSubmit(c *gin.Context, relayMode int) (taskErr *dto.TaskError) {
return
}
- modelName := service.CoverTaskActionToModelName(platform, relayInfo.Action)
- if platform == constant.TaskPlatformKling {
- modelName = relayInfo.OriginModelName
+ modelName := relayInfo.OriginModelName
+ if modelName == "" {
+ modelName = service.CoverTaskActionToModelName(platform, relayInfo.Action)
}
modelPrice, success := ratio_setting.GetModelPrice(modelName, true)
if !success {
diff --git a/relay/rerank_handler.go b/relay/rerank_handler.go
index 5cf384a85b2..a092de4bfc9 100644
--- a/relay/rerank_handler.go
+++ b/relay/rerank_handler.go
@@ -2,15 +2,16 @@ package relay
import (
"bytes"
- "encoding/json"
"fmt"
- "github.com/gin-gonic/gin"
"net/http"
"one-api/common"
"one-api/dto"
relaycommon "one-api/relay/common"
"one-api/relay/helper"
"one-api/service"
+ "one-api/types"
+
+ "github.com/gin-gonic/gin"
)
func getRerankPromptToken(rerankRequest dto.RerankRequest) int {
@@ -22,27 +23,27 @@ func getRerankPromptToken(rerankRequest dto.RerankRequest) int {
return token
}
-func RerankHelper(c *gin.Context, relayMode int) (openaiErr *dto.OpenAIErrorWithStatusCode) {
+func RerankHelper(c *gin.Context, relayMode int) (newAPIError *types.NewAPIError) {
var rerankRequest *dto.RerankRequest
err := common.UnmarshalBodyReusable(c, &rerankRequest)
if err != nil {
common.LogError(c, fmt.Sprintf("getAndValidateTextRequest failed: %s", err.Error()))
- return service.OpenAIErrorWrapperLocal(err, "invalid_text_request", http.StatusBadRequest)
+ return types.NewError(err, types.ErrorCodeInvalidRequest)
}
relayInfo := relaycommon.GenRelayInfoRerank(c, rerankRequest)
if rerankRequest.Query == "" {
- return service.OpenAIErrorWrapperLocal(fmt.Errorf("query is empty"), "invalid_query", http.StatusBadRequest)
+ return types.NewError(fmt.Errorf("query is empty"), types.ErrorCodeInvalidRequest)
}
if len(rerankRequest.Documents) == 0 {
- return service.OpenAIErrorWrapperLocal(fmt.Errorf("documents is empty"), "invalid_documents", http.StatusBadRequest)
+ return types.NewError(fmt.Errorf("documents is empty"), types.ErrorCodeInvalidRequest)
}
err = helper.ModelMappedHelper(c, relayInfo, rerankRequest)
if err != nil {
- return service.OpenAIErrorWrapperLocal(err, "model_mapped_error", http.StatusInternalServerError)
+ return types.NewError(err, types.ErrorCodeChannelModelMappedError)
}
promptToken := getRerankPromptToken(*rerankRequest)
@@ -50,32 +51,32 @@ func RerankHelper(c *gin.Context, relayMode int) (openaiErr *dto.OpenAIErrorWith
priceData, err := helper.ModelPriceHelper(c, relayInfo, promptToken, 0)
if err != nil {
- return service.OpenAIErrorWrapperLocal(err, "model_price_error", http.StatusInternalServerError)
+ return types.NewError(err, types.ErrorCodeModelPriceError)
}
// pre-consume quota 预消耗配额
- preConsumedQuota, userQuota, openaiErr := preConsumeQuota(c, priceData.ShouldPreConsumedQuota, relayInfo)
- if openaiErr != nil {
- return openaiErr
+ preConsumedQuota, userQuota, newAPIError := preConsumeQuota(c, priceData.ShouldPreConsumedQuota, relayInfo)
+ if newAPIError != nil {
+ return newAPIError
}
defer func() {
- if openaiErr != nil {
+ if newAPIError != nil {
returnPreConsumedQuota(c, relayInfo, userQuota, preConsumedQuota)
}
}()
adaptor := GetAdaptor(relayInfo.ApiType)
if adaptor == nil {
- return service.OpenAIErrorWrapperLocal(fmt.Errorf("invalid api type: %d", relayInfo.ApiType), "invalid_api_type", http.StatusBadRequest)
+ return types.NewError(fmt.Errorf("invalid api type: %d", relayInfo.ApiType), types.ErrorCodeInvalidApiType)
}
adaptor.Init(relayInfo)
convertedRequest, err := adaptor.ConvertRerankRequest(c, relayInfo.RelayMode, *rerankRequest)
if err != nil {
- return service.OpenAIErrorWrapperLocal(err, "convert_request_failed", http.StatusInternalServerError)
+ return types.NewError(err, types.ErrorCodeConvertRequestFailed)
}
- jsonData, err := json.Marshal(convertedRequest)
+ jsonData, err := common.Marshal(convertedRequest)
if err != nil {
- return service.OpenAIErrorWrapperLocal(err, "json_marshal_failed", http.StatusInternalServerError)
+ return types.NewError(err, types.ErrorCodeConvertRequestFailed)
}
requestBody := bytes.NewBuffer(jsonData)
if common.DebugEnabled {
@@ -83,7 +84,7 @@ func RerankHelper(c *gin.Context, relayMode int) (openaiErr *dto.OpenAIErrorWith
}
resp, err := adaptor.DoRequest(c, relayInfo, requestBody)
if err != nil {
- return service.OpenAIErrorWrapper(err, "do_request_failed", http.StatusInternalServerError)
+ return types.NewOpenAIError(err, types.ErrorCodeDoRequestFailed, http.StatusInternalServerError)
}
statusCodeMappingStr := c.GetString("status_code_mapping")
@@ -91,18 +92,18 @@ func RerankHelper(c *gin.Context, relayMode int) (openaiErr *dto.OpenAIErrorWith
if resp != nil {
httpResp = resp.(*http.Response)
if httpResp.StatusCode != http.StatusOK {
- openaiErr = service.RelayErrorHandler(httpResp, false)
+ newAPIError = service.RelayErrorHandler(httpResp, false)
// reset status code 重置状态码
- service.ResetStatusCode(openaiErr, statusCodeMappingStr)
- return openaiErr
+ service.ResetStatusCode(newAPIError, statusCodeMappingStr)
+ return newAPIError
}
}
- usage, openaiErr := adaptor.DoResponse(c, httpResp, relayInfo)
- if openaiErr != nil {
+ usage, newAPIError := adaptor.DoResponse(c, httpResp, relayInfo)
+ if newAPIError != nil {
// reset status code 重置状态码
- service.ResetStatusCode(openaiErr, statusCodeMappingStr)
- return openaiErr
+ service.ResetStatusCode(newAPIError, statusCodeMappingStr)
+ return newAPIError
}
postConsumeQuota(c, relayInfo, usage.(*dto.Usage), preConsumedQuota, userQuota, priceData, "")
return nil
diff --git a/relay/responses_handler.go b/relay/responses_handler.go
index e744e354760..52d1db6efb2 100644
--- a/relay/responses_handler.go
+++ b/relay/responses_handler.go
@@ -14,6 +14,7 @@ import (
"one-api/service"
"one-api/setting"
"one-api/setting/model_setting"
+ "one-api/types"
"strings"
"github.com/gin-gonic/gin"
@@ -46,11 +47,11 @@ func getInputTokens(req *dto.OpenAIResponsesRequest, info *relaycommon.RelayInfo
return inputTokens
}
-func ResponsesHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode) {
+func ResponsesHelper(c *gin.Context) (newAPIError *types.NewAPIError) {
req, err := getAndValidateResponsesRequest(c)
if err != nil {
common.LogError(c, fmt.Sprintf("getAndValidateResponsesRequest error: %s", err.Error()))
- return service.OpenAIErrorWrapperLocal(err, "invalid_responses_request", http.StatusBadRequest)
+ return types.NewError(err, types.ErrorCodeInvalidRequest)
}
relayInfo := relaycommon.GenRelayInfoResponses(c, req)
@@ -59,13 +60,13 @@ func ResponsesHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode)
sensitiveWords, err := checkInputSensitive(req, relayInfo)
if err != nil {
common.LogWarn(c, fmt.Sprintf("user sensitive words detected: %s", strings.Join(sensitiveWords, ", ")))
- return service.OpenAIErrorWrapperLocal(err, "check_request_sensitive_error", http.StatusBadRequest)
+ return types.NewError(err, types.ErrorCodeSensitiveWordsDetected)
}
}
err = helper.ModelMappedHelper(c, relayInfo, req)
if err != nil {
- return service.OpenAIErrorWrapperLocal(err, "model_mapped_error", http.StatusBadRequest)
+ return types.NewError(err, types.ErrorCodeChannelModelMappedError)
}
if value, exists := c.Get("prompt_tokens"); exists {
@@ -78,52 +79,52 @@ func ResponsesHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode)
priceData, err := helper.ModelPriceHelper(c, relayInfo, relayInfo.PromptTokens, int(req.MaxOutputTokens))
if err != nil {
- return service.OpenAIErrorWrapperLocal(err, "model_price_error", http.StatusInternalServerError)
+ return types.NewError(err, types.ErrorCodeModelPriceError)
}
// pre consume quota
- preConsumedQuota, userQuota, openaiErr := preConsumeQuota(c, priceData.ShouldPreConsumedQuota, relayInfo)
- if openaiErr != nil {
- return openaiErr
+ preConsumedQuota, userQuota, newAPIError := preConsumeQuota(c, priceData.ShouldPreConsumedQuota, relayInfo)
+ if newAPIError != nil {
+ return newAPIError
}
defer func() {
- if openaiErr != nil {
+ if newAPIError != nil {
returnPreConsumedQuota(c, relayInfo, userQuota, preConsumedQuota)
}
}()
adaptor := GetAdaptor(relayInfo.ApiType)
if adaptor == nil {
- return service.OpenAIErrorWrapperLocal(fmt.Errorf("invalid api type: %d", relayInfo.ApiType), "invalid_api_type", http.StatusBadRequest)
+ return types.NewError(fmt.Errorf("invalid api type: %d", relayInfo.ApiType), types.ErrorCodeInvalidApiType)
}
adaptor.Init(relayInfo)
var requestBody io.Reader
if model_setting.GetGlobalSettings().PassThroughRequestEnabled {
body, err := common.GetRequestBody(c)
if err != nil {
- return service.OpenAIErrorWrapperLocal(err, "get_request_body_error", http.StatusInternalServerError)
+ return types.NewError(err, types.ErrorCodeReadRequestBodyFailed)
}
requestBody = bytes.NewBuffer(body)
} else {
convertedRequest, err := adaptor.ConvertOpenAIResponsesRequest(c, relayInfo, *req)
if err != nil {
- return service.OpenAIErrorWrapperLocal(err, "convert_request_error", http.StatusBadRequest)
+ return types.NewError(err, types.ErrorCodeConvertRequestFailed)
}
jsonData, err := json.Marshal(convertedRequest)
if err != nil {
- return service.OpenAIErrorWrapperLocal(err, "marshal_request_error", http.StatusInternalServerError)
+ return types.NewError(err, types.ErrorCodeConvertRequestFailed)
}
// apply param override
if len(relayInfo.ParamOverride) > 0 {
reqMap := make(map[string]interface{})
err = json.Unmarshal(jsonData, &reqMap)
if err != nil {
- return service.OpenAIErrorWrapperLocal(err, "param_override_unmarshal_failed", http.StatusInternalServerError)
+ return types.NewError(err, types.ErrorCodeChannelParamOverrideInvalid)
}
for key, value := range relayInfo.ParamOverride {
reqMap[key] = value
}
jsonData, err = json.Marshal(reqMap)
if err != nil {
- return service.OpenAIErrorWrapperLocal(err, "param_override_marshal_failed", http.StatusInternalServerError)
+ return types.NewError(err, types.ErrorCodeConvertRequestFailed)
}
}
@@ -136,7 +137,7 @@ func ResponsesHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode)
var httpResp *http.Response
resp, err := adaptor.DoRequest(c, relayInfo, requestBody)
if err != nil {
- return service.OpenAIErrorWrapper(err, "do_request_failed", http.StatusInternalServerError)
+ return types.NewOpenAIError(err, types.ErrorCodeDoRequestFailed, http.StatusInternalServerError)
}
statusCodeMappingStr := c.GetString("status_code_mapping")
@@ -145,18 +146,18 @@ func ResponsesHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode)
httpResp = resp.(*http.Response)
if httpResp.StatusCode != http.StatusOK {
- openaiErr = service.RelayErrorHandler(httpResp, false)
+ newAPIError = service.RelayErrorHandler(httpResp, false)
// reset status code 重置状态码
- service.ResetStatusCode(openaiErr, statusCodeMappingStr)
- return openaiErr
+ service.ResetStatusCode(newAPIError, statusCodeMappingStr)
+ return newAPIError
}
}
- usage, openaiErr := adaptor.DoResponse(c, httpResp, relayInfo)
- if openaiErr != nil {
+ usage, newAPIError := adaptor.DoResponse(c, httpResp, relayInfo)
+ if newAPIError != nil {
// reset status code 重置状态码
- service.ResetStatusCode(openaiErr, statusCodeMappingStr)
- return openaiErr
+ service.ResetStatusCode(newAPIError, statusCodeMappingStr)
+ return newAPIError
}
if strings.HasPrefix(relayInfo.OriginModelName, "gpt-4o-audio") {
diff --git a/relay/websocket.go b/relay/websocket.go
index 571f3a82497..659e27d566c 100644
--- a/relay/websocket.go
+++ b/relay/websocket.go
@@ -1,18 +1,18 @@
package relay
import (
- "encoding/json"
"fmt"
- "github.com/gin-gonic/gin"
- "github.com/gorilla/websocket"
- "net/http"
"one-api/dto"
relaycommon "one-api/relay/common"
"one-api/relay/helper"
"one-api/service"
+ "one-api/types"
+
+ "github.com/gin-gonic/gin"
+ "github.com/gorilla/websocket"
)
-func WssHelper(c *gin.Context, ws *websocket.Conn) (openaiErr *dto.OpenAIErrorWithStatusCode) {
+func WssHelper(c *gin.Context, ws *websocket.Conn) (newAPIError *types.NewAPIError) {
relayInfo := relaycommon.GenRelayInfoWs(c, ws)
// get & validate textRequest 获取并验证文本请求
@@ -22,42 +22,31 @@ func WssHelper(c *gin.Context, ws *websocket.Conn) (openaiErr *dto.OpenAIErrorWi
// return service.OpenAIErrorWrapperLocal(err, "invalid_text_request", http.StatusBadRequest)
//}
- // map model name
- modelMapping := c.GetString("model_mapping")
- //isModelMapped := false
- if modelMapping != "" && modelMapping != "{}" {
- modelMap := make(map[string]string)
- err := json.Unmarshal([]byte(modelMapping), &modelMap)
- if err != nil {
- return service.OpenAIErrorWrapperLocal(err, "unmarshal_model_mapping_failed", http.StatusInternalServerError)
- }
- if modelMap[relayInfo.OriginModelName] != "" {
- relayInfo.UpstreamModelName = modelMap[relayInfo.OriginModelName]
- // set upstream model name
- //isModelMapped = true
- }
+ err := helper.ModelMappedHelper(c, relayInfo, nil)
+ if err != nil {
+ return types.NewError(err, types.ErrorCodeChannelModelMappedError)
}
priceData, err := helper.ModelPriceHelper(c, relayInfo, 0, 0)
if err != nil {
- return service.OpenAIErrorWrapperLocal(err, "model_price_error", http.StatusInternalServerError)
+ return types.NewError(err, types.ErrorCodeModelPriceError)
}
// pre-consume quota 预消耗配额
- preConsumedQuota, userQuota, openaiErr := preConsumeQuota(c, priceData.ShouldPreConsumedQuota, relayInfo)
- if openaiErr != nil {
- return openaiErr
+ preConsumedQuota, userQuota, newAPIError := preConsumeQuota(c, priceData.ShouldPreConsumedQuota, relayInfo)
+ if newAPIError != nil {
+ return newAPIError
}
defer func() {
- if openaiErr != nil {
+ if newAPIError != nil {
returnPreConsumedQuota(c, relayInfo, userQuota, preConsumedQuota)
}
}()
adaptor := GetAdaptor(relayInfo.ApiType)
if adaptor == nil {
- return service.OpenAIErrorWrapperLocal(fmt.Errorf("invalid api type: %d", relayInfo.ApiType), "invalid_api_type", http.StatusBadRequest)
+ return types.NewError(fmt.Errorf("invalid api type: %d", relayInfo.ApiType), types.ErrorCodeInvalidApiType)
}
adaptor.Init(relayInfo)
//var requestBody io.Reader
@@ -67,7 +56,7 @@ func WssHelper(c *gin.Context, ws *websocket.Conn) (openaiErr *dto.OpenAIErrorWi
statusCodeMappingStr := c.GetString("status_code_mapping")
resp, err := adaptor.DoRequest(c, relayInfo, nil)
if err != nil {
- return service.OpenAIErrorWrapper(err, "do_request_failed", http.StatusInternalServerError)
+ return types.NewError(err, types.ErrorCodeDoRequestFailed)
}
if resp != nil {
@@ -75,11 +64,11 @@ func WssHelper(c *gin.Context, ws *websocket.Conn) (openaiErr *dto.OpenAIErrorWi
defer relayInfo.TargetWs.Close()
}
- usage, openaiErr := adaptor.DoResponse(c, nil, relayInfo)
- if openaiErr != nil {
+ usage, newAPIError := adaptor.DoResponse(c, nil, relayInfo)
+ if newAPIError != nil {
// reset status code 重置状态码
- service.ResetStatusCode(openaiErr, statusCodeMappingStr)
- return openaiErr
+ service.ResetStatusCode(newAPIError, statusCodeMappingStr)
+ return newAPIError
}
service.PostWssConsumeQuota(c, relayInfo, relayInfo.UpstreamModelName, usage.(*dto.RealtimeUsage), preConsumedQuota,
userQuota, priceData, "")
diff --git a/router/api-router.go b/router/api-router.go
index db4c3898524..bc49803a2e0 100644
--- a/router/api-router.go
+++ b/router/api-router.go
@@ -38,6 +38,8 @@ func SetApiRouter(router *gin.Engine) {
apiRouter.GET("/oauth/telegram/bind", middleware.CriticalRateLimit(), controller.TelegramBind)
apiRouter.GET("/ratio_config", middleware.CriticalRateLimit(), controller.GetRatioConfig)
+ apiRouter.POST("/stripe/webhook", controller.StripeWebhook)
+
userRoute := apiRouter.Group("/user")
{
userRoute.POST("/register", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.Register)
@@ -57,9 +59,11 @@ func SetApiRouter(router *gin.Engine) {
selfRoute.DELETE("/self", controller.DeleteSelf)
selfRoute.GET("/token", controller.GenerateAccessToken)
selfRoute.GET("/aff", controller.GetAffCode)
- selfRoute.POST("/topup", controller.TopUp)
- selfRoute.POST("/pay", controller.RequestEpay)
+ selfRoute.POST("/topup", middleware.CriticalRateLimit(), controller.TopUp)
+ selfRoute.POST("/pay", middleware.CriticalRateLimit(), controller.RequestEpay)
selfRoute.POST("/amount", controller.RequestAmount)
+ selfRoute.POST("/stripe/pay", middleware.CriticalRateLimit(), controller.RequestStripePay)
+ selfRoute.POST("/stripe/amount", controller.RequestStripeAmount)
selfRoute.POST("/aff_transfer", controller.TransferAffQuota)
selfRoute.PUT("/setting", controller.UpdateUserSetting)
}
@@ -115,6 +119,7 @@ func SetApiRouter(router *gin.Engine) {
channelRoute.POST("/fetch_models", controller.FetchModels)
channelRoute.POST("/batch/tag", controller.BatchSetChannelTag)
channelRoute.GET("/tag/models", controller.GetTagModels)
+ channelRoute.POST("/copy/:id", controller.CopyChannel)
}
tokenRoute := apiRouter.Group("/token")
tokenRoute.Use(middleware.UserAuth())
diff --git a/router/relay-router.go b/router/relay-router.go
index b48c9dc7003..5b293dbdcdc 100644
--- a/router/relay-router.go
+++ b/router/relay-router.go
@@ -20,7 +20,7 @@ func SetRelayRouter(router *gin.Engine) {
modelsRouter.GET("/:model", controller.RetrieveModel)
}
playgroundRouter := router.Group("/pg")
- playgroundRouter.Use(middleware.UserAuth())
+ playgroundRouter.Use(middleware.UserAuth(), middleware.Distribute())
{
playgroundRouter.POST("/chat/completions", controller.Playground)
}
diff --git a/service/channel.go b/service/channel.go
index d50de78d92a..4d38e6edc7e 100644
--- a/service/channel.go
+++ b/service/channel.go
@@ -8,6 +8,7 @@ import (
"one-api/dto"
"one-api/model"
"one-api/setting/operation_setting"
+ "one-api/types"
"strings"
)
@@ -16,17 +17,17 @@ func formatNotifyType(channelId int, status int) string {
}
// disable & notify
-func DisableChannel(channelId int, channelName string, reason string) {
- success := model.UpdateChannelStatusById(channelId, common.ChannelStatusAutoDisabled, reason)
+func DisableChannel(channelError types.ChannelError, reason string) {
+ success := model.UpdateChannelStatus(channelError.ChannelId, channelError.UsingKey, common.ChannelStatusAutoDisabled, reason)
if success {
- subject := fmt.Sprintf("通道「%s」(#%d)已被禁用", channelName, channelId)
- content := fmt.Sprintf("通道「%s」(#%d)已被禁用,原因:%s", channelName, channelId, reason)
- NotifyRootUser(formatNotifyType(channelId, common.ChannelStatusAutoDisabled), subject, content)
+ subject := fmt.Sprintf("通道「%s」(#%d)已被禁用", channelError.ChannelName, channelError.ChannelId)
+ content := fmt.Sprintf("通道「%s」(#%d)已被禁用,原因:%s", channelError.ChannelName, channelError.ChannelId, reason)
+ NotifyRootUser(formatNotifyType(channelError.ChannelId, common.ChannelStatusAutoDisabled), subject, content)
}
}
-func EnableChannel(channelId int, channelName string) {
- success := model.UpdateChannelStatusById(channelId, common.ChannelStatusEnabled, "")
+func EnableChannel(channelId int, usingKey string, channelName string) {
+ success := model.UpdateChannelStatus(channelId, usingKey, common.ChannelStatusEnabled, "")
if success {
subject := fmt.Sprintf("通道「%s」(#%d)已被启用", channelName, channelId)
content := fmt.Sprintf("通道「%s」(#%d)已被启用", channelName, channelId)
@@ -34,14 +35,17 @@ func EnableChannel(channelId int, channelName string) {
}
}
-func ShouldDisableChannel(channelType int, err *dto.OpenAIErrorWithStatusCode) bool {
+func ShouldDisableChannel(channelType int, err *types.NewAPIError) bool {
if !common.AutomaticDisableChannelEnabled {
return false
}
if err == nil {
return false
}
- if err.LocalError {
+ if types.IsChannelError(err) {
+ return true
+ }
+ if types.IsLocalError(err) {
return false
}
if err.StatusCode == http.StatusUnauthorized {
@@ -53,7 +57,8 @@ func ShouldDisableChannel(channelType int, err *dto.OpenAIErrorWithStatusCode) b
return true
}
}
- switch err.Error.Code {
+ oaiErr := err.ToOpenAIError()
+ switch oaiErr.Code {
case "invalid_api_key":
return true
case "account_deactivated":
@@ -63,7 +68,7 @@ func ShouldDisableChannel(channelType int, err *dto.OpenAIErrorWithStatusCode) b
case "pre_consume_token_quota_failed":
return true
}
- switch err.Error.Type {
+ switch oaiErr.Type {
case "insufficient_quota":
return true
case "insufficient_user_quota":
@@ -77,23 +82,16 @@ func ShouldDisableChannel(channelType int, err *dto.OpenAIErrorWithStatusCode) b
return true
}
- lowerMessage := strings.ToLower(err.Error.Message)
+ lowerMessage := strings.ToLower(err.Error())
search, _ := AcSearch(lowerMessage, operation_setting.AutomaticDisableKeywords, true)
- if search {
- return true
- }
-
- return false
+ return search
}
-func ShouldEnableChannel(err error, openaiWithStatusErr *dto.OpenAIErrorWithStatusCode, status int) bool {
+func ShouldEnableChannel(newAPIError *types.NewAPIError, status int) bool {
if !common.AutomaticEnableChannelEnabled {
return false
}
- if err != nil {
- return false
- }
- if openaiWithStatusErr != nil {
+ if newAPIError != nil {
return false
}
if status != common.ChannelStatusAutoDisabled {
diff --git a/service/convert.go b/service/convert.go
index c97f8475195..593b59d94f4 100644
--- a/service/convert.go
+++ b/service/convert.go
@@ -163,7 +163,7 @@ func ClaudeToOpenAIRequest(claudeRequest dto.ClaudeRequest, info *relaycommon.Re
oaiToolMessage.SetStringContent(mediaMsg.GetStringContent())
} else {
mediaContents := mediaMsg.ParseMediaContent()
- encodeJson, _ := common.EncodeJson(mediaContents)
+ encodeJson, _ := common.Marshal(mediaContents)
oaiToolMessage.SetStringContent(string(encodeJson))
}
openAIMessages = append(openAIMessages, oaiToolMessage)
diff --git a/service/error.go b/service/error.go
index 21835f2a4fa..a0713b55ba8 100644
--- a/service/error.go
+++ b/service/error.go
@@ -2,11 +2,13 @@ package service
import (
"encoding/json"
+ "errors"
"fmt"
"io"
"net/http"
"one-api/common"
"one-api/dto"
+ "one-api/types"
"strconv"
"strings"
)
@@ -25,32 +27,32 @@ func MidjourneyErrorWithStatusCodeWrapper(code int, desc string, statusCode int)
}
}
-// OpenAIErrorWrapper wraps an error into an OpenAIErrorWithStatusCode
-func OpenAIErrorWrapper(err error, code string, statusCode int) *dto.OpenAIErrorWithStatusCode {
- text := err.Error()
- lowerText := strings.ToLower(text)
- if !strings.HasPrefix(lowerText, "get file base64 from url") && !strings.HasPrefix(lowerText, "mime type is not supported") {
- if strings.Contains(lowerText, "post") || strings.Contains(lowerText, "dial") || strings.Contains(lowerText, "http") {
- common.SysLog(fmt.Sprintf("error: %s", text))
- text = "请求上游地址失败"
- }
- }
- openAIError := dto.OpenAIError{
- Message: text,
- Type: "new_api_error",
- Code: code,
- }
- return &dto.OpenAIErrorWithStatusCode{
- Error: openAIError,
- StatusCode: statusCode,
- }
-}
-
-func OpenAIErrorWrapperLocal(err error, code string, statusCode int) *dto.OpenAIErrorWithStatusCode {
- openaiErr := OpenAIErrorWrapper(err, code, statusCode)
- openaiErr.LocalError = true
- return openaiErr
-}
+//// OpenAIErrorWrapper wraps an error into an OpenAIErrorWithStatusCode
+//func OpenAIErrorWrapper(err error, code string, statusCode int) *dto.OpenAIErrorWithStatusCode {
+// text := err.Error()
+// lowerText := strings.ToLower(text)
+// if !strings.HasPrefix(lowerText, "get file base64 from url") && !strings.HasPrefix(lowerText, "mime type is not supported") {
+// if strings.Contains(lowerText, "post") || strings.Contains(lowerText, "dial") || strings.Contains(lowerText, "http") {
+// common.SysLog(fmt.Sprintf("error: %s", text))
+// text = "请求上游地址失败"
+// }
+// }
+// openAIError := dto.OpenAIError{
+// Message: text,
+// Type: "new_api_error",
+// Code: code,
+// }
+// return &dto.OpenAIErrorWithStatusCode{
+// Error: openAIError,
+// StatusCode: statusCode,
+// }
+//}
+//
+//func OpenAIErrorWrapperLocal(err error, code string, statusCode int) *dto.OpenAIErrorWithStatusCode {
+// openaiErr := OpenAIErrorWrapper(err, code, statusCode)
+// openaiErr.LocalError = true
+// return openaiErr
+//}
func ClaudeErrorWrapper(err error, code string, statusCode int) *dto.ClaudeErrorWithStatusCode {
text := err.Error()
@@ -77,43 +79,39 @@ func ClaudeErrorWrapperLocal(err error, code string, statusCode int) *dto.Claude
return claudeErr
}
-func RelayErrorHandler(resp *http.Response, showBodyWhenFail bool) (errWithStatusCode *dto.OpenAIErrorWithStatusCode) {
- errWithStatusCode = &dto.OpenAIErrorWithStatusCode{
+func RelayErrorHandler(resp *http.Response, showBodyWhenFail bool) (newApiErr *types.NewAPIError) {
+ newApiErr = &types.NewAPIError{
StatusCode: resp.StatusCode,
- Error: dto.OpenAIError{
- Type: "upstream_error",
- Code: "bad_response_status_code",
- Param: strconv.Itoa(resp.StatusCode),
- },
+ ErrorType: types.ErrorTypeOpenAIError,
}
+
responseBody, err := io.ReadAll(resp.Body)
if err != nil {
return
}
common.CloseResponseBodyGracefully(resp)
var errResponse dto.GeneralErrorResponse
- err = json.Unmarshal(responseBody, &errResponse)
+
+ err = common.Unmarshal(responseBody, &errResponse)
if err != nil {
if showBodyWhenFail {
- errWithStatusCode.Error.Message = string(responseBody)
+ newApiErr.Err = fmt.Errorf("bad response status code %d, body: %s", resp.StatusCode, string(responseBody))
} else {
- errWithStatusCode.Error.Message = fmt.Sprintf("bad response status code %d", resp.StatusCode)
+ newApiErr.Err = fmt.Errorf("bad response status code %d", resp.StatusCode)
}
return
}
if errResponse.Error.Message != "" {
- // OpenAI format error, so we override the default one
- errWithStatusCode.Error = errResponse.Error
+ // General format error (OpenAI, Anthropic, Gemini, etc.)
+ newApiErr = types.WithOpenAIError(errResponse.Error, resp.StatusCode)
} else {
- errWithStatusCode.Error.Message = errResponse.ToMessage()
- }
- if errWithStatusCode.Error.Message == "" {
- errWithStatusCode.Error.Message = fmt.Sprintf("bad response status code %d", resp.StatusCode)
+ newApiErr = types.NewErrorWithStatusCode(errors.New(errResponse.ToMessage()), types.ErrorCodeBadResponseStatusCode, resp.StatusCode)
+ newApiErr.ErrorType = types.ErrorTypeOpenAIError
}
return
}
-func ResetStatusCode(openaiErr *dto.OpenAIErrorWithStatusCode, statusCodeMappingStr string) {
+func ResetStatusCode(newApiErr *types.NewAPIError, statusCodeMappingStr string) {
if statusCodeMappingStr == "" || statusCodeMappingStr == "{}" {
return
}
@@ -122,13 +120,13 @@ func ResetStatusCode(openaiErr *dto.OpenAIErrorWithStatusCode, statusCodeMapping
if err != nil {
return
}
- if openaiErr.StatusCode == http.StatusOK {
+ if newApiErr.StatusCode == http.StatusOK {
return
}
- codeStr := strconv.Itoa(openaiErr.StatusCode)
+ codeStr := strconv.Itoa(newApiErr.StatusCode)
if _, ok := statusCodeMapping[codeStr]; ok {
intCode, _ := strconv.Atoi(statusCodeMapping[codeStr])
- openaiErr.StatusCode = intCode
+ newApiErr.StatusCode = intCode
}
}
diff --git a/service/log_info_generate.go b/service/log_info_generate.go
index affae5fba85..020a2ba9fc2 100644
--- a/service/log_info_generate.go
+++ b/service/log_info_generate.go
@@ -1,6 +1,8 @@
package service
import (
+ "one-api/common"
+ "one-api/constant"
"one-api/dto"
relaycommon "one-api/relay/common"
"one-api/relay/helper"
@@ -28,6 +30,11 @@ func GenerateTextOtherInfo(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, m
}
adminInfo := make(map[string]interface{})
adminInfo["use_channel"] = ctx.GetStringSlice("use_channel")
+ isMultiKey := common.GetContextKeyBool(ctx, constant.ContextKeyChannelIsMultiKey)
+ if isMultiKey {
+ adminInfo["is_multi_key"] = true
+ adminInfo["multi_key_index"] = common.GetContextKeyInt(ctx, constant.ContextKeyChannelMultiKeyIndex)
+ }
other["admin_info"] = adminInfo
return other
}
diff --git a/service/midjourney.go b/service/midjourney.go
index 83404bd98c0..1fc19682263 100644
--- a/service/midjourney.go
+++ b/service/midjourney.go
@@ -204,7 +204,7 @@ func DoMidjourneyHttpRequest(c *gin.Context, timeout time.Duration, fullRequestU
req = req.WithContext(ctx)
req.Header.Set("Content-Type", c.Request.Header.Get("Content-Type"))
req.Header.Set("Accept", c.Request.Header.Get("Accept"))
- auth := c.Request.Header.Get("Authorization")
+ auth := common.GetContextKeyString(c, constant.ContextKeyChannelKey)
if auth != "" {
auth = strings.TrimPrefix(auth, "Bearer ")
req.Header.Set("mj-api-secret", auth)
diff --git a/service/quota.go b/service/quota.go
index 7a6177de34b..0f618402f1c 100644
--- a/service/quota.go
+++ b/service/quota.go
@@ -96,7 +96,7 @@ func PreWssConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usag
audioInputTokens := usage.InputTokenDetails.AudioTokens
audioOutTokens := usage.OutputTokenDetails.AudioTokens
groupRatio := ratio_setting.GetGroupRatio(relayInfo.UsingGroup)
- modelRatio, _ := ratio_setting.GetModelRatio(modelName)
+ modelRatio, _, _ := ratio_setting.GetModelRatio(modelName)
autoGroup, exists := ctx.Get("auto_group")
if exists {
@@ -326,7 +326,7 @@ func CalcOpenRouterCacheCreateTokens(usage dto.Usage, priceData helper.PriceData
promptCacheReadPrice := quotaPrice * priceData.CacheRatio
completionPrice := quotaPrice * priceData.CompletionRatio
- cost := usage.Cost
+ cost, _ := usage.Cost.(float64)
totalPromptTokens := float64(usage.PromptTokens)
completionTokens := float64(usage.CompletionTokens)
promptCacheReadTokens := float64(usage.PromptTokensDetails.CachedTokens)
diff --git a/setting/operation_setting/tools.go b/setting/operation_setting/tools.go
index a401b923fc7..a59090ce0eb 100644
--- a/setting/operation_setting/tools.go
+++ b/setting/operation_setting/tools.go
@@ -23,6 +23,15 @@ const (
Gemini20FlashInputAudioPrice = 0.70
)
+const (
+ // Claude Web search
+ ClaudeWebSearchPrice = 10.00
+)
+
+func GetClaudeWebSearchPricePerThousand() float64 {
+ return ClaudeWebSearchPrice
+}
+
func GetWebSearchPricePerThousand(modelName string, contextSize string) float64 {
// 确定模型类型
// https://platform.openai.com/docs/pricing Web search 价格按模型类型和 search context size 收费
diff --git a/setting/payment.go b/setting/payment.go
index 3fc0f14a09c..7fc5ad3fd35 100644
--- a/setting/payment.go
+++ b/setting/payment.go
@@ -8,6 +8,7 @@ var EpayId = ""
var EpayKey = ""
var Price = 7.3
var MinTopUp = 1
+var USDExchangeRate = 7.3
var PayMethods = []map[string]string{
{
diff --git a/setting/payment_stripe.go b/setting/payment_stripe.go
new file mode 100644
index 00000000000..80d877dfa08
--- /dev/null
+++ b/setting/payment_stripe.go
@@ -0,0 +1,7 @@
+package setting
+
+var StripeApiSecret = ""
+var StripeWebhookSecret = ""
+var StripePriceId = ""
+var StripeUnitPrice = 8.0
+var StripeMinTopUp = 1
diff --git a/setting/ratio_setting/model_ratio.go b/setting/ratio_setting/model_ratio.go
index 033b07a0dd6..8a1d6aaed42 100644
--- a/setting/ratio_setting/model_ratio.go
+++ b/setting/ratio_setting/model_ratio.go
@@ -369,7 +369,7 @@ func handleThinkingBudgetModel(name, prefix, wildcard string) string {
return name
}
-func GetModelRatio(name string) (float64, bool) {
+func GetModelRatio(name string) (float64, bool, string) {
modelRatioMapMutex.RLock()
defer modelRatioMapMutex.RUnlock()
@@ -380,9 +380,9 @@ func GetModelRatio(name string) (float64, bool) {
}
ratio, ok := modelRatioMap[name]
if !ok {
- return 37.5, operation_setting.SelfUseModeEnabled
+ return 37.5, operation_setting.SelfUseModeEnabled, name
}
- return ratio, true
+ return ratio, true, name
}
func DefaultModelRatio2JSONString() string {
diff --git a/setting/user_usable_group.go b/setting/user_usable_group.go
index fdf2f723fdd..0ae132d0e4c 100644
--- a/setting/user_usable_group.go
+++ b/setting/user_usable_group.go
@@ -3,14 +3,19 @@ package setting
import (
"encoding/json"
"one-api/common"
+ "sync"
)
var userUsableGroups = map[string]string{
"default": "默认分组",
"vip": "vip分组",
}
+var userUsableGroupsMutex sync.RWMutex
func GetUserUsableGroupsCopy() map[string]string {
+ userUsableGroupsMutex.RLock()
+ defer userUsableGroupsMutex.RUnlock()
+
copyUserUsableGroups := make(map[string]string)
for k, v := range userUsableGroups {
copyUserUsableGroups[k] = v
@@ -19,6 +24,9 @@ func GetUserUsableGroupsCopy() map[string]string {
}
func UserUsableGroups2JSONString() string {
+ userUsableGroupsMutex.RLock()
+ defer userUsableGroupsMutex.RUnlock()
+
jsonBytes, err := json.Marshal(userUsableGroups)
if err != nil {
common.SysError("error marshalling user groups: " + err.Error())
@@ -27,6 +35,9 @@ func UserUsableGroups2JSONString() string {
}
func UpdateUserUsableGroupsByJSONString(jsonStr string) error {
+ userUsableGroupsMutex.Lock()
+ defer userUsableGroupsMutex.Unlock()
+
userUsableGroups = make(map[string]string)
return json.Unmarshal([]byte(jsonStr), &userUsableGroups)
}
@@ -47,11 +58,17 @@ func GetUserUsableGroups(userGroup string) map[string]string {
}
func GroupInUserUsableGroups(groupName string) bool {
+ userUsableGroupsMutex.RLock()
+ defer userUsableGroupsMutex.RUnlock()
+
_, ok := userUsableGroups[groupName]
return ok
}
func GetUsableGroupDescription(groupName string) string {
+ userUsableGroupsMutex.RLock()
+ defer userUsableGroupsMutex.RUnlock()
+
if desc, ok := userUsableGroups[groupName]; ok {
return desc
}
diff --git a/types/channel_error.go b/types/channel_error.go
new file mode 100644
index 00000000000..f2d72bf536e
--- /dev/null
+++ b/types/channel_error.go
@@ -0,0 +1,21 @@
+package types
+
+type ChannelError struct {
+ ChannelId int `json:"channel_id"`
+ ChannelType int `json:"channel_type"`
+ ChannelName string `json:"channel_name"`
+ IsMultiKey bool `json:"is_multi_key"`
+ AutoBan bool `json:"auto_ban"`
+ UsingKey string `json:"using_key"`
+}
+
+func NewChannelError(channelId int, channelType int, channelName string, isMultiKey bool, usingKey string, autoBan bool) *ChannelError {
+ return &ChannelError{
+ ChannelId: channelId,
+ ChannelType: channelType,
+ ChannelName: channelName,
+ IsMultiKey: isMultiKey,
+ AutoBan: autoBan,
+ UsingKey: usingKey,
+ }
+}
diff --git a/types/error.go b/types/error.go
new file mode 100644
index 00000000000..5c8b37d223a
--- /dev/null
+++ b/types/error.go
@@ -0,0 +1,210 @@
+package types
+
+import (
+ "errors"
+ "fmt"
+ "net/http"
+ "strings"
+)
+
+type OpenAIError struct {
+ Message string `json:"message"`
+ Type string `json:"type"`
+ Param string `json:"param"`
+ Code any `json:"code"`
+}
+
+type ClaudeError struct {
+ Message string `json:"message,omitempty"`
+ Type string `json:"type,omitempty"`
+}
+
+type ErrorType string
+
+const (
+ ErrorTypeNewAPIError ErrorType = "new_api_error"
+ ErrorTypeOpenAIError ErrorType = "openai_error"
+ ErrorTypeClaudeError ErrorType = "claude_error"
+ ErrorTypeMidjourneyError ErrorType = "midjourney_error"
+ ErrorTypeGeminiError ErrorType = "gemini_error"
+ ErrorTypeRerankError ErrorType = "rerank_error"
+)
+
+type ErrorCode string
+
+const (
+ ErrorCodeInvalidRequest ErrorCode = "invalid_request"
+ ErrorCodeSensitiveWordsDetected ErrorCode = "sensitive_words_detected"
+
+ // new api error
+ ErrorCodeCountTokenFailed ErrorCode = "count_token_failed"
+ ErrorCodeModelPriceError ErrorCode = "model_price_error"
+ ErrorCodeInvalidApiType ErrorCode = "invalid_api_type"
+ ErrorCodeJsonMarshalFailed ErrorCode = "json_marshal_failed"
+ ErrorCodeDoRequestFailed ErrorCode = "do_request_failed"
+ ErrorCodeGetChannelFailed ErrorCode = "get_channel_failed"
+
+ // channel error
+ ErrorCodeChannelNoAvailableKey ErrorCode = "channel:no_available_key"
+ ErrorCodeChannelParamOverrideInvalid ErrorCode = "channel:param_override_invalid"
+ ErrorCodeChannelModelMappedError ErrorCode = "channel:model_mapped_error"
+ ErrorCodeChannelAwsClientError ErrorCode = "channel:aws_client_error"
+ ErrorCodeChannelInvalidKey ErrorCode = "channel:invalid_key"
+ ErrorCodeChannelResponseTimeExceeded ErrorCode = "channel:response_time_exceeded"
+
+ // client request error
+ ErrorCodeReadRequestBodyFailed ErrorCode = "read_request_body_failed"
+ ErrorCodeConvertRequestFailed ErrorCode = "convert_request_failed"
+ ErrorCodeAccessDenied ErrorCode = "access_denied"
+
+ // response error
+ ErrorCodeReadResponseBodyFailed ErrorCode = "read_response_body_failed"
+ ErrorCodeBadResponseStatusCode ErrorCode = "bad_response_status_code"
+ ErrorCodeBadResponse ErrorCode = "bad_response"
+ ErrorCodeBadResponseBody ErrorCode = "bad_response_body"
+
+ // sql error
+ ErrorCodeQueryDataError ErrorCode = "query_data_error"
+ ErrorCodeUpdateDataError ErrorCode = "update_data_error"
+
+ // quota error
+ ErrorCodeInsufficientUserQuota ErrorCode = "insufficient_user_quota"
+ ErrorCodePreConsumeTokenQuotaFailed ErrorCode = "pre_consume_token_quota_failed"
+)
+
+type NewAPIError struct {
+ Err error
+ RelayError any
+ ErrorType ErrorType
+ errorCode ErrorCode
+ StatusCode int
+}
+
+func (e *NewAPIError) GetErrorCode() ErrorCode {
+ if e == nil {
+ return ""
+ }
+ return e.errorCode
+}
+
+func (e *NewAPIError) Error() string {
+ if e == nil {
+ return ""
+ }
+ if e.Err == nil {
+ // fallback message when underlying error is missing
+ return string(e.errorCode)
+ }
+ return e.Err.Error()
+}
+
+func (e *NewAPIError) SetMessage(message string) {
+ e.Err = errors.New(message)
+}
+
+func (e *NewAPIError) ToOpenAIError() OpenAIError {
+ switch e.ErrorType {
+ case ErrorTypeOpenAIError:
+ return e.RelayError.(OpenAIError)
+ case ErrorTypeClaudeError:
+ claudeError := e.RelayError.(ClaudeError)
+ return OpenAIError{
+ Message: e.Error(),
+ Type: claudeError.Type,
+ Param: "",
+ Code: e.errorCode,
+ }
+ default:
+ return OpenAIError{
+ Message: e.Error(),
+ Type: string(e.ErrorType),
+ Param: "",
+ Code: e.errorCode,
+ }
+ }
+}
+
+func (e *NewAPIError) ToClaudeError() ClaudeError {
+ switch e.ErrorType {
+ case ErrorTypeOpenAIError:
+ openAIError := e.RelayError.(OpenAIError)
+ return ClaudeError{
+ Message: e.Error(),
+ Type: fmt.Sprintf("%v", openAIError.Code),
+ }
+ case ErrorTypeClaudeError:
+ return e.RelayError.(ClaudeError)
+ default:
+ return ClaudeError{
+ Message: e.Error(),
+ Type: string(e.ErrorType),
+ }
+ }
+}
+
+func NewError(err error, errorCode ErrorCode) *NewAPIError {
+ return &NewAPIError{
+ Err: err,
+ RelayError: nil,
+ ErrorType: ErrorTypeNewAPIError,
+ StatusCode: http.StatusInternalServerError,
+ errorCode: errorCode,
+ }
+}
+
+func NewOpenAIError(err error, errorCode ErrorCode, statusCode int) *NewAPIError {
+ openaiError := OpenAIError{
+ Message: err.Error(),
+ Type: string(errorCode),
+ }
+ return WithOpenAIError(openaiError, statusCode)
+}
+
+func NewErrorWithStatusCode(err error, errorCode ErrorCode, statusCode int) *NewAPIError {
+ return &NewAPIError{
+ Err: err,
+ RelayError: nil,
+ ErrorType: ErrorTypeNewAPIError,
+ StatusCode: statusCode,
+ errorCode: errorCode,
+ }
+}
+
+func WithOpenAIError(openAIError OpenAIError, statusCode int) *NewAPIError {
+ code, ok := openAIError.Code.(string)
+ if !ok {
+ code = fmt.Sprintf("%v", openAIError.Code)
+ }
+ return &NewAPIError{
+ RelayError: openAIError,
+ ErrorType: ErrorTypeOpenAIError,
+ StatusCode: statusCode,
+ Err: errors.New(openAIError.Message),
+ errorCode: ErrorCode(code),
+ }
+}
+
+func WithClaudeError(claudeError ClaudeError, statusCode int) *NewAPIError {
+ return &NewAPIError{
+ RelayError: claudeError,
+ ErrorType: ErrorTypeClaudeError,
+ StatusCode: statusCode,
+ Err: errors.New(claudeError.Message),
+ errorCode: ErrorCode(claudeError.Type),
+ }
+}
+
+func IsChannelError(err *NewAPIError) bool {
+ if err == nil {
+ return false
+ }
+ return strings.HasPrefix(string(err.errorCode), "channel:")
+}
+
+func IsLocalError(err *NewAPIError) bool {
+ if err == nil {
+ return false
+ }
+
+ return err.ErrorType == ErrorTypeNewAPIError
+}
diff --git a/web/bun.lock b/web/bun.lock
new file mode 100644
index 00000000000..b78c149bff9
--- /dev/null
+++ b/web/bun.lock
@@ -0,0 +1,2010 @@
+{
+ "lockfileVersion": 1,
+ "workspaces": {
+ "": {
+ "name": "react-template",
+ "dependencies": {
+ "@douyinfe/semi-icons": "^2.63.1",
+ "@douyinfe/semi-ui": "^2.69.1",
+ "@lobehub/icons": "^2.0.0",
+ "@visactor/react-vchart": "~1.8.8",
+ "@visactor/vchart": "~1.8.8",
+ "@visactor/vchart-semi-theme": "~1.8.8",
+ "axios": "^0.27.2",
+ "clsx": "^2.1.1",
+ "country-flag-icons": "^1.5.19",
+ "dayjs": "^1.11.11",
+ "history": "^5.3.0",
+ "i18next": "^23.16.8",
+ "i18next-browser-languagedetector": "^7.2.0",
+ "katex": "^0.16.22",
+ "lucide-react": "^0.511.0",
+ "marked": "^4.1.1",
+ "mermaid": "^11.6.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-dropzone": "^14.2.3",
+ "react-fireworks": "^1.0.4",
+ "react-i18next": "^13.0.0",
+ "react-icons": "^5.5.0",
+ "react-markdown": "^10.1.0",
+ "react-router-dom": "^6.3.0",
+ "react-telegram-login": "^1.1.2",
+ "react-toastify": "^9.0.8",
+ "react-turnstile": "^1.0.5",
+ "rehype-highlight": "^7.0.2",
+ "rehype-katex": "^7.0.1",
+ "remark-breaks": "^4.0.0",
+ "remark-gfm": "^4.0.1",
+ "remark-math": "^6.0.0",
+ "sse.js": "^2.6.0",
+ "unist-util-visit": "^5.0.0",
+ "use-debounce": "^10.0.4",
+ },
+ "devDependencies": {
+ "@douyinfe/vite-plugin-semi": "^2.74.0-alpha.6",
+ "@so1ve/prettier-config": "^3.1.0",
+ "@vitejs/plugin-react": "^4.2.1",
+ "autoprefixer": "^10.4.21",
+ "postcss": "^8.5.3",
+ "prettier": "^3.0.0",
+ "tailwindcss": "^3",
+ "typescript": "4.4.2",
+ "vite": "^5.2.0",
+ },
+ },
+ },
+ "packages": {
+ "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
+
+ "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
+
+ "@ant-design/colors": ["@ant-design/colors@7.2.1", "", { "dependencies": { "@ant-design/fast-color": "^2.0.6" } }, "sha512-lCHDcEzieu4GA3n8ELeZ5VQ8pKQAWcGGLRTQ50aQM2iqPpq2evTxER84jfdPvsPAtEcZ7m44NI45edFMo8oOYQ=="],
+
+ "@ant-design/cssinjs": ["@ant-design/cssinjs@1.23.0", "", { "dependencies": { "@babel/runtime": "^7.11.1", "@emotion/hash": "^0.8.0", "@emotion/unitless": "^0.7.5", "classnames": "^2.3.1", "csstype": "^3.1.3", "rc-util": "^5.35.0", "stylis": "^4.3.4" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-7GAg9bD/iC9ikWatU9ym+P9ugJhi/WbsTWzcKN6T4gU0aehsprtke1UAaaSxxkjjmkJb3llet/rbUSLPgwlY4w=="],
+
+ "@ant-design/cssinjs-utils": ["@ant-design/cssinjs-utils@1.1.3", "", { "dependencies": { "@ant-design/cssinjs": "^1.21.0", "@babel/runtime": "^7.23.2", "rc-util": "^5.38.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-nOoQMLW1l+xR1Co8NFVYiP8pZp3VjIIzqV6D6ShYF2ljtdwWJn5WSsH+7kvCktXL/yhEtWURKOfH5Xz/gzlwsg=="],
+
+ "@ant-design/fast-color": ["@ant-design/fast-color@2.0.6", "", { "dependencies": { "@babel/runtime": "^7.24.7" } }, "sha512-y2217gk4NqL35giHl72o6Zzqji9O7vHh9YmhUVkPtAOpoTCH4uWxo/pr4VE8t0+ChEPs0qo4eJRC5Q1eXWo3vA=="],
+
+ "@ant-design/icons": ["@ant-design/icons@5.6.1", "", { "dependencies": { "@ant-design/colors": "^7.0.0", "@ant-design/icons-svg": "^4.4.0", "@babel/runtime": "^7.24.8", "classnames": "^2.2.6", "rc-util": "^5.31.1" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg=="],
+
+ "@ant-design/icons-svg": ["@ant-design/icons-svg@4.4.2", "", {}, "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA=="],
+
+ "@ant-design/react-slick": ["@ant-design/react-slick@1.1.2", "", { "dependencies": { "@babel/runtime": "^7.10.4", "classnames": "^2.2.5", "json2mq": "^0.2.0", "resize-observer-polyfill": "^1.5.1", "throttle-debounce": "^5.0.0" }, "peerDependencies": { "react": ">=16.9.0" } }, "sha512-EzlvzE6xQUBrZuuhSAFTdsr4P2bBBHGZwKFemEfq8gIGyIQCxalYfZW/T2ORbtQx5rU69o+WycP3exY/7T1hGA=="],
+
+ "@antfu/install-pkg": ["@antfu/install-pkg@1.1.0", "", { "dependencies": { "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" } }, "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ=="],
+
+ "@antfu/utils": ["@antfu/utils@8.1.1", "", {}, "sha512-Mex9nXf9vR6AhcXmMrlz/HVgYYZpVGJ6YlPgwl7UnaFpnshXs6EK/oa5Gpf3CzENMjkvEx2tQtntGnb7UtSTOQ=="],
+
+ "@astrojs/compiler": ["@astrojs/compiler@2.10.3", "", {}, "sha512-bL/O7YBxsFt55YHU021oL+xz+B/9HvGNId3F9xURN16aeqDK9juHGktdkCSXz+U4nqFACq6ZFvWomOzhV+zfPw=="],
+
+ "@babel/code-frame": ["@babel/code-frame@7.26.2", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" } }, "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ=="],
+
+ "@babel/compat-data": ["@babel/compat-data@7.26.3", "", {}, "sha512-nHIxvKPniQXpmQLb0vhY3VaFb3S0YrTAwpOWJZh1wn3oJPjJk9Asva204PsBdmAE8vpzfHudT8DB0scYvy9q0g=="],
+
+ "@babel/core": ["@babel/core@7.26.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.0", "@babel/generator": "^7.26.0", "@babel/helper-compilation-targets": "^7.25.9", "@babel/helper-module-transforms": "^7.26.0", "@babel/helpers": "^7.26.0", "@babel/parser": "^7.26.0", "@babel/template": "^7.25.9", "@babel/traverse": "^7.25.9", "@babel/types": "^7.26.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg=="],
+
+ "@babel/generator": ["@babel/generator@7.26.3", "", { "dependencies": { "@babel/parser": "^7.26.3", "@babel/types": "^7.26.3", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ=="],
+
+ "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.25.9", "", { "dependencies": { "@babel/compat-data": "^7.25.9", "@babel/helper-validator-option": "^7.25.9", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ=="],
+
+ "@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.6.4", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", "@babel/helper-plugin-utils": "^7.22.5", "debug": "^4.1.1", "lodash.debounce": "^4.0.8", "resolve": "^1.14.2" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-jljfR1rGnXXNWnmQg2K3+bvhkxB51Rl32QRaOTuwwjviGrHzIbSc8+x9CpraDtbT7mfyjXObULP4w/adunNwAw=="],
+
+ "@babel/helper-module-imports": ["@babel/helper-module-imports@7.25.9", "", { "dependencies": { "@babel/traverse": "^7.25.9", "@babel/types": "^7.25.9" } }, "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw=="],
+
+ "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.26.0", "", { "dependencies": { "@babel/helper-module-imports": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9", "@babel/traverse": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw=="],
+
+ "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.25.9", "", {}, "sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw=="],
+
+ "@babel/helper-string-parser": ["@babel/helper-string-parser@7.25.9", "", {}, "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA=="],
+
+ "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="],
+
+ "@babel/helper-validator-option": ["@babel/helper-validator-option@7.25.9", "", {}, "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw=="],
+
+ "@babel/helpers": ["@babel/helpers@7.26.0", "", { "dependencies": { "@babel/template": "^7.25.9", "@babel/types": "^7.26.0" } }, "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw=="],
+
+ "@babel/parser": ["@babel/parser@7.26.3", "", { "dependencies": { "@babel/types": "^7.26.3" }, "bin": "./bin/babel-parser.js" }, "sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA=="],
+
+ "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg=="],
+
+ "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg=="],
+
+ "@babel/plugin-transform-runtime": ["@babel/plugin-transform-runtime@7.27.1", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "babel-plugin-polyfill-corejs2": "^0.4.10", "babel-plugin-polyfill-corejs3": "^0.11.0", "babel-plugin-polyfill-regenerator": "^0.6.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TqGF3desVsTcp3WrJGj4HfKokfCXCLcHpt4PJF0D8/iT6LPd9RS82Upw3KPeyr6B22Lfd3DO8MVrmp0oRkUDdw=="],
+
+ "@babel/runtime": ["@babel/runtime@7.26.0", "", { "dependencies": { "regenerator-runtime": "^0.14.0" } }, "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw=="],
+
+ "@babel/template": ["@babel/template@7.25.9", "", { "dependencies": { "@babel/code-frame": "^7.25.9", "@babel/parser": "^7.25.9", "@babel/types": "^7.25.9" } }, "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg=="],
+
+ "@babel/traverse": ["@babel/traverse@7.26.4", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/generator": "^7.26.3", "@babel/parser": "^7.26.3", "@babel/template": "^7.25.9", "@babel/types": "^7.26.3", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w=="],
+
+ "@babel/types": ["@babel/types@7.26.3", "", { "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA=="],
+
+ "@braintree/sanitize-url": ["@braintree/sanitize-url@7.1.1", "", {}, "sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw=="],
+
+ "@chevrotain/cst-dts-gen": ["@chevrotain/cst-dts-gen@11.0.3", "", { "dependencies": { "@chevrotain/gast": "11.0.3", "@chevrotain/types": "11.0.3", "lodash-es": "4.17.21" } }, "sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ=="],
+
+ "@chevrotain/gast": ["@chevrotain/gast@11.0.3", "", { "dependencies": { "@chevrotain/types": "11.0.3", "lodash-es": "4.17.21" } }, "sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q=="],
+
+ "@chevrotain/regexp-to-ast": ["@chevrotain/regexp-to-ast@11.0.3", "", {}, "sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA=="],
+
+ "@chevrotain/types": ["@chevrotain/types@11.0.3", "", {}, "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ=="],
+
+ "@chevrotain/utils": ["@chevrotain/utils@11.0.3", "", {}, "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ=="],
+
+ "@dnd-kit/accessibility": ["@dnd-kit/accessibility@3.1.1", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw=="],
+
+ "@dnd-kit/core": ["@dnd-kit/core@6.3.1", "", { "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ=="],
+
+ "@dnd-kit/modifiers": ["@dnd-kit/modifiers@9.0.0", "", { "dependencies": { "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@dnd-kit/core": "^6.3.0", "react": ">=16.8.0" } }, "sha512-ybiLc66qRGuZoC20wdSSG6pDXFikui/dCNGthxv4Ndy8ylErY0N3KVxY2bgo7AWwIbxDmXDg3ylAFmnrjcbVvw=="],
+
+ "@dnd-kit/sortable": ["@dnd-kit/sortable@7.0.2", "", { "dependencies": { "@dnd-kit/utilities": "^3.2.0", "tslib": "^2.0.0" }, "peerDependencies": { "@dnd-kit/core": "^6.0.7", "react": ">=16.8.0" } }, "sha512-wDkBHHf9iCi1veM834Gbk1429bd4lHX4RpAwT0y2cHLf246GAvU2sVw/oxWNpPKQNQRQaeGXhAVgrOl1IT+iyA=="],
+
+ "@dnd-kit/utilities": ["@dnd-kit/utilities@3.2.2", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg=="],
+
+ "@douyinfe/semi-animation": ["@douyinfe/semi-animation@2.72.2", "", { "dependencies": { "bezier-easing": "^2.1.0" } }, "sha512-MM2We1Nzvqa6uOrrWurUR+r5klOtOucpBHSjN13plVfZrd1VW8aIlwAyvqEntjOutOoVgnVwkeJHN1P56UV6dQ=="],
+
+ "@douyinfe/semi-animation-react": ["@douyinfe/semi-animation-react@2.72.2", "", { "dependencies": { "@douyinfe/semi-animation": "2.72.2", "@douyinfe/semi-animation-styled": "2.72.2", "classnames": "^2.2.6" } }, "sha512-Iz2mDHDg8Gbur4pzqAyptkA6SK3LB5coGm5r/hevVNWif8Q7gDH9/UR/E9PAx1zORwlxov7BJxUMhrmgaHx7uw=="],
+
+ "@douyinfe/semi-animation-styled": ["@douyinfe/semi-animation-styled@2.72.2", "", {}, "sha512-RKiHV71nWqpp/FiLDLNyw2CNrkR9W7qNnF/zkRosxRs5t4qRCtukdDaTNruuD2exekmCuejs+ClQi4AAwgkIYw=="],
+
+ "@douyinfe/semi-foundation": ["@douyinfe/semi-foundation@2.72.2", "", { "dependencies": { "@douyinfe/semi-animation": "2.72.2", "@douyinfe/semi-json-viewer-core": "2.72.2", "@mdx-js/mdx": "^3.0.1", "async-validator": "^3.5.0", "classnames": "^2.2.6", "date-fns": "^2.29.3", "date-fns-tz": "^1.3.8", "fast-copy": "^3.0.1 ", "lodash": "^4.17.21", "lottie-web": "^5.12.2", "memoize-one": "^5.2.1", "prismjs": "^1.29.0", "remark-gfm": "^4.0.0", "scroll-into-view-if-needed": "^2.2.24" } }, "sha512-pIJIz5rrayVyx2Dk4ntCifet5ZL9bEeTRSnauQtKRxq15ZqT10IETeUha235NAXZr+qA8YGhY+v9dhCXM9SMNA=="],
+
+ "@douyinfe/semi-icons": ["@douyinfe/semi-icons@2.72.2", "", { "dependencies": { "classnames": "^2.2.6" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "sha512-Jbq/U+/R+UWQyp6Wz19ZnSSwDob0f2m/7ZfXBLUgRGGaYrtDTW+RNY1yp7Y4JEvYIcYThPzrNa8WDga7eK//Ng=="],
+
+ "@douyinfe/semi-illustrations": ["@douyinfe/semi-illustrations@2.72.2", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "sha512-Rp1JBcZaEFyIJ+LYfESIp4Xf0rv4h6Es+XOVHtjzuZLD+cSmlhf8zT8WJpLT/9RA16YUFKXTNUFDMjTc/VQKTQ=="],
+
+ "@douyinfe/semi-json-viewer-core": ["@douyinfe/semi-json-viewer-core@2.72.2", "", { "dependencies": { "jsonc-parser": "^3.3.1" } }, "sha512-h87OKEgvWAzqu9XBmc1Y0v6+QiETFFZx5wJbEqMSXuPx3EpW8Q6oYiKKSyaaLNcVC6ytBB2Jf71GFvM8HpcuSg=="],
+
+ "@douyinfe/semi-theme-default": ["@douyinfe/semi-theme-default@2.72.2", "", {}, "sha512-XaMXl9hPtgNF0h8SIptJQSUzQdk78wB5AlAAnOIY0i+27S/3CT5WGBCj27wZqgz/FZcT8xK1sG29B0oXdRhgew=="],
+
+ "@douyinfe/semi-ui": ["@douyinfe/semi-ui@2.72.2", "", { "dependencies": { "@dnd-kit/core": "^6.0.8", "@dnd-kit/sortable": "^7.0.2", "@dnd-kit/utilities": "^3.2.1", "@douyinfe/semi-animation": "2.72.2", "@douyinfe/semi-animation-react": "2.72.2", "@douyinfe/semi-foundation": "2.72.2", "@douyinfe/semi-icons": "2.72.2", "@douyinfe/semi-illustrations": "2.72.2", "@douyinfe/semi-theme-default": "2.72.2", "async-validator": "^3.5.0", "classnames": "^2.2.6", "copy-text-to-clipboard": "^2.1.1", "date-fns": "^2.29.3", "date-fns-tz": "^1.3.8", "fast-copy": "^3.0.1 ", "jsonc-parser": "^3.3.1", "lodash": "^4.17.21", "prop-types": "^15.7.2", "react-resizable": "^3.0.5", "react-window": "^1.8.2", "scroll-into-view-if-needed": "^2.2.24", "utility-types": "^3.10.0" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-okV/9mwSjsGEw5iBOGt2Z/cNIp/VQmgFjQRgwzagoXPmXU5NVSUyxYLdgQXUZXa6po4ZmVZB5IZpyddGGUeDiA=="],
+
+ "@douyinfe/vite-plugin-semi": ["@douyinfe/vite-plugin-semi@2.74.0-alpha.6", "", { "dependencies": { "sass": "^1.85.1" }, "peerDependencies": { "vite": "5.1.0" } }, "sha512-juyKSG0onVBG29FLdGPBA0yHT9Kh7P8e0FDtwhp0DuMk6drd45bDQZuU171gzx0ahv9rJaojnD6CgcBiggtQ3A=="],
+
+ "@emoji-mart/data": ["@emoji-mart/data@1.2.1", "", {}, "sha512-no2pQMWiBy6gpBEiqGeU77/bFejDqUTRY7KX+0+iur13op3bqUsXdnwoZs6Xb1zbv0gAj5VvS1PWoUUckSr5Dw=="],
+
+ "@emoji-mart/react": ["@emoji-mart/react@1.1.1", "", { "peerDependencies": { "emoji-mart": "^5.2", "react": "^16.8 || ^17 || ^18" } }, "sha512-NMlFNeWgv1//uPsvLxvGQoIerPuVdXwK/EUek8OOkJ6wVOWPUizRBJU0hDqWZCOROVpfBgCemaC3m6jDOXi03g=="],
+
+ "@emotion/babel-plugin": ["@emotion/babel-plugin@11.13.5", "", { "dependencies": { "@babel/helper-module-imports": "^7.16.7", "@babel/runtime": "^7.18.3", "@emotion/hash": "^0.9.2", "@emotion/memoize": "^0.9.0", "@emotion/serialize": "^1.3.3", "babel-plugin-macros": "^3.1.0", "convert-source-map": "^1.5.0", "escape-string-regexp": "^4.0.0", "find-root": "^1.1.0", "source-map": "^0.5.7", "stylis": "4.2.0" } }, "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ=="],
+
+ "@emotion/cache": ["@emotion/cache@11.14.0", "", { "dependencies": { "@emotion/memoize": "^0.9.0", "@emotion/sheet": "^1.4.0", "@emotion/utils": "^1.4.2", "@emotion/weak-memoize": "^0.4.0", "stylis": "4.2.0" } }, "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA=="],
+
+ "@emotion/css": ["@emotion/css@11.13.5", "", { "dependencies": { "@emotion/babel-plugin": "^11.13.5", "@emotion/cache": "^11.13.5", "@emotion/serialize": "^1.3.3", "@emotion/sheet": "^1.4.0", "@emotion/utils": "^1.4.2" } }, "sha512-wQdD0Xhkn3Qy2VNcIzbLP9MR8TafI0MJb7BEAXKp+w4+XqErksWR4OXomuDzPsN4InLdGhVe6EYcn2ZIUCpB8w=="],
+
+ "@emotion/hash": ["@emotion/hash@0.8.0", "", {}, "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow=="],
+
+ "@emotion/memoize": ["@emotion/memoize@0.9.0", "", {}, "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ=="],
+
+ "@emotion/react": ["@emotion/react@11.14.0", "", { "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", "@emotion/cache": "^11.14.0", "@emotion/serialize": "^1.3.3", "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", "@emotion/utils": "^1.4.2", "@emotion/weak-memoize": "^0.4.0", "hoist-non-react-statics": "^3.3.1" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA=="],
+
+ "@emotion/serialize": ["@emotion/serialize@1.3.3", "", { "dependencies": { "@emotion/hash": "^0.9.2", "@emotion/memoize": "^0.9.0", "@emotion/unitless": "^0.10.0", "@emotion/utils": "^1.4.2", "csstype": "^3.0.2" } }, "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA=="],
+
+ "@emotion/sheet": ["@emotion/sheet@1.4.0", "", {}, "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg=="],
+
+ "@emotion/unitless": ["@emotion/unitless@0.7.5", "", {}, "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg=="],
+
+ "@emotion/use-insertion-effect-with-fallbacks": ["@emotion/use-insertion-effect-with-fallbacks@1.2.0", "", { "peerDependencies": { "react": ">=16.8.0" } }, "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg=="],
+
+ "@emotion/utils": ["@emotion/utils@1.4.2", "", {}, "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA=="],
+
+ "@emotion/weak-memoize": ["@emotion/weak-memoize@0.4.0", "", {}, "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg=="],
+
+ "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="],
+
+ "@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="],
+
+ "@esbuild/android-arm64": ["@esbuild/android-arm64@0.21.5", "", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="],
+
+ "@esbuild/android-x64": ["@esbuild/android-x64@0.21.5", "", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="],
+
+ "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.21.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="],
+
+ "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.21.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="],
+
+ "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.21.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="],
+
+ "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.21.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="],
+
+ "@esbuild/linux-arm": ["@esbuild/linux-arm@0.21.5", "", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="],
+
+ "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.21.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="],
+
+ "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.21.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="],
+
+ "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="],
+
+ "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="],
+
+ "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.21.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="],
+
+ "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="],
+
+ "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.21.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="],
+
+ "@esbuild/linux-x64": ["@esbuild/linux-x64@0.21.5", "", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="],
+
+ "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.21.5", "", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="],
+
+ "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.21.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="],
+
+ "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.21.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="],
+
+ "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.21.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="],
+
+ "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.21.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="],
+
+ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="],
+
+ "@floating-ui/core": ["@floating-ui/core@1.7.0", "", { "dependencies": { "@floating-ui/utils": "^0.2.9" } }, "sha512-FRdBLykrPPA6P76GGGqlex/e7fbe0F1ykgxHYNXQsH/iTEtjMj/f9bpY5oQqbjt5VgZvgz/uKXbGuROijh3VLA=="],
+
+ "@floating-ui/dom": ["@floating-ui/dom@1.7.0", "", { "dependencies": { "@floating-ui/core": "^1.7.0", "@floating-ui/utils": "^0.2.9" } }, "sha512-lGTor4VlXcesUMh1cupTUTDoCxMb0V6bm3CnxHzQcw8Eaf1jQbgQX4i02fYgT0vJ82tb5MZ4CZk1LRGkktJCzg=="],
+
+ "@floating-ui/react": ["@floating-ui/react@0.27.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.1.2", "@floating-ui/utils": "^0.2.9", "tabbable": "^6.0.0" }, "peerDependencies": { "react": ">=17.0.0", "react-dom": ">=17.0.0" } }, "sha512-EQJ4Th328y2wyHR3KzOUOoTW2UKjFk53fmyahfwExnFQ8vnsMYqKc+fFPOkeYtj5tcp1DUMiNJ7BFhed7e9ONw=="],
+
+ "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.2", "", { "dependencies": { "@floating-ui/dom": "^1.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A=="],
+
+ "@floating-ui/utils": ["@floating-ui/utils@0.2.9", "", {}, "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg=="],
+
+ "@giscus/react": ["@giscus/react@3.1.0", "", { "dependencies": { "giscus": "^1.6.0" }, "peerDependencies": { "react": "^16 || ^17 || ^18 || ^19", "react-dom": "^16 || ^17 || ^18 || ^19" } }, "sha512-0TCO2TvL43+oOdyVVGHDItwxD1UMKP2ZYpT6gXmhFOqfAJtZxTzJ9hkn34iAF/b6YzyJ4Um89QIt9z/ajmAEeg=="],
+
+ "@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="],
+
+ "@iconify/utils": ["@iconify/utils@2.3.0", "", { "dependencies": { "@antfu/install-pkg": "^1.0.0", "@antfu/utils": "^8.1.0", "@iconify/types": "^2.0.0", "debug": "^4.4.0", "globals": "^15.14.0", "kolorist": "^1.8.0", "local-pkg": "^1.0.0", "mlly": "^1.7.4" } }, "sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA=="],
+
+ "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
+
+ "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="],
+
+ "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
+
+ "@jridgewell/set-array": ["@jridgewell/set-array@1.2.1", "", {}, "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A=="],
+
+ "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
+
+ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
+
+ "@lit-labs/ssr-dom-shim": ["@lit-labs/ssr-dom-shim@1.3.0", "", {}, "sha512-nQIWonJ6eFAvUUrSlwyHDm/aE8PBDu5kRpL0vHMg6K8fK3Diq1xdPjTnsJSwxABhaZ+5eBi1btQB5ShUTKo4nQ=="],
+
+ "@lit/reactive-element": ["@lit/reactive-element@2.1.0", "", { "dependencies": { "@lit-labs/ssr-dom-shim": "^1.2.0" } }, "sha512-L2qyoZSQClcBmq0qajBVbhYEcG6iK0XfLn66ifLe/RfC0/ihpc+pl0Wdn8bJ8o+hj38cG0fGXRgSS20MuXn7qA=="],
+
+ "@lobehub/emojilib": ["@lobehub/emojilib@1.0.0", "", {}, "sha512-s9KnjaPjsEefaNv150G3aifvB+J3P4eEKG+epY9zDPS2BeB6+V2jELWqAZll+nkogMaVovjEE813z3V751QwGw=="],
+
+ "@lobehub/fluent-emoji": ["@lobehub/fluent-emoji@2.0.0", "", { "dependencies": { "@lobehub/emojilib": "^1.0.0", "@lobehub/ui": "^2.0.0", "antd-style": "^3.7.1", "emoji-regex": "^10.4.0", "lodash-es": "^4.17.21", "lucide-react": "^0.469.0", "react-layout-kit": "^1.9.1", "url-join": "^5.0.0" }, "peerDependencies": { "antd": "^5.23.0", "react": "^19.0.0", "react-dom": "^19.0.0" } }, "sha512-bKjU3sf0+7NppvcdqD/raWvKGJIw8HDJVporNQ7oR8pIPoLeb9IUu/vqIYClOlwfu9qntji7FFySfbdNqXSiJw=="],
+
+ "@lobehub/icons": ["@lobehub/icons@2.1.0", "", { "dependencies": { "@lobehub/ui": "^2.0.0", "antd-style": "^3.7.1", "lucide-react": "^0.469.0", "polished": "^4.3.1", "react-layout-kit": "^1.9.1" }, "peerDependencies": { "antd": "^5.23.0", "react": "^19.0.0", "react-dom": "^19.0.0" } }, "sha512-iHtIp8a05/YHxTDlOFXCTfvYXUjKi1Mbq5a9qsEN+zwJ5U+mR2WgKz5zUausIzZiMZo+P3pgxbhh3/eHf7Q1pw=="],
+
+ "@lobehub/ui": ["@lobehub/ui@2.1.10", "", { "dependencies": { "@ant-design/cssinjs": "^1.23.0", "@dnd-kit/core": "^6.3.1", "@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", "@emoji-mart/data": "^1.2.1", "@emoji-mart/react": "^1.1.1", "@floating-ui/react": "^0.27.5", "@giscus/react": "^3.1.0", "@lobehub/fluent-emoji": "^2.0.0", "@lobehub/icons": "^2.0.0", "@mdx-js/mdx": "^3.1.0", "@mdx-js/react": "^3.1.0", "@radix-ui/react-slot": "^1.1.2", "@shikijs/transformers": "^3.2.1", "@splinetool/runtime": "0.9.526", "ahooks": "^3.8.4", "antd-style": "^3.7.1", "chroma-js": "^3.1.2", "class-variance-authority": "^0.7.1", "dayjs": "^1.11.13", "emoji-mart": "^5.6.0", "fast-deep-equal": "^3.1.3", "framer-motion": "^12.6.3", "immer": "^10.1.1", "katex": "^0.16.9", "leva": "^0.10.0", "lodash-es": "^4.17.21", "lucide-react": "^0.484.0", "mermaid": "^11.6.0", "numeral": "^2.0.6", "polished": "^4.3.1", "query-string": "^9.1.1", "rc-collapse": "^4.0.0", "rc-footer": "^0.6.8", "rc-image": "^7.11.1", "rc-menu": "^9.16.1", "re-resizable": "^6.11.2", "react-avatar-editor": "^13.0.2", "react-error-boundary": "^5.0.0", "react-hotkeys-hook": "^5.1.0", "react-layout-kit": "^1.9.1", "react-markdown": "^10.1.0", "react-merge-refs": "^3.0.2", "react-rnd": "^10.5.2", "react-zoom-pan-pinch": "^3.7.0", "rehype-katex": "^7.0.1", "rehype-raw": "^7.0.0", "remark-breaks": "^4.0.0", "remark-gfm": "^4.0.1", "remark-math": "^6.0.0", "shiki": "^3.2.1", "swr": "^2.3.3", "ts-md5": "^1.3.1", "unified": "^11.0.5", "url-join": "^5.0.0", "use-merge-value": "^1.2.0", "uuid": "^11.1.0" }, "peerDependencies": { "antd": "^5.25.0", "react": "^19.0.0", "react-dom": "^19.0.0" } }, "sha512-R1/t5I8UAjvd5xoEDJXg6RzHmwhdOU45JQN297MlYB/sGqcvySfQL9POpDmySSs+QMyjkhwhum254cfXFKJIZA=="],
+
+ "@mdx-js/mdx": ["@mdx-js/mdx@3.1.0", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-/QxEhPAvGwbQmy1Px8F899L5Uc2KZ6JtXwlCgJmjSTBedwOZkByYcBG4GceIGPXRDsmfxhHazuS+hlOShRLeDw=="],
+
+ "@mdx-js/react": ["@mdx-js/react@3.1.0", "", { "dependencies": { "@types/mdx": "^2.0.0" }, "peerDependencies": { "@types/react": ">=16", "react": ">=16" } }, "sha512-QjHtSaoameoalGnKDT3FoIl4+9RwyTmo9ZJGBdLOks/YOiWHoRDI3PUwEzOE7kEmGcV3AFcp9K6dYu9rEuKLAQ=="],
+
+ "@mermaid-js/parser": ["@mermaid-js/parser@0.4.0", "", { "dependencies": { "langium": "3.3.1" } }, "sha512-wla8XOWvQAwuqy+gxiZqY+c7FokraOTHRWMsbB4AgRx9Sy7zKslNyejy7E+a77qHfey5GXw/ik3IXv/NHMJgaA=="],
+
+ "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
+
+ "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
+
+ "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
+
+ "@parcel/watcher": ["@parcel/watcher@2.5.1", "", { "dependencies": { "detect-libc": "^1.0.3", "is-glob": "^4.0.3", "micromatch": "^4.0.5", "node-addon-api": "^7.0.0" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.5.1", "@parcel/watcher-darwin-arm64": "2.5.1", "@parcel/watcher-darwin-x64": "2.5.1", "@parcel/watcher-freebsd-x64": "2.5.1", "@parcel/watcher-linux-arm-glibc": "2.5.1", "@parcel/watcher-linux-arm-musl": "2.5.1", "@parcel/watcher-linux-arm64-glibc": "2.5.1", "@parcel/watcher-linux-arm64-musl": "2.5.1", "@parcel/watcher-linux-x64-glibc": "2.5.1", "@parcel/watcher-linux-x64-musl": "2.5.1", "@parcel/watcher-win32-arm64": "2.5.1", "@parcel/watcher-win32-ia32": "2.5.1", "@parcel/watcher-win32-x64": "2.5.1" } }, "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg=="],
+
+ "@parcel/watcher-android-arm64": ["@parcel/watcher-android-arm64@2.5.1", "", { "os": "android", "cpu": "arm64" }, "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA=="],
+
+ "@parcel/watcher-darwin-arm64": ["@parcel/watcher-darwin-arm64@2.5.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw=="],
+
+ "@parcel/watcher-darwin-x64": ["@parcel/watcher-darwin-x64@2.5.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg=="],
+
+ "@parcel/watcher-freebsd-x64": ["@parcel/watcher-freebsd-x64@2.5.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ=="],
+
+ "@parcel/watcher-linux-arm-glibc": ["@parcel/watcher-linux-arm-glibc@2.5.1", "", { "os": "linux", "cpu": "arm" }, "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA=="],
+
+ "@parcel/watcher-linux-arm-musl": ["@parcel/watcher-linux-arm-musl@2.5.1", "", { "os": "linux", "cpu": "arm" }, "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q=="],
+
+ "@parcel/watcher-linux-arm64-glibc": ["@parcel/watcher-linux-arm64-glibc@2.5.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w=="],
+
+ "@parcel/watcher-linux-arm64-musl": ["@parcel/watcher-linux-arm64-musl@2.5.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg=="],
+
+ "@parcel/watcher-linux-x64-glibc": ["@parcel/watcher-linux-x64-glibc@2.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A=="],
+
+ "@parcel/watcher-linux-x64-musl": ["@parcel/watcher-linux-x64-musl@2.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg=="],
+
+ "@parcel/watcher-win32-arm64": ["@parcel/watcher-win32-arm64@2.5.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw=="],
+
+ "@parcel/watcher-win32-ia32": ["@parcel/watcher-win32-ia32@2.5.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ=="],
+
+ "@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.1", "", { "os": "win32", "cpu": "x64" }, "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA=="],
+
+ "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
+
+ "@radix-ui/primitive": ["@radix-ui/primitive@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" } }, "sha512-3e7rn8FDMin4CgeL7Z/49smCA3rFYY3Ha2rUQ7HRWFadS5iCRw08ZgVT1LaNTCNqgvrUiyczLflrVrF0SRQtNA=="],
+
+ "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.0.2", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-primitive": "1.0.2" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-fqYwhhI9IarZ0ll2cUSfKuXHlJK0qE4AfnRrPBbRwEH/4mGQn04/QFGomLi8TXWIdv9WJk//KgGm+aDxVIr1wA=="],
+
+ "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
+
+ "@radix-ui/react-context": ["@radix-ui/react-context@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-1pVM9RfOQ+n/N5PJK33kRSKsr1glNxomxONs5c49MliinBY6Yw2Q995qfBUUo0/Mbg05B/sGA0gkgPI7kmSHBg=="],
+
+ "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.0.3", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/primitive": "1.0.0", "@radix-ui/react-compose-refs": "1.0.0", "@radix-ui/react-primitive": "1.0.2", "@radix-ui/react-use-callback-ref": "1.0.0", "@radix-ui/react-use-escape-keydown": "1.0.2" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-nXZOvFjOuHS1ovumntGV7NNoLaEp9JEvTht3MBjP44NSW5hUKj/8OnfN3+8WmB+CEhN44XaGhpHoSsUIEl5P7Q=="],
+
+ "@radix-ui/react-id": ["@radix-ui/react-id@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-use-layout-effect": "1.0.0" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-Q6iAB/U7Tq3NTolBBQbHTgclPmGWE3OlktGGqrClPozSw4vkQ1DfQAOtzgRPecKsMdJINE05iaoDUG8tRzCBjw=="],
+
+ "@radix-ui/react-popper": ["@radix-ui/react-popper@1.1.1", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@floating-ui/react-dom": "0.7.2", "@radix-ui/react-arrow": "1.0.2", "@radix-ui/react-compose-refs": "1.0.0", "@radix-ui/react-context": "1.0.0", "@radix-ui/react-primitive": "1.0.2", "@radix-ui/react-use-callback-ref": "1.0.0", "@radix-ui/react-use-layout-effect": "1.0.0", "@radix-ui/react-use-rect": "1.0.0", "@radix-ui/react-use-size": "1.0.0", "@radix-ui/rect": "1.0.0" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-keYDcdMPNMjSC8zTsZ8wezUMiWM9Yj14wtF3s0PTIs9srnEPC9Kt2Gny1T3T81mmSeyDjZxsD9N5WCwNNb712w=="],
+
+ "@radix-ui/react-portal": ["@radix-ui/react-portal@1.0.2", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-primitive": "1.0.2" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-swu32idoCW7KA2VEiUZGBSu9nB6qwGdV6k6HYhUoOo3M1FFpD+VgLzUqtt3mwL1ssz7r2x8MggpLSQach2Xy/Q=="],
+
+ "@radix-ui/react-presence": ["@radix-ui/react-presence@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-compose-refs": "1.0.0", "@radix-ui/react-use-layout-effect": "1.0.0" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-A+6XEvN01NfVWiKu38ybawfHsBjWum42MRPnEuqPsBZ4eV7e/7K321B5VgYMPv3Xx5An6o1/l9ZuDBgmcmWK3w=="],
+
+ "@radix-ui/react-primitive": ["@radix-ui/react-primitive@1.0.2", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-slot": "1.0.1" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-zY6G5Qq4R8diFPNwtyoLRZBxzu1Z+SXMlfYpChN7Dv8gvmx9X3qhDqiLWvKseKVJMuedFeU/Sa0Sy/Ia+t06Dw=="],
+
+ "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
+ "@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.0.5", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/primitive": "1.0.0", "@radix-ui/react-compose-refs": "1.0.0", "@radix-ui/react-context": "1.0.0", "@radix-ui/react-dismissable-layer": "1.0.3", "@radix-ui/react-id": "1.0.0", "@radix-ui/react-popper": "1.1.1", "@radix-ui/react-portal": "1.0.2", "@radix-ui/react-presence": "1.0.0", "@radix-ui/react-primitive": "1.0.2", "@radix-ui/react-slot": "1.0.1", "@radix-ui/react-use-controllable-state": "1.0.0", "@radix-ui/react-visually-hidden": "1.0.2" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-cDKVcfzyO6PpckZekODJZDe5ZxZ2fCZlzKzTmPhe4mX9qTHRfLcKgqb0OKf22xLwDequ2tVleim+ZYx3rabD5w=="],
+
+ "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg=="],
+
+ "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-use-callback-ref": "1.0.0" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-FohDoZvk3mEXh9AWAVyRTYR4Sq7/gavuofglmiXB2g1aKyboUD4YtgWxKj8O5n+Uak52gXQ4wKz5IFST4vtJHg=="],
+
+ "@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.0.2", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-use-callback-ref": "1.0.0" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-DXGim3x74WgUv+iMNCF+cAo8xUHHeqvjx8zs7trKf+FkQKPQXLk2sX7Gx1ysH7Q76xCpZuxIJE7HLPxRE+Q+GA=="],
+
+ "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-6Tpkq+R6LOlmQb1R5NNETLG0B4YP0wc+klfXafpUCj6JGyaUc8il7/kUZ7m59rGbXGczE9Bs+iz2qloqsZBduQ=="],
+
+ "@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/rect": "1.0.0" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-TB7pID8NRMEHxb/qQJpvSt3hQU4sqNPM1VCTjTRjEOa7cEop/QMuq8S6fb/5Tsz64kqSvB9WnwsDHtjnrM9qew=="],
+
+ "@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-use-layout-effect": "1.0.0" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-imZ3aYcoYCKhhgNpkNDh/aTiU05qw9hX+HHI1QDBTyIlcFjgeFlKKySNGMwTp7nYFLQg/j0VA2FmCY4WPDDHMg=="],
+
+ "@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.0.2", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-primitive": "1.0.2" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-qirnJxtYn73HEk1rXL12/mXnu2rwsNHDID10th2JGtdK25T9wX+mxRmGt7iPSahw512GbZOc0syZX1nLQGoEOg=="],
+
+ "@radix-ui/rect": ["@radix-ui/rect@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" } }, "sha512-d0O68AYy/9oeEy1DdC07bz1/ZXX+DqCskRd3i4JzLSTXwefzaepQrKjXC7aNM8lTHjFLDO0pDgaEiQ7jEk+HVg=="],
+
+ "@rc-component/async-validator": ["@rc-component/async-validator@5.0.4", "", { "dependencies": { "@babel/runtime": "^7.24.4" } }, "sha512-qgGdcVIF604M9EqjNF0hbUTz42bz/RDtxWdWuU5EQe3hi7M8ob54B6B35rOsvX5eSvIHIzT9iH1R3n+hk3CGfg=="],
+
+ "@rc-component/color-picker": ["@rc-component/color-picker@2.0.1", "", { "dependencies": { "@ant-design/fast-color": "^2.0.6", "@babel/runtime": "^7.23.6", "classnames": "^2.2.6", "rc-util": "^5.38.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-WcZYwAThV/b2GISQ8F+7650r5ZZJ043E57aVBFkQ+kSY4C6wdofXgB0hBx+GPGpIU0Z81eETNoDUJMr7oy/P8Q=="],
+
+ "@rc-component/context": ["@rc-component/context@1.4.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "rc-util": "^5.27.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-kFcNxg9oLRMoL3qki0OMxK+7g5mypjgaaJp/pkOis/6rVxma9nJBF/8kCIuTYHUQNr0ii7MxqE33wirPZLJQ2w=="],
+
+ "@rc-component/mini-decimal": ["@rc-component/mini-decimal@1.1.0", "", { "dependencies": { "@babel/runtime": "^7.18.0" } }, "sha512-jS4E7T9Li2GuYwI6PyiVXmxTiM6b07rlD9Ge8uGZSCz3WlzcG5ZK7g5bbuKNeZ9pgUuPK/5guV781ujdVpm4HQ=="],
+
+ "@rc-component/mutate-observer": ["@rc-component/mutate-observer@1.1.0", "", { "dependencies": { "@babel/runtime": "^7.18.0", "classnames": "^2.3.2", "rc-util": "^5.24.4" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-QjrOsDXQusNwGZPf4/qRQasg7UFEj06XiCJ8iuiq/Io7CrHrgVi6Uuetw60WAMG1799v+aM8kyc+1L/GBbHSlw=="],
+
+ "@rc-component/portal": ["@rc-component/portal@1.1.2", "", { "dependencies": { "@babel/runtime": "^7.18.0", "classnames": "^2.3.2", "rc-util": "^5.24.4" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-6f813C0IsasTZms08kfA8kPAGxbbkYToa8ALaiDIGGECU4i9hj8Plgbx0sNJDrey3EtHO30hmdaxtT0138xZcg=="],
+
+ "@rc-component/qrcode": ["@rc-component/qrcode@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.24.7", "classnames": "^2.3.2", "rc-util": "^5.38.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-L+rZ4HXP2sJ1gHMGHjsg9jlYBX/SLN2D6OxP9Zn3qgtpMWtO2vUfxVFwiogHpAIqs54FnALxraUy/BCO1yRIgg=="],
+
+ "@rc-component/tour": ["@rc-component/tour@1.15.1", "", { "dependencies": { "@babel/runtime": "^7.18.0", "@rc-component/portal": "^1.0.0-9", "@rc-component/trigger": "^2.0.0", "classnames": "^2.3.2", "rc-util": "^5.24.4" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-Tr2t7J1DKZUpfJuDZWHxyxWpfmj8EZrqSgyMZ+BCdvKZ6r1UDsfU46M/iWAAFBy961Ssfom2kv5f3UcjIL2CmQ=="],
+
+ "@rc-component/trigger": ["@rc-component/trigger@2.2.6", "", { "dependencies": { "@babel/runtime": "^7.23.2", "@rc-component/portal": "^1.1.0", "classnames": "^2.3.2", "rc-motion": "^2.0.0", "rc-resize-observer": "^1.3.1", "rc-util": "^5.44.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-/9zuTnWwhQ3S3WT1T8BubuFTT46kvnXgaERR9f4BTKyn61/wpf/BvbImzYBubzJibU707FxwbKszLlHjcLiv1Q=="],
+
+ "@remix-run/router": ["@remix-run/router@1.21.0", "", {}, "sha512-xfSkCAchbdG5PnbrKqFWwia4Bi61nH+wm8wLEqfHDyp7Y3dZzgqS2itV8i4gAq9pC2HsTpwyBC6Ds8VHZ96JlA=="],
+
+ "@resvg/resvg-js": ["@resvg/resvg-js@2.4.1", "", { "optionalDependencies": { "@resvg/resvg-js-android-arm-eabi": "2.4.1", "@resvg/resvg-js-android-arm64": "2.4.1", "@resvg/resvg-js-darwin-arm64": "2.4.1", "@resvg/resvg-js-darwin-x64": "2.4.1", "@resvg/resvg-js-linux-arm-gnueabihf": "2.4.1", "@resvg/resvg-js-linux-arm64-gnu": "2.4.1", "@resvg/resvg-js-linux-arm64-musl": "2.4.1", "@resvg/resvg-js-linux-x64-gnu": "2.4.1", "@resvg/resvg-js-linux-x64-musl": "2.4.1", "@resvg/resvg-js-win32-arm64-msvc": "2.4.1", "@resvg/resvg-js-win32-ia32-msvc": "2.4.1", "@resvg/resvg-js-win32-x64-msvc": "2.4.1" } }, "sha512-wTOf1zerZX8qYcMmLZw3czR4paI4hXqPjShNwJRh5DeHxvgffUS5KM7XwxtbIheUW6LVYT5fhT2AJiP6mU7U4A=="],
+
+ "@resvg/resvg-js-android-arm-eabi": ["@resvg/resvg-js-android-arm-eabi@2.4.1", "", { "os": "android", "cpu": "arm" }, "sha512-AA6f7hS0FAPpvQMhBCf6f1oD1LdlqNXKCxAAPpKh6tR11kqV0YIB9zOlIYgITM14mq2YooLFl6XIbbvmY+jwUw=="],
+
+ "@resvg/resvg-js-android-arm64": ["@resvg/resvg-js-android-arm64@2.4.1", "", { "os": "android", "cpu": "arm64" }, "sha512-/QleoRdPfsEuH9jUjilYcDtKK/BkmWcK+1LXM8L2nsnf/CI8EnFyv7ZzCj4xAIvZGAy9dTYr/5NZBcTwxG2HQg=="],
+
+ "@resvg/resvg-js-darwin-arm64": ["@resvg/resvg-js-darwin-arm64@2.4.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-U1oMNhea+kAXgiEXgzo7EbFGCD1Edq5aSlQoe6LMly6UjHzgx2W3N5kEXCwU/CgN5FiQhZr7PlSJSlcr7mdhfg=="],
+
+ "@resvg/resvg-js-darwin-x64": ["@resvg/resvg-js-darwin-x64@2.4.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-avyVh6DpebBfHHtTQTZYSr6NG1Ur6TEilk1+H0n7V+g4F7x7WPOo8zL00ZhQCeRQ5H4f8WXNWIEKL8fwqcOkYw=="],
+
+ "@resvg/resvg-js-linux-arm-gnueabihf": ["@resvg/resvg-js-linux-arm-gnueabihf@2.4.1", "", { "os": "linux", "cpu": "arm" }, "sha512-isY/mdKoBWH4VB5v621co+8l101jxxYjuTkwOLsbW+5RK9EbLciPlCB02M99ThAHzI2MYxIUjXNmNgOW8btXvw=="],
+
+ "@resvg/resvg-js-linux-arm64-gnu": ["@resvg/resvg-js-linux-arm64-gnu@2.4.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-uY5voSCrFI8TH95vIYBm5blpkOtltLxLRODyhKJhGfskOI7XkRw5/t1u0sWAGYD8rRSNX+CA+np86otKjubrNg=="],
+
+ "@resvg/resvg-js-linux-arm64-musl": ["@resvg/resvg-js-linux-arm64-musl@2.4.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-6mT0+JBCsermKMdi/O2mMk3m7SqOjwi9TKAwSngRZ/nQoL3Z0Z5zV+572ztgbWr0GODB422uD8e9R9zzz38dRQ=="],
+
+ "@resvg/resvg-js-linux-x64-gnu": ["@resvg/resvg-js-linux-x64-gnu@2.4.1", "", { "os": "linux", "cpu": "x64" }, "sha512-60KnrscLj6VGhkYOJEmmzPlqqfcw1keDh6U+vMcNDjPhV3B5vRSkpP/D/a8sfokyeh4VEacPSYkWGezvzS2/mg=="],
+
+ "@resvg/resvg-js-linux-x64-musl": ["@resvg/resvg-js-linux-x64-musl@2.4.1", "", { "os": "linux", "cpu": "x64" }, "sha512-0AMyZSICC1D7ge115cOZQW8Pcad6PjWuZkBFF3FJuSxC6Dgok0MQnLTs2MfMdKBlAcwO9dXsf3bv9tJZj8pATA=="],
+
+ "@resvg/resvg-js-win32-arm64-msvc": ["@resvg/resvg-js-win32-arm64-msvc@2.4.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-76XDFOFSa3d0QotmcNyChh2xHwk+JTFiEQBVxMlHpHMeq7hNrQJ1IpE1zcHSQvrckvkdfLboKRrlGB86B10Qjw=="],
+
+ "@resvg/resvg-js-win32-ia32-msvc": ["@resvg/resvg-js-win32-ia32-msvc@2.4.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-odyVFGrEWZIzzJ89KdaFtiYWaIJh9hJRW/frcEcG3agJ464VXkN/2oEVF5ulD+5mpGlug9qJg7htzHcKxDN8sg=="],
+
+ "@resvg/resvg-js-win32-x64-msvc": ["@resvg/resvg-js-win32-x64-msvc@2.4.1", "", { "os": "win32", "cpu": "x64" }, "sha512-vY4kTLH2S3bP+puU5x7hlAxHv+ulFgcK6Zn3efKSr0M0KnZ9A3qeAjZteIpkowEFfUeMPNg2dvvoFRJA9zqxSw=="],
+
+ "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.30.0", "", { "os": "android", "cpu": "arm" }, "sha512-qFcFto9figFLz2g25DxJ1WWL9+c91fTxnGuwhToCl8BaqDsDYMl/kOnBXAyAqkkzAWimYMSWNPWEjt+ADAHuoQ=="],
+
+ "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.30.0", "", { "os": "android", "cpu": "arm64" }, "sha512-vqrQdusvVl7dthqNjWCL043qelBK+gv9v3ZiqdxgaJvmZyIAAXMjeGVSqZynKq69T7062T5VrVTuikKSAAVP6A=="],
+
+ "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.30.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-617pd92LhdA9+wpixnzsyhVft3szYiN16aNUMzVkf2N+yAk8UXY226Bfp36LvxYTUt7MO/ycqGFjQgJ0wlMaWQ=="],
+
+ "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.30.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Y3b4oDoaEhCypg8ajPqigKDcpi5ZZovemQl9Edpem0uNv6UUjXv7iySBpGIUTSs2ovWOzYpfw9EbFJXF/fJHWw=="],
+
+ "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.30.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-3REQJ4f90sFIBfa0BUokiCdrV/E4uIjhkWe1bMgCkhFXbf4D8YN6C4zwJL881GM818qVYE9BO3dGwjKhpo2ABA=="],
+
+ "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.30.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-ZtY3Y8icbe3Cc+uQicsXG5L+CRGUfLZjW6j2gn5ikpltt3Whqjfo5mkyZ86UiuHF9Q3ZsaQeW7YswlHnN+lAcg=="],
+
+ "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.30.0", "", { "os": "linux", "cpu": "arm" }, "sha512-bsPGGzfiHXMhQGuFGpmo2PyTwcrh2otL6ycSZAFTESviUoBOuxF7iBbAL5IJXc/69peXl5rAtbewBFeASZ9O0g=="],
+
+ "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.30.0", "", { "os": "linux", "cpu": "arm" }, "sha512-kvyIECEhs2DrrdfQf++maCWJIQ974EI4txlz1nNSBaCdtf7i5Xf1AQCEJWOC5rEBisdaMFFnOWNLYt7KpFqy5A=="],
+
+ "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.30.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-CFE7zDNrokaotXu+shwIrmWrFxllg79vciH4E/zeK7NitVuWEaXRzS0mFfFvyhZfn8WfVOG/1E9u8/DFEgK7WQ=="],
+
+ "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.30.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-MctNTBlvMcIBP0t8lV/NXiUwFg9oK5F79CxLU+a3xgrdJjfBLVIEHSAjQ9+ipofN2GKaMLnFFXLltg1HEEPaGQ=="],
+
+ "@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.30.0", "", { "os": "linux", "cpu": "none" }, "sha512-fBpoYwLEPivL3q368+gwn4qnYnr7GVwM6NnMo8rJ4wb0p/Y5lg88vQRRP077gf+tc25akuqd+1Sxbn9meODhwA=="],
+
+ "@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.30.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1hiHPV6dUaqIMXrIjN+vgJqtfkLpqHS1Xsg0oUfUVD98xGp1wX89PIXgDF2DWra1nxAd8dfE0Dk59MyeKaBVAw=="],
+
+ "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.30.0", "", { "os": "linux", "cpu": "none" }, "sha512-U0xcC80SMpEbvvLw92emHrNjlS3OXjAM0aVzlWfar6PR0ODWCTQtKeeB+tlAPGfZQXicv1SpWwRz9Hyzq3Jx3g=="],
+
+ "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.30.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-VU/P/IODrNPasgZDLIFJmMiLGez+BN11DQWfTVlViJVabyF3JaeaJkP6teI8760f18BMGCQOW9gOmuzFaI1pUw=="],
+
+ "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.30.0", "", { "os": "linux", "cpu": "x64" }, "sha512-laQVRvdbKmjXuFA3ZiZj7+U24FcmoPlXEi2OyLfbpY2MW1oxLt9Au8q9eHd0x6Pw/Kw4oe9gwVXWwIf2PVqblg=="],
+
+ "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.30.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3wzKzduS7jzxqcOvy/ocU/gMR3/QrHEFLge5CD7Si9fyHuoXcidyYZ6jyx8OPYmCcGm3uKTUl+9jUSAY74Ln5A=="],
+
+ "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.30.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-jROwnI1+wPyuv696rAFHp5+6RFhXGGwgmgSfzE8e4xfit6oLRg7GyMArVUoM3ChS045OwWr9aTnU+2c1UdBMyw=="],
+
+ "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.30.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-duzweyup5WELhcXx5H1jokpr13i3BV9b48FMiikYAwk/MT1LrMYYk2TzenBd0jj4ivQIt58JWSxc19y4SvLP4g=="],
+
+ "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.30.0", "", { "os": "win32", "cpu": "x64" }, "sha512-DYvxS0M07PvgvavMIybCOBYheyrqlui6ZQBHJs6GqduVzHSZ06TPPvlfvnYstjODHQ8UUXFwt5YE+h0jFI8kwg=="],
+
+ "@shikijs/core": ["@shikijs/core@3.4.2", "", { "dependencies": { "@shikijs/types": "3.4.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-AG8vnSi1W2pbgR2B911EfGqtLE9c4hQBYkv/x7Z+Kt0VxhgQKcW7UNDVYsu9YxwV6u+OJrvdJrMq6DNWoBjihQ=="],
+
+ "@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.4.2", "", { "dependencies": { "@shikijs/types": "3.4.2", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.3" } }, "sha512-1/adJbSMBOkpScCE/SB6XkjJU17ANln3Wky7lOmrnpl+zBdQ1qXUJg2GXTYVHRq+2j3hd1DesmElTXYDgtfSOQ=="],
+
+ "@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.4.2", "", { "dependencies": { "@shikijs/types": "3.4.2", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-zcZKMnNndgRa3ORja6Iemsr3DrLtkX3cAF7lTJkdMB6v9alhlBsX9uNiCpqofNrXOvpA3h6lHcLJxgCIhVOU5Q=="],
+
+ "@shikijs/langs": ["@shikijs/langs@3.4.2", "", { "dependencies": { "@shikijs/types": "3.4.2" } }, "sha512-H6azIAM+OXD98yztIfs/KH5H4PU39t+SREhmM8LaNXyUrqj2mx+zVkr8MWYqjceSjDw9I1jawm1WdFqU806rMA=="],
+
+ "@shikijs/themes": ["@shikijs/themes@3.4.2", "", { "dependencies": { "@shikijs/types": "3.4.2" } }, "sha512-qAEuAQh+brd8Jyej2UDDf+b4V2g1Rm8aBIdvt32XhDPrHvDkEnpb7Kzc9hSuHUxz0Iuflmq7elaDuQAP9bHIhg=="],
+
+ "@shikijs/transformers": ["@shikijs/transformers@3.4.2", "", { "dependencies": { "@shikijs/core": "3.4.2", "@shikijs/types": "3.4.2" } }, "sha512-I5baLVi/ynLEOZoWSAMlACHNnG+yw5HDmse0oe+GW6U1u+ULdEB3UHiVWaHoJSSONV7tlcVxuaMy74sREDkSvg=="],
+
+ "@shikijs/types": ["@shikijs/types@3.4.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-zHC1l7L+eQlDXLnxvM9R91Efh2V4+rN3oMVS2swCBssbj2U/FBwybD1eeLaq8yl/iwT+zih8iUbTBCgGZOYlVg=="],
+
+ "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
+
+ "@so1ve/prettier-config": ["@so1ve/prettier-config@3.1.0", "", { "dependencies": { "@so1ve/prettier-plugin-toml": "3.1.0", "prettier-plugin-astro": "^0.14.0", "prettier-plugin-curly-and-jsdoc": "3.1.0", "prettier-plugin-pkgsort": "^0.2.1" }, "peerDependencies": { "prettier": "^3.0.0" } }, "sha512-9GJ1yXKBC4DzqCTTaZoBf8zw7WWkVuXcccZt1Aqk4lj6ab/GiNUnjPGajUVYLjaqAEOKqM7jUSUfTjk2JTjCAg=="],
+
+ "@so1ve/prettier-plugin-toml": ["@so1ve/prettier-plugin-toml@3.1.0", "", { "peerDependencies": { "prettier": "^3.0.0" } }, "sha512-8WZAGjAVNIJlkfWL6wHKxlUuEBY45fdd5qY5bR/Z6r/txgzKXk/r9qi1DTwc17gi/WcNuRrcRugecRT+mWbIYg=="],
+
+ "@splinetool/runtime": ["@splinetool/runtime@0.9.526", "", { "dependencies": { "on-change": "^4.0.0", "semver-compare": "^1.0.0" } }, "sha512-qznHbXA5aKwDbCgESAothCNm1IeEZcmNWG145p5aXj4w5uoqR1TZ9qkTHTKLTsUbHeitCwdhzmRqan1kxboLgQ=="],
+
+ "@stitches/react": ["@stitches/react@1.2.8", "", { "peerDependencies": { "react": ">= 16.3.0" } }, "sha512-9g9dWI4gsSVe8bNLlb+lMkBYsnIKCZTmvqvDG+Avnn69XfmHZKiaMrx7cgTaddq7aTPPmXiTsbFcUy0xgI4+wA=="],
+
+ "@turf/boolean-clockwise": ["@turf/boolean-clockwise@6.5.0", "", { "dependencies": { "@turf/helpers": "^6.5.0", "@turf/invariant": "^6.5.0" } }, "sha512-45+C7LC5RMbRWrxh3Z0Eihsc8db1VGBO5d9BLTOAwU4jR6SgsunTfRWR16X7JUwIDYlCVEmnjcXJNi/kIU3VIw=="],
+
+ "@turf/clone": ["@turf/clone@6.5.0", "", { "dependencies": { "@turf/helpers": "^6.5.0" } }, "sha512-mzVtTFj/QycXOn6ig+annKrM6ZlimreKYz6f/GSERytOpgzodbQyOgkfwru100O1KQhhjSudKK4DsQ0oyi9cTw=="],
+
+ "@turf/flatten": ["@turf/flatten@6.5.0", "", { "dependencies": { "@turf/helpers": "^6.5.0", "@turf/meta": "^6.5.0" } }, "sha512-IBZVwoNLVNT6U/bcUUllubgElzpMsNoCw8tLqBw6dfYg9ObGmpEjf9BIYLr7a2Yn5ZR4l7YIj2T7kD5uJjZADQ=="],
+
+ "@turf/helpers": ["@turf/helpers@6.5.0", "", {}, "sha512-VbI1dV5bLFzohYYdgqwikdMVpe7pJ9X3E+dlr425wa2/sMJqYDhTO++ec38/pcPvPE6oD9WEEeU3Xu3gza+VPw=="],
+
+ "@turf/invariant": ["@turf/invariant@6.5.0", "", { "dependencies": { "@turf/helpers": "^6.5.0" } }, "sha512-Wv8PRNCtPD31UVbdJE/KVAWKe7l6US+lJItRR/HOEW3eh+U/JwRCSUl/KZ7bmjM/C+zLNoreM2TU6OoLACs4eg=="],
+
+ "@turf/meta": ["@turf/meta@6.5.0", "", { "dependencies": { "@turf/helpers": "^6.5.0" } }, "sha512-RrArvtsV0vdsCBegoBtOalgdSOfkBrTJ07VkpiCnq/491W67hnMWmDu7e6Ztw0C3WldRYTXkg3SumfdzZxLBHA=="],
+
+ "@turf/rewind": ["@turf/rewind@6.5.0", "", { "dependencies": { "@turf/boolean-clockwise": "^6.5.0", "@turf/clone": "^6.5.0", "@turf/helpers": "^6.5.0", "@turf/invariant": "^6.5.0", "@turf/meta": "^6.5.0" } }, "sha512-IoUAMcHWotBWYwSYuYypw/LlqZmO+wcBpn8ysrBNbazkFNkLf3btSDZMkKJO/bvOzl55imr/Xj4fi3DdsLsbzQ=="],
+
+ "@types/acorn": ["@types/acorn@4.0.6", "", { "dependencies": { "@types/estree": "*" } }, "sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ=="],
+
+ "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
+
+ "@types/babel__generator": ["@types/babel__generator@7.6.8", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw=="],
+
+ "@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="],
+
+ "@types/babel__traverse": ["@types/babel__traverse@7.20.6", "", { "dependencies": { "@babel/types": "^7.20.7" } }, "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg=="],
+
+ "@types/d3": ["@types/d3@7.4.3", "", { "dependencies": { "@types/d3-array": "*", "@types/d3-axis": "*", "@types/d3-brush": "*", "@types/d3-chord": "*", "@types/d3-color": "*", "@types/d3-contour": "*", "@types/d3-delaunay": "*", "@types/d3-dispatch": "*", "@types/d3-drag": "*", "@types/d3-dsv": "*", "@types/d3-ease": "*", "@types/d3-fetch": "*", "@types/d3-force": "*", "@types/d3-format": "*", "@types/d3-geo": "*", "@types/d3-hierarchy": "*", "@types/d3-interpolate": "*", "@types/d3-path": "*", "@types/d3-polygon": "*", "@types/d3-quadtree": "*", "@types/d3-random": "*", "@types/d3-scale": "*", "@types/d3-scale-chromatic": "*", "@types/d3-selection": "*", "@types/d3-shape": "*", "@types/d3-time": "*", "@types/d3-time-format": "*", "@types/d3-timer": "*", "@types/d3-transition": "*", "@types/d3-zoom": "*" } }, "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww=="],
+
+ "@types/d3-array": ["@types/d3-array@3.2.1", "", {}, "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg=="],
+
+ "@types/d3-axis": ["@types/d3-axis@3.0.6", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw=="],
+
+ "@types/d3-brush": ["@types/d3-brush@3.0.6", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A=="],
+
+ "@types/d3-chord": ["@types/d3-chord@3.0.6", "", {}, "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg=="],
+
+ "@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="],
+
+ "@types/d3-contour": ["@types/d3-contour@3.0.6", "", { "dependencies": { "@types/d3-array": "*", "@types/geojson": "*" } }, "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg=="],
+
+ "@types/d3-delaunay": ["@types/d3-delaunay@6.0.4", "", {}, "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw=="],
+
+ "@types/d3-dispatch": ["@types/d3-dispatch@3.0.6", "", {}, "sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ=="],
+
+ "@types/d3-drag": ["@types/d3-drag@3.0.7", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ=="],
+
+ "@types/d3-dsv": ["@types/d3-dsv@3.0.7", "", {}, "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g=="],
+
+ "@types/d3-ease": ["@types/d3-ease@3.0.2", "", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="],
+
+ "@types/d3-fetch": ["@types/d3-fetch@3.0.7", "", { "dependencies": { "@types/d3-dsv": "*" } }, "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA=="],
+
+ "@types/d3-force": ["@types/d3-force@3.0.10", "", {}, "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw=="],
+
+ "@types/d3-format": ["@types/d3-format@3.0.4", "", {}, "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g=="],
+
+ "@types/d3-geo": ["@types/d3-geo@3.1.0", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ=="],
+
+ "@types/d3-hierarchy": ["@types/d3-hierarchy@3.1.7", "", {}, "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg=="],
+
+ "@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="],
+
+ "@types/d3-path": ["@types/d3-path@3.1.1", "", {}, "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="],
+
+ "@types/d3-polygon": ["@types/d3-polygon@3.0.2", "", {}, "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA=="],
+
+ "@types/d3-quadtree": ["@types/d3-quadtree@3.0.6", "", {}, "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg=="],
+
+ "@types/d3-random": ["@types/d3-random@3.0.3", "", {}, "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ=="],
+
+ "@types/d3-scale": ["@types/d3-scale@4.0.9", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="],
+
+ "@types/d3-scale-chromatic": ["@types/d3-scale-chromatic@3.1.0", "", {}, "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ=="],
+
+ "@types/d3-selection": ["@types/d3-selection@3.0.11", "", {}, "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w=="],
+
+ "@types/d3-shape": ["@types/d3-shape@3.1.7", "", { "dependencies": { "@types/d3-path": "*" } }, "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg=="],
+
+ "@types/d3-time": ["@types/d3-time@3.0.4", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="],
+
+ "@types/d3-time-format": ["@types/d3-time-format@4.0.3", "", {}, "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg=="],
+
+ "@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="],
+
+ "@types/d3-transition": ["@types/d3-transition@3.0.9", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg=="],
+
+ "@types/d3-zoom": ["@types/d3-zoom@3.0.8", "", { "dependencies": { "@types/d3-interpolate": "*", "@types/d3-selection": "*" } }, "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw=="],
+
+ "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
+
+ "@types/estree": ["@types/estree@1.0.6", "", {}, "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="],
+
+ "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="],
+
+ "@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="],
+
+ "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
+
+ "@types/katex": ["@types/katex@0.16.7", "", {}, "sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ=="],
+
+ "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
+
+ "@types/mdx": ["@types/mdx@2.0.13", "", {}, "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw=="],
+
+ "@types/ms": ["@types/ms@0.7.34", "", {}, "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g=="],
+
+ "@types/parse-author": ["@types/parse-author@2.0.3", "", {}, "sha512-pgRW2K/GVQoogylrGJXDl7PBLW9A6T4OOc9Hy9MLT5f7vgufK2GQ8FcfAbjFHR5HjcN9ByzuCczAORk49REqoA=="],
+
+ "@types/parse-json": ["@types/parse-json@4.0.2", "", {}, "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="],
+
+ "@types/react": ["@types/react@19.1.5", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-piErsCVVbpMMT2r7wbawdZsq4xMvIAhQuac2gedQHysu1TZYEigE6pnFfgZT+/jQnrRuF5r+SHzuehFjfRjr4g=="],
+
+ "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
+
+ "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
+
+ "@ungap/structured-clone": ["@ungap/structured-clone@1.2.1", "", {}, "sha512-fEzPV3hSkSMltkw152tJKNARhOupqbH96MZWyRjNaYZOMIzbrTeQDG+MTc6Mr2pgzFQzFxAfmhGDNP5QK++2ZA=="],
+
+ "@use-gesture/core": ["@use-gesture/core@10.3.1", "", {}, "sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw=="],
+
+ "@use-gesture/react": ["@use-gesture/react@10.3.1", "", { "dependencies": { "@use-gesture/core": "10.3.1" }, "peerDependencies": { "react": ">= 16.8.0" } }, "sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g=="],
+
+ "@visactor/react-vchart": ["@visactor/react-vchart@1.8.11", "", { "dependencies": { "@visactor/vchart": "1.8.11", "@visactor/vgrammar-core": "0.10.11", "@visactor/vrender-core": "0.17.17", "@visactor/vrender-kits": "0.17.17", "@visactor/vutils": "~0.17.3", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-wHnCex9gOpnttTtSu04ozKJhTveUk8Ln2KX/7PZyCJxqlXq+eWvW4zvM6Ja8T8kGXfXtFYVVNh9zBMQ7y2T/Sw=="],
+
+ "@visactor/vchart": ["@visactor/vchart@1.8.11", "", { "dependencies": { "@visactor/vdataset": "~0.17.3", "@visactor/vgrammar-core": "0.10.11", "@visactor/vgrammar-hierarchy": "0.10.11", "@visactor/vgrammar-projection": "0.10.11", "@visactor/vgrammar-sankey": "0.10.11", "@visactor/vgrammar-util": "0.10.11", "@visactor/vgrammar-wordcloud": "0.10.11", "@visactor/vgrammar-wordcloud-shape": "0.10.11", "@visactor/vrender-components": "0.17.17", "@visactor/vrender-core": "0.17.17", "@visactor/vrender-kits": "0.17.17", "@visactor/vscale": "~0.17.3", "@visactor/vutils": "~0.17.3", "@visactor/vutils-extension": "1.8.11" } }, "sha512-RdQ822J02GgAQNXvO1LiT0T3O6FjdgPdcm9hVBFyrpBBmuI8MH02IE7Y1kGe9NiFTH4tDwP0ixRgBmqNSGSLZQ=="],
+
+ "@visactor/vchart-semi-theme": ["@visactor/vchart-semi-theme@1.8.8", "", { "dependencies": { "@visactor/vchart-theme-utils": "1.8.8" }, "peerDependencies": { "@visactor/vchart": "~1.8.8" } }, "sha512-lm57CX3r6Bm7iGBYYyWhDY+1BvkyhNVLEckKx2PnlPKpJHikKSIK2ACyI5SmHuSOOdYzhY2QK6ZfYa2NShJ83w=="],
+
+ "@visactor/vchart-theme-utils": ["@visactor/vchart-theme-utils@1.8.8", "", { "peerDependencies": { "@visactor/vchart": "~1.8.8" } }, "sha512-RdCey3/t0+82EYyFZvx210rgJJWti9rsgcL3ROZS7o9CtRW1CMj9u9LKLDNIcPLNcLNACFC0aoT03jpdD1BCpA=="],
+
+ "@visactor/vdataset": ["@visactor/vdataset@0.17.5", "", { "dependencies": { "@turf/flatten": "^6.5.0", "@turf/helpers": "^6.5.0", "@turf/rewind": "^6.5.0", "@visactor/vutils": "0.17.5", "d3-dsv": "^2.0.0", "d3-geo": "^1.12.1", "d3-hexbin": "^0.2.2", "d3-hierarchy": "^3.1.1", "eventemitter3": "^4.0.7", "geobuf": "^3.0.1", "geojson-dissolve": "^3.1.0", "path-browserify": "^1.0.1", "pbf": "^3.2.1", "point-at-length": "^1.1.0", "simple-statistics": "^7.7.3", "simplify-geojson": "^1.0.4", "topojson-client": "^3.1.0" } }, "sha512-zVBdLWHWrhldGc8JDjSYF9lvpFT4ZEFQDB0b6yvfSiHzHKHiSco+rWmUFvA7r4ObT6j2QWF1vZAV9To8Ml4vHw=="],
+
+ "@visactor/vgrammar-coordinate": ["@visactor/vgrammar-coordinate@0.10.11", "", { "dependencies": { "@visactor/vgrammar-util": "0.10.11", "@visactor/vutils": "~0.17.3" } }, "sha512-XSUvEkaf/NQHFafmTwqoIMZicp9fF3o6NB2FDpuWrK4DI1lTuip/0RkqrC+kBAjc5erjt0em0TiITyqXpp4G6w=="],
+
+ "@visactor/vgrammar-core": ["@visactor/vgrammar-core@0.10.11", "", { "dependencies": { "@visactor/vdataset": "~0.17.3", "@visactor/vgrammar-coordinate": "0.10.11", "@visactor/vgrammar-util": "0.10.11", "@visactor/vrender-components": "0.17.17", "@visactor/vrender-core": "0.17.17", "@visactor/vrender-kits": "0.17.17", "@visactor/vscale": "~0.17.3", "@visactor/vutils": "~0.17.3" } }, "sha512-VL9vcLPDg1LrHl7EOx0Ga9ATsoaChKIaCGzxjrPEjWiIS5VPU9Rs0jBKP+ch8BjamAoSuqL5mKd0L/RaUBqlaA=="],
+
+ "@visactor/vgrammar-hierarchy": ["@visactor/vgrammar-hierarchy@0.10.11", "", { "dependencies": { "@visactor/vgrammar-core": "0.10.11", "@visactor/vgrammar-util": "0.10.11", "@visactor/vrender-core": "0.17.17", "@visactor/vrender-kits": "0.17.17", "@visactor/vutils": "~0.17.3" } }, "sha512-0r3k51pPlJHu63BduG3htsV/ul62aVcKJxFftRfvKkwGjm1KeHoOZEEAwIf78U2puio0BkLqVn2Ek2L4FYZaIg=="],
+
+ "@visactor/vgrammar-projection": ["@visactor/vgrammar-projection@0.10.11", "", { "dependencies": { "@visactor/vgrammar-core": "0.10.11", "@visactor/vgrammar-util": "0.10.11", "@visactor/vutils": "~0.17.3", "d3-geo": "^1.12.1" } }, "sha512-yEiKsxdfs5+g60wv5xZ1kyS/EDrAsUzAxCMpFFASVUYbQObHvW+elm+UPq2TBX6KZqAM0gsd1inzaLvfsCrLSg=="],
+
+ "@visactor/vgrammar-sankey": ["@visactor/vgrammar-sankey@0.10.11", "", { "dependencies": { "@visactor/vgrammar-core": "0.10.11", "@visactor/vgrammar-util": "0.10.11", "@visactor/vrender-core": "0.17.17", "@visactor/vrender-kits": "0.17.17", "@visactor/vutils": "~0.17.3" } }, "sha512-BbJTPuyydsL/L5XtQv59Q82GgJeePY7Wleac798usx3GnDK0GAOrPsI3bubSsOESJ4pNk3V4HPGEQDG1vCPb4w=="],
+
+ "@visactor/vgrammar-util": ["@visactor/vgrammar-util@0.10.11", "", { "dependencies": { "@visactor/vutils": "~0.17.3" } }, "sha512-cJZLmKZvN95Y+yGhX+28+UpZu3bhYYlXDlHJNvXHyonI76ZYgtceyon2b3lI6XIsUsBGcD4Uo777s949X5os3g=="],
+
+ "@visactor/vgrammar-wordcloud": ["@visactor/vgrammar-wordcloud@0.10.11", "", { "dependencies": { "@visactor/vgrammar-core": "0.10.11", "@visactor/vgrammar-util": "0.10.11", "@visactor/vrender-core": "0.17.17", "@visactor/vrender-kits": "0.17.17", "@visactor/vutils": "~0.17.3" } }, "sha512-JWDqjGhr9JlYkKVBeEkiOqLQk7C1x1BtnsZ+E8oN541gzUqHwfS9qZyhwI3OyoSLewJlsSSPu1vXLKSQzLzKPA=="],
+
+ "@visactor/vgrammar-wordcloud-shape": ["@visactor/vgrammar-wordcloud-shape@0.10.11", "", { "dependencies": { "@visactor/vgrammar-core": "0.10.11", "@visactor/vgrammar-util": "0.10.11", "@visactor/vrender-core": "0.17.17", "@visactor/vrender-kits": "0.17.17", "@visactor/vscale": "~0.17.3", "@visactor/vutils": "~0.17.3" } }, "sha512-NsQOYJp+9WHnIApMvkcUOaajxIg5U/r6rD8LKnoXW/HqAN2TFYXcRR3Daqmk9rrpM5VztQimKOsA1yZWyzozrA=="],
+
+ "@visactor/vrender-components": ["@visactor/vrender-components@0.17.17", "", { "dependencies": { "@visactor/vrender-core": "0.17.17", "@visactor/vrender-kits": "0.17.17", "@visactor/vscale": "~0.17.3", "@visactor/vutils": "~0.17.3" } }, "sha512-7gYFQrozvBkyGF7s/JHXdWDZnATzymxzug63CZd4EB7A0OXKatVDImXRePqwzlPD3QamF7QMVWn0CuIx3gQ2gA=="],
+
+ "@visactor/vrender-core": ["@visactor/vrender-core@0.17.17", "", { "dependencies": { "@visactor/vutils": "~0.17.3", "color-convert": "2.0.1" } }, "sha512-pAZGaimunDAWOBdFhzPh0auH5ryxAHr+MVoz+QdASG+6RZXy8D02l8v2QYu4+e4uorxe/s2ZkdNDm81SlNkoHQ=="],
+
+ "@visactor/vrender-kits": ["@visactor/vrender-kits@0.17.17", "", { "dependencies": { "@resvg/resvg-js": "2.4.1", "@visactor/vrender-core": "0.17.17", "@visactor/vutils": "~0.17.3", "roughjs": "4.5.2" } }, "sha512-noRP1hAHvPCv36nf2P6sZ930Tk+dJ8jpPWIUm1cFYmUNdcumgIS8Cug0RyeZ+saSqVt5FDTwIwifhOqupw5Zaw=="],
+
+ "@visactor/vscale": ["@visactor/vscale@0.17.5", "", { "dependencies": { "@visactor/vutils": "0.17.5" } }, "sha512-2dkS1IlAJ/IdTp8JElbctOOv6lkHKBKPDm8KvwBo0NuGWQeYAebSeyN3QCdwKbj76gMlCub4zc+xWrS5YiA2zA=="],
+
+ "@visactor/vutils": ["@visactor/vutils@0.17.5", "", { "dependencies": { "@turf/helpers": "^6.5.0", "@turf/invariant": "^6.5.0", "eventemitter3": "^4.0.7" } }, "sha512-HFN6Pk1Wc1RK842g02MeKOlvdri5L7/nqxMVTqxIvi0XMhHXpmoqN4+/9H+h8LmJpVohyrI/MT85TRBV/rManw=="],
+
+ "@visactor/vutils-extension": ["@visactor/vutils-extension@1.8.11", "", { "dependencies": { "@visactor/vrender-core": "0.17.17", "@visactor/vrender-kits": "0.17.17", "@visactor/vscale": "~0.17.3", "@visactor/vutils": "~0.17.3" } }, "sha512-Hknzpy3+xh4sdL0iSn5N93BHiMJF4FdwSwhHYEibRpriZmWKG6wBxsJ0Bll4d7oS4f+svxt8Sg2vRYKzQEcIxQ=="],
+
+ "@vitejs/plugin-react": ["@vitejs/plugin-react@4.3.4", "", { "dependencies": { "@babel/core": "^7.26.0", "@babel/plugin-transform-react-jsx-self": "^7.25.9", "@babel/plugin-transform-react-jsx-source": "^7.25.9", "@types/babel__core": "^7.20.5", "react-refresh": "^0.14.2" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" } }, "sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug=="],
+
+ "abs-svg-path": ["abs-svg-path@0.1.1", "", {}, "sha512-d8XPSGjfyzlXC3Xx891DJRyZfqk5JU0BJrDQcsWomFIV1/BIzPW5HDH5iDdWpqWaav0YVIEzT1RHTwWr0FFshA=="],
+
+ "acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="],
+
+ "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
+
+ "ahooks": ["ahooks@3.8.5", "", { "dependencies": { "@babel/runtime": "^7.21.0", "dayjs": "^1.9.1", "intersection-observer": "^0.12.0", "js-cookie": "^3.0.5", "lodash": "^4.17.21", "react-fast-compare": "^3.2.2", "resize-observer-polyfill": "^1.5.1", "screenfull": "^5.0.0", "tslib": "^2.4.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Y+MLoJpBXVdjsnnBjE5rOSPkQ4DK+8i5aPDzLJdIOsCpo/fiAeXcBY1Y7oWgtOK0TpOz0gFa/XcyO1UGdoqLcw=="],
+
+ "ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
+
+ "ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="],
+
+ "antd": ["antd@5.25.2", "", { "dependencies": { "@ant-design/colors": "^7.2.0", "@ant-design/cssinjs": "^1.23.0", "@ant-design/cssinjs-utils": "^1.1.3", "@ant-design/fast-color": "^2.0.6", "@ant-design/icons": "^5.6.1", "@ant-design/react-slick": "~1.1.2", "@babel/runtime": "^7.26.0", "@rc-component/color-picker": "~2.0.1", "@rc-component/mutate-observer": "^1.1.0", "@rc-component/qrcode": "~1.0.0", "@rc-component/tour": "~1.15.1", "@rc-component/trigger": "^2.2.6", "classnames": "^2.5.1", "copy-to-clipboard": "^3.3.3", "dayjs": "^1.11.11", "rc-cascader": "~3.34.0", "rc-checkbox": "~3.5.0", "rc-collapse": "~3.9.0", "rc-dialog": "~9.6.0", "rc-drawer": "~7.2.0", "rc-dropdown": "~4.2.1", "rc-field-form": "~2.7.0", "rc-image": "~7.12.0", "rc-input": "~1.8.0", "rc-input-number": "~9.5.0", "rc-mentions": "~2.20.0", "rc-menu": "~9.16.1", "rc-motion": "^2.9.5", "rc-notification": "~5.6.4", "rc-pagination": "~5.1.0", "rc-picker": "~4.11.3", "rc-progress": "~4.0.0", "rc-rate": "~2.13.1", "rc-resize-observer": "^1.4.3", "rc-segmented": "~2.7.0", "rc-select": "~14.16.8", "rc-slider": "~11.1.8", "rc-steps": "~6.0.1", "rc-switch": "~4.1.0", "rc-table": "~7.50.5", "rc-tabs": "~15.6.1", "rc-textarea": "~1.10.0", "rc-tooltip": "~6.4.0", "rc-tree": "~5.13.1", "rc-tree-select": "~5.27.0", "rc-upload": "~4.9.0", "rc-util": "^5.44.4", "scroll-into-view-if-needed": "^3.1.0", "throttle-debounce": "^5.0.2" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-7R2nUvlHhey7Trx64+hCtGXOiy+DTUs1Lv5bwbV1LzEIZIhWb0at1AM6V3K108a5lyoR9n7DX3ptlLF7uYV/DQ=="],
+
+ "antd-style": ["antd-style@3.7.1", "", { "dependencies": { "@ant-design/cssinjs": "^1.21.1", "@babel/runtime": "^7.24.1", "@emotion/cache": "^11.11.0", "@emotion/css": "^11.11.2", "@emotion/react": "^11.11.4", "@emotion/serialize": "^1.1.3", "@emotion/utils": "^1.2.1", "use-merge-value": "^1.2.0" }, "peerDependencies": { "antd": ">=5.8.1", "react": ">=18" } }, "sha512-CQOfddVp4aOvBfCepa+Kj2e7ap+2XBINg1Kn2osdE3oQvrD7KJu/K0sfnLcFLkgCJygbxmuazYdWLKb+drPDYA=="],
+
+ "any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="],
+
+ "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="],
+
+ "arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="],
+
+ "array-source": ["array-source@0.0.4", "", {}, "sha512-frNdc+zBn80vipY+GdcJkLEbMWj3xmzArYApmUGxoiV8uAu/ygcs9icPdsGdA26h0MkHUMW6EN2piIvVx+M5Mw=="],
+
+ "assign-symbols": ["assign-symbols@1.0.0", "", {}, "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw=="],
+
+ "astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="],
+
+ "async-validator": ["async-validator@3.5.2", "", {}, "sha512-8eLCg00W9pIRZSB781UUX/H6Oskmm8xloZfr09lz5bikRpBVDlJ3hRVuxxP1SxcwsEYfJ4IU8Q19Y8/893r3rQ=="],
+
+ "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
+
+ "attr-accept": ["attr-accept@2.2.5", "", {}, "sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ=="],
+
+ "author-regex": ["author-regex@1.0.0", "", {}, "sha512-KbWgR8wOYRAPekEmMXrYYdc7BRyhn2Ftk7KWfMUnQ43hFdojWEFRxhhRUm3/OFEdPa1r0KAvTTg9YQK57xTe0g=="],
+
+ "autoprefixer": ["autoprefixer@10.4.21", "", { "dependencies": { "browserslist": "^4.24.4", "caniuse-lite": "^1.0.30001702", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ=="],
+
+ "axios": ["axios@0.27.2", "", { "dependencies": { "follow-redirects": "^1.14.9", "form-data": "^4.0.0" } }, "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ=="],
+
+ "babel-plugin-macros": ["babel-plugin-macros@3.1.0", "", { "dependencies": { "@babel/runtime": "^7.12.5", "cosmiconfig": "^7.0.0", "resolve": "^1.19.0" } }, "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg=="],
+
+ "babel-plugin-polyfill-corejs2": ["babel-plugin-polyfill-corejs2@0.4.13", "", { "dependencies": { "@babel/compat-data": "^7.22.6", "@babel/helper-define-polyfill-provider": "^0.6.4", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-3sX/eOms8kd3q2KZ6DAhKPc0dgm525Gqq5NtWKZ7QYYZEv57OQ54KtblzJzH1lQF/eQxO8KjWGIK9IPUJNus5g=="],
+
+ "babel-plugin-polyfill-corejs3": ["babel-plugin-polyfill-corejs3@0.11.1", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.3", "core-js-compat": "^3.40.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-yGCqvBT4rwMczo28xkH/noxJ6MZ4nJfkVYdoDaC/utLtWrXxv27HVrzAeSbqR8SxDsp46n0YF47EbHoixy6rXQ=="],
+
+ "babel-plugin-polyfill-regenerator": ["babel-plugin-polyfill-regenerator@0.6.4", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.4" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-7gD3pRadPrbjhjLyxebmx/WrFYcuSjZ0XbdUujQMZ/fcE9oeewk2U/7PCvez84UeuK3oSjmPZ0Ch0dlupQvGzw=="],
+
+ "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
+
+ "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
+
+ "bezier-easing": ["bezier-easing@2.1.0", "", {}, "sha512-gbIqZ/eslnUFC1tjEvtz0sgx+xTK20wDnYMIA27VA04R7w6xxXQPZDbibjA9DTWZRA2CXtwHykkVzlCaAJAZig=="],
+
+ "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],
+
+ "brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="],
+
+ "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
+
+ "browserslist": ["browserslist@4.24.5", "", { "dependencies": { "caniuse-lite": "^1.0.30001716", "electron-to-chromium": "^1.5.149", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw=="],
+
+ "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
+
+ "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
+
+ "camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="],
+
+ "caniuse-lite": ["caniuse-lite@1.0.30001718", "", {}, "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw=="],
+
+ "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
+
+ "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="],
+
+ "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="],
+
+ "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="],
+
+ "character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="],
+
+ "chevrotain": ["chevrotain@11.0.3", "", { "dependencies": { "@chevrotain/cst-dts-gen": "11.0.3", "@chevrotain/gast": "11.0.3", "@chevrotain/regexp-to-ast": "11.0.3", "@chevrotain/types": "11.0.3", "@chevrotain/utils": "11.0.3", "lodash-es": "4.17.21" } }, "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw=="],
+
+ "chevrotain-allstar": ["chevrotain-allstar@0.3.1", "", { "dependencies": { "lodash-es": "^4.17.21" }, "peerDependencies": { "chevrotain": "^11.0.0" } }, "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw=="],
+
+ "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
+
+ "chroma-js": ["chroma-js@3.1.2", "", {}, "sha512-IJnETTalXbsLx1eKEgx19d5L6SRM7cH4vINw/99p/M11HCuXGRWL+6YmCm7FWFGIo6dtWuQoQi1dc5yQ7ESIHg=="],
+
+ "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
+
+ "classnames": ["classnames@2.5.1", "", {}, "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="],
+
+ "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
+
+ "collapse-white-space": ["collapse-white-space@2.1.0", "", {}, "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw=="],
+
+ "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
+
+ "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
+
+ "colord": ["colord@2.9.3", "", {}, "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw=="],
+
+ "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
+
+ "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
+
+ "commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
+
+ "compute-scroll-into-view": ["compute-scroll-into-view@1.0.20", "", {}, "sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg=="],
+
+ "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
+
+ "concat-stream": ["concat-stream@2.0.0", "", { "dependencies": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.0.2", "typedarray": "^0.0.6" } }, "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A=="],
+
+ "confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="],
+
+ "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
+
+ "copy-text-to-clipboard": ["copy-text-to-clipboard@2.2.0", "", {}, "sha512-WRvoIdnTs1rgPMkgA2pUOa/M4Enh2uzCwdKsOMYNAJiz/4ZvEJgmbF4OmninPmlFdAWisfeh0tH+Cpf7ni3RqQ=="],
+
+ "copy-to-clipboard": ["copy-to-clipboard@3.3.3", "", { "dependencies": { "toggle-selection": "^1.0.6" } }, "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA=="],
+
+ "core-js-compat": ["core-js-compat@3.42.0", "", { "dependencies": { "browserslist": "^4.24.4" } }, "sha512-bQasjMfyDGyaeWKBIu33lHh9qlSR0MFE/Nmc6nMjf/iU9b3rSMdAYz1Baxrv4lPdGUsTqZudHA4jIGSJy0SWZQ=="],
+
+ "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="],
+
+ "cose-base": ["cose-base@1.0.3", "", { "dependencies": { "layout-base": "^1.0.0" } }, "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg=="],
+
+ "cosmiconfig": ["cosmiconfig@7.1.0", "", { "dependencies": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.2.1", "parse-json": "^5.0.0", "path-type": "^4.0.0", "yaml": "^1.10.0" } }, "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA=="],
+
+ "country-flag-icons": ["country-flag-icons@1.5.19", "", {}, "sha512-D/ZkRyj+ywJC6b2IrAN3/tpbReMUqmuRLlcKFoY/o0+EPQN9Ev/e8tV+D3+9scvu/tarxwLErNwS73C3yzxs/g=="],
+
+ "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
+
+ "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
+
+ "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
+
+ "cytoscape": ["cytoscape@3.32.0", "", {}, "sha512-5JHBC9n75kz5851jeklCPmZWcg3hUe6sjqJvyk3+hVqFaKcHwHgxsjeN1yLmggoUc6STbtm9/NQyabQehfjvWQ=="],
+
+ "cytoscape-cose-bilkent": ["cytoscape-cose-bilkent@4.1.0", "", { "dependencies": { "cose-base": "^1.0.0" }, "peerDependencies": { "cytoscape": "^3.2.0" } }, "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ=="],
+
+ "cytoscape-fcose": ["cytoscape-fcose@2.2.0", "", { "dependencies": { "cose-base": "^2.2.0" }, "peerDependencies": { "cytoscape": "^3.2.0" } }, "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ=="],
+
+ "d3": ["d3@7.9.0", "", { "dependencies": { "d3-array": "3", "d3-axis": "3", "d3-brush": "3", "d3-chord": "3", "d3-color": "3", "d3-contour": "4", "d3-delaunay": "6", "d3-dispatch": "3", "d3-drag": "3", "d3-dsv": "3", "d3-ease": "3", "d3-fetch": "3", "d3-force": "3", "d3-format": "3", "d3-geo": "3", "d3-hierarchy": "3", "d3-interpolate": "3", "d3-path": "3", "d3-polygon": "3", "d3-quadtree": "3", "d3-random": "3", "d3-scale": "4", "d3-scale-chromatic": "3", "d3-selection": "3", "d3-shape": "3", "d3-time": "3", "d3-time-format": "4", "d3-timer": "3", "d3-transition": "3", "d3-zoom": "3" } }, "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA=="],
+
+ "d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="],
+
+ "d3-axis": ["d3-axis@3.0.0", "", {}, "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw=="],
+
+ "d3-brush": ["d3-brush@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "3", "d3-transition": "3" } }, "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ=="],
+
+ "d3-chord": ["d3-chord@3.0.1", "", { "dependencies": { "d3-path": "1 - 3" } }, "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g=="],
+
+ "d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="],
+
+ "d3-contour": ["d3-contour@4.0.2", "", { "dependencies": { "d3-array": "^3.2.0" } }, "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA=="],
+
+ "d3-delaunay": ["d3-delaunay@6.0.4", "", { "dependencies": { "delaunator": "5" } }, "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A=="],
+
+ "d3-dispatch": ["d3-dispatch@3.0.1", "", {}, "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg=="],
+
+ "d3-drag": ["d3-drag@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-selection": "3" } }, "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg=="],
+
+ "d3-dsv": ["d3-dsv@2.0.0", "", { "dependencies": { "commander": "2", "iconv-lite": "0.4", "rw": "1" }, "bin": { "csv2json": "bin/dsv2json", "csv2tsv": "bin/dsv2dsv", "dsv2dsv": "bin/dsv2dsv", "dsv2json": "bin/dsv2json", "json2csv": "bin/json2dsv", "json2dsv": "bin/json2dsv", "json2tsv": "bin/json2dsv", "tsv2csv": "bin/dsv2dsv", "tsv2json": "bin/dsv2json" } }, "sha512-E+Pn8UJYx9mViuIUkoc93gJGGYut6mSDKy2+XaPwccwkRGlR+LO97L2VCCRjQivTwLHkSnAJG7yo00BWY6QM+w=="],
+
+ "d3-ease": ["d3-ease@3.0.1", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="],
+
+ "d3-fetch": ["d3-fetch@3.0.1", "", { "dependencies": { "d3-dsv": "1 - 3" } }, "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw=="],
+
+ "d3-force": ["d3-force@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-quadtree": "1 - 3", "d3-timer": "1 - 3" } }, "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg=="],
+
+ "d3-format": ["d3-format@3.1.0", "", {}, "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA=="],
+
+ "d3-geo": ["d3-geo@1.12.1", "", { "dependencies": { "d3-array": "1" } }, "sha512-XG4d1c/UJSEX9NfU02KwBL6BYPj8YKHxgBEw5om2ZnTRSbIcego6dhHwcxuSR3clxh0EpE38os1DVPOmnYtTPg=="],
+
+ "d3-hexbin": ["d3-hexbin@0.2.2", "", {}, "sha512-KS3fUT2ReD4RlGCjvCEm1RgMtp2NFZumdMu4DBzQK8AZv3fXRM6Xm8I4fSU07UXvH4xxg03NwWKWdvxfS/yc4w=="],
+
+ "d3-hierarchy": ["d3-hierarchy@3.1.2", "", {}, "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA=="],
+
+ "d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="],
+
+ "d3-path": ["d3-path@3.1.0", "", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="],
+
+ "d3-polygon": ["d3-polygon@3.0.1", "", {}, "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg=="],
+
+ "d3-quadtree": ["d3-quadtree@3.0.1", "", {}, "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw=="],
+
+ "d3-random": ["d3-random@3.0.1", "", {}, "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ=="],
+
+ "d3-sankey": ["d3-sankey@0.12.3", "", { "dependencies": { "d3-array": "1 - 2", "d3-shape": "^1.2.0" } }, "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ=="],
+
+ "d3-scale": ["d3-scale@4.0.2", "", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="],
+
+ "d3-scale-chromatic": ["d3-scale-chromatic@3.1.0", "", { "dependencies": { "d3-color": "1 - 3", "d3-interpolate": "1 - 3" } }, "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ=="],
+
+ "d3-selection": ["d3-selection@3.0.0", "", {}, "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ=="],
+
+ "d3-shape": ["d3-shape@3.2.0", "", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="],
+
+ "d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="],
+
+ "d3-time-format": ["d3-time-format@4.1.0", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="],
+
+ "d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="],
+
+ "d3-transition": ["d3-transition@3.0.1", "", { "dependencies": { "d3-color": "1 - 3", "d3-dispatch": "1 - 3", "d3-ease": "1 - 3", "d3-interpolate": "1 - 3", "d3-timer": "1 - 3" }, "peerDependencies": { "d3-selection": "2 - 3" } }, "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w=="],
+
+ "d3-zoom": ["d3-zoom@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "2 - 3", "d3-transition": "2 - 3" } }, "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw=="],
+
+ "dagre-d3-es": ["dagre-d3-es@7.0.11", "", { "dependencies": { "d3": "^7.9.0", "lodash-es": "^4.17.21" } }, "sha512-tvlJLyQf834SylNKax8Wkzco/1ias1OPw8DcUMDE7oUIoSEW25riQVuiu/0OWEFqT0cxHT3Pa9/D82Jr47IONw=="],
+
+ "date-fns": ["date-fns@2.30.0", "", { "dependencies": { "@babel/runtime": "^7.21.0" } }, "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw=="],
+
+ "date-fns-tz": ["date-fns-tz@1.3.8", "", { "peerDependencies": { "date-fns": ">=2.0.0" } }, "sha512-qwNXUFtMHTTU6CFSFjoJ80W8Fzzp24LntbjFFBgL/faqds4e5mo9mftoRLgr3Vi1trISsg4awSpYVsOQCRnapQ=="],
+
+ "dayjs": ["dayjs@1.11.13", "", {}, "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg=="],
+
+ "debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="],
+
+ "decode-named-character-reference": ["decode-named-character-reference@1.0.2", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg=="],
+
+ "decode-uri-component": ["decode-uri-component@0.4.1", "", {}, "sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ=="],
+
+ "delaunator": ["delaunator@5.0.1", "", { "dependencies": { "robust-predicates": "^3.0.2" } }, "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw=="],
+
+ "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
+
+ "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
+
+ "detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="],
+
+ "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
+
+ "didyoumean": ["didyoumean@1.2.2", "", {}, "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="],
+
+ "dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="],
+
+ "dompurify": ["dompurify@3.2.6", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ=="],
+
+ "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
+
+ "electron-to-chromium": ["electron-to-chromium@1.5.157", "", {}, "sha512-/0ybgsQd1muo8QlnuTpKwtl0oX5YMlUGbm8xyqgDU00motRkKFFbUJySAQBWcY79rVqNLWIWa87BGVGClwAB2w=="],
+
+ "emoji-mart": ["emoji-mart@5.6.0", "", {}, "sha512-eJp3QRe79pjwa+duv+n7+5YsNhRcMl812EcFVwrnRvYKoNPoQb5qxU8DG6Bgwji0akHdp6D4Ln6tYLG58MFSow=="],
+
+ "emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="],
+
+ "entities": ["entities@6.0.0", "", {}, "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw=="],
+
+ "error-ex": ["error-ex@1.3.2", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g=="],
+
+ "esast-util-from-estree": ["esast-util-from-estree@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "unist-util-position-from-estree": "^2.0.0" } }, "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ=="],
+
+ "esast-util-from-js": ["esast-util-from-js@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "acorn": "^8.0.0", "esast-util-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw=="],
+
+ "esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="],
+
+ "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
+
+ "escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
+
+ "estree-util-attach-comments": ["estree-util-attach-comments@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw=="],
+
+ "estree-util-build-jsx": ["estree-util-build-jsx@3.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-walker": "^3.0.0" } }, "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ=="],
+
+ "estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="],
+
+ "estree-util-scope": ["estree-util-scope@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0" } }, "sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ=="],
+
+ "estree-util-to-js": ["estree-util-to-js@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "astring": "^1.8.0", "source-map": "^0.7.0" } }, "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg=="],
+
+ "estree-util-visit": ["estree-util-visit@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/unist": "^3.0.0" } }, "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww=="],
+
+ "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
+
+ "eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="],
+
+ "exsolve": ["exsolve@1.0.5", "", {}, "sha512-pz5dvkYYKQ1AHVrgOzBKWeP4u4FRb3a6DNK2ucr0OoNwYIU4QWsJ+NM36LLzORT+z845MzKHHhpXiUF5nvQoJg=="],
+
+ "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
+
+ "extend-shallow": ["extend-shallow@2.0.1", "", { "dependencies": { "is-extendable": "^0.1.0" } }, "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug=="],
+
+ "fast-copy": ["fast-copy@3.0.2", "", {}, "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ=="],
+
+ "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
+
+ "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
+
+ "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="],
+
+ "file-selector": ["file-selector@2.1.2", "", { "dependencies": { "tslib": "^2.7.0" } }, "sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig=="],
+
+ "file-source": ["file-source@0.6.1", "", { "dependencies": { "stream-source": "0.3" } }, "sha512-1R1KneL7eTXmXfKxC10V/9NeGOdbsAXJ+lQ//fvvcHUgtaZcZDWNJNblxAoVOyV1cj45pOtUrR3vZTBwqcW8XA=="],
+
+ "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
+
+ "filter-obj": ["filter-obj@5.1.0", "", {}, "sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng=="],
+
+ "find-root": ["find-root@1.1.0", "", {}, "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="],
+
+ "follow-redirects": ["follow-redirects@1.15.9", "", {}, "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="],
+
+ "for-in": ["for-in@1.0.2", "", {}, "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ=="],
+
+ "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
+
+ "form-data": ["form-data@4.0.1", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "mime-types": "^2.1.12" } }, "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw=="],
+
+ "fraction.js": ["fraction.js@4.3.7", "", {}, "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew=="],
+
+ "framer-motion": ["framer-motion@12.12.2", "", { "dependencies": { "motion-dom": "^12.12.1", "motion-utils": "^12.12.1", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-qCszZCiGWkilL40E3VuhIJJC/CS3SIBl2IHyGK8FU30nOUhTmhBNWPrNFyozAWH/bXxwzi19vJHIGVdALF0LCg=="],
+
+ "fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="],
+
+ "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
+
+ "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
+
+ "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
+
+ "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
+
+ "geobuf": ["geobuf@3.0.2", "", { "dependencies": { "concat-stream": "^2.0.0", "pbf": "^3.2.1", "shapefile": "~0.6.6" }, "bin": { "geobuf2json": "bin/geobuf2json", "json2geobuf": "bin/json2geobuf", "shp2geobuf": "bin/shp2geobuf" } }, "sha512-ASgKwEAQQRnyNFHNvpd5uAwstbVYmiTW0Caw3fBb509tNTqXyAAPMyFs5NNihsLZhLxU1j/kjFhkhLWA9djuVg=="],
+
+ "geojson-dissolve": ["geojson-dissolve@3.1.0", "", { "dependencies": { "@turf/meta": "^3.7.5", "geojson-flatten": "^0.2.1", "geojson-linestring-dissolve": "0.0.1", "topojson-client": "^3.0.0", "topojson-server": "^3.0.0" } }, "sha512-JXHfn+A3tU392HA703gJbjmuHaQOAE/C1KzbELCczFRFux+GdY6zt1nKb1VMBHp4LWeE7gUY2ql+g06vJqhiwQ=="],
+
+ "geojson-flatten": ["geojson-flatten@0.2.4", "", { "dependencies": { "get-stdin": "^6.0.0", "minimist": "1.2.0" }, "bin": { "geojson-flatten": "./geojson-flatten" } }, "sha512-LiX6Jmot8adiIdZ/fthbcKKPOfWjTQchX/ggHnwMZ2e4b0I243N1ANUos0LvnzepTEsj0+D4fIJ5bKhBrWnAHA=="],
+
+ "geojson-linestring-dissolve": ["geojson-linestring-dissolve@0.0.1", "", {}, "sha512-Y8I2/Ea28R/Xeki7msBcpMvJL2TaPfaPKP8xqueJfQ9/jEhps+iOJxOR2XCBGgVb12Z6XnDb1CMbaPfLepsLaw=="],
+
+ "get-stdin": ["get-stdin@6.0.0", "", {}, "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g=="],
+
+ "get-value": ["get-value@2.0.6", "", {}, "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA=="],
+
+ "giscus": ["giscus@1.6.0", "", { "dependencies": { "lit": "^3.2.1" } }, "sha512-Zrsi8r4t1LVW950keaWcsURuZUQwUaMKjvJgTCY125vkW6OiEBkatE7ScJDbpqKHdZwb///7FVC21SE3iFK3PQ=="],
+
+ "glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="],
+
+ "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
+
+ "globals": ["globals@15.15.0", "", {}, "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg=="],
+
+ "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
+
+ "hachure-fill": ["hachure-fill@0.5.2", "", {}, "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg=="],
+
+ "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
+
+ "hast-util-from-dom": ["hast-util-from-dom@5.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hastscript": "^9.0.0", "web-namespaces": "^2.0.0" } }, "sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q=="],
+
+ "hast-util-from-html": ["hast-util-from-html@2.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.1.0", "hast-util-from-parse5": "^8.0.0", "parse5": "^7.0.0", "vfile": "^6.0.0", "vfile-message": "^4.0.0" } }, "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw=="],
+
+ "hast-util-from-html-isomorphic": ["hast-util-from-html-isomorphic@2.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-from-dom": "^5.0.0", "hast-util-from-html": "^2.0.0", "unist-util-remove-position": "^5.0.0" } }, "sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw=="],
+
+ "hast-util-from-parse5": ["hast-util-from-parse5@8.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "hastscript": "^9.0.0", "property-information": "^7.0.0", "vfile": "^6.0.0", "vfile-location": "^5.0.0", "web-namespaces": "^2.0.0" } }, "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg=="],
+
+ "hast-util-is-element": ["hast-util-is-element@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g=="],
+
+ "hast-util-parse-selector": ["hast-util-parse-selector@4.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A=="],
+
+ "hast-util-raw": ["hast-util-raw@9.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-from-parse5": "^8.0.0", "hast-util-to-parse5": "^8.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "parse5": "^7.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw=="],
+
+ "hast-util-to-estree": ["hast-util-to-estree@3.1.1", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-attach-comments": "^3.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^6.0.0", "space-separated-tokens": "^2.0.0", "style-to-object": "^1.0.0", "unist-util-position": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-IWtwwmPskfSmma9RpzCappDUitC8t5jhAynHhc1m2+5trOgsrp7txscUSavc5Ic8PATyAjfrCK1wgtxh2cICVQ=="],
+
+ "hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="],
+
+ "hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.2", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^6.0.0", "space-separated-tokens": "^2.0.0", "style-to-object": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-1ngXYb+V9UT5h+PxNRa1O1FYguZK/XL+gkeqvp7EdHlB9oHUG0eYRo/vY5inBdcqo3RkPMC58/H94HvkbfGdyg=="],
+
+ "hast-util-to-parse5": ["hast-util-to-parse5@8.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "property-information": "^6.0.0", "space-separated-tokens": "^2.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw=="],
+
+ "hast-util-to-text": ["hast-util-to-text@4.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "hast-util-is-element": "^3.0.0", "unist-util-find-after": "^5.0.0" } }, "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A=="],
+
+ "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="],
+
+ "hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="],
+
+ "highlight.js": ["highlight.js@11.11.1", "", {}, "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w=="],
+
+ "history": ["history@5.3.0", "", { "dependencies": { "@babel/runtime": "^7.7.6" } }, "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ=="],
+
+ "hoist-non-react-statics": ["hoist-non-react-statics@3.3.2", "", { "dependencies": { "react-is": "^16.7.0" } }, "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw=="],
+
+ "html-parse-stringify": ["html-parse-stringify@3.0.1", "", { "dependencies": { "void-elements": "3.1.0" } }, "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg=="],
+
+ "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="],
+
+ "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="],
+
+ "i18next": ["i18next@23.16.8", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg=="],
+
+ "i18next-browser-languagedetector": ["i18next-browser-languagedetector@7.2.2", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-6b7r75uIJDWCcCflmbof+sJ94k9UQO4X0YR62oUfqGI/GjCLVzlCwu8TFdRZIqVLzWbzNcmkmhfqKEr4TLz4HQ=="],
+
+ "iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
+
+ "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
+
+ "immer": ["immer@10.1.1", "", {}, "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw=="],
+
+ "immutable": ["immutable@5.1.2", "", {}, "sha512-qHKXW1q6liAk1Oys6umoaZbDRqjcjgSrbnrifHsfsttza7zcvRAsL7mMV6xWcyhwQy7Xj5v4hhbr6b+iDYwlmQ=="],
+
+ "import-fresh": ["import-fresh@3.3.0", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw=="],
+
+ "inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="],
+
+ "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
+
+ "inline-style-parser": ["inline-style-parser@0.2.4", "", {}, "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q=="],
+
+ "internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="],
+
+ "intersection-observer": ["intersection-observer@0.12.2", "", {}, "sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg=="],
+
+ "is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="],
+
+ "is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="],
+
+ "is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="],
+
+ "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="],
+
+ "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="],
+
+ "is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="],
+
+ "is-extendable": ["is-extendable@1.0.1", "", { "dependencies": { "is-plain-object": "^2.0.4" } }, "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA=="],
+
+ "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
+
+ "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
+
+ "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
+
+ "is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="],
+
+ "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
+
+ "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
+
+ "is-plain-object": ["is-plain-object@2.0.4", "", { "dependencies": { "isobject": "^3.0.1" } }, "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og=="],
+
+ "isarray": ["isarray@0.0.1", "", {}, "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ=="],
+
+ "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
+
+ "isobject": ["isobject@3.0.1", "", {}, "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg=="],
+
+ "jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
+
+ "jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="],
+
+ "js-cookie": ["js-cookie@3.0.5", "", {}, "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw=="],
+
+ "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
+
+ "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
+
+ "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="],
+
+ "json2mq": ["json2mq@0.2.0", "", { "dependencies": { "string-convert": "^0.2.0" } }, "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA=="],
+
+ "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
+
+ "jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="],
+
+ "jsonfile": ["jsonfile@6.1.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ=="],
+
+ "katex": ["katex@0.16.22", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg=="],
+
+ "khroma": ["khroma@2.1.0", "", {}, "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw=="],
+
+ "kolorist": ["kolorist@1.8.0", "", {}, "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ=="],
+
+ "langium": ["langium@3.3.1", "", { "dependencies": { "chevrotain": "~11.0.3", "chevrotain-allstar": "~0.3.0", "vscode-languageserver": "~9.0.1", "vscode-languageserver-textdocument": "~1.0.11", "vscode-uri": "~3.0.8" } }, "sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w=="],
+
+ "layout-base": ["layout-base@1.0.2", "", {}, "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg=="],
+
+ "leva": ["leva@0.10.0", "", { "dependencies": { "@radix-ui/react-portal": "1.0.2", "@radix-ui/react-tooltip": "1.0.5", "@stitches/react": "^1.2.8", "@use-gesture/react": "^10.2.5", "colord": "^2.9.2", "dequal": "^2.0.2", "merge-value": "^1.0.0", "react-colorful": "^5.5.1", "react-dropzone": "^12.0.0", "v8n": "^1.3.3", "zustand": "^3.6.9" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-RiNJWmeqQdKIeHuVXgshmxIHu144a2AMYtLxKf8Nm1j93pisDPexuQDHKNdQlbo37wdyDQibLjY9JKGIiD7gaw=="],
+
+ "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="],
+
+ "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
+
+ "lit": ["lit@3.3.0", "", { "dependencies": { "@lit/reactive-element": "^2.1.0", "lit-element": "^4.2.0", "lit-html": "^3.3.0" } }, "sha512-DGVsqsOIHBww2DqnuZzW7QsuCdahp50ojuDaBPC7jUDRpYoH0z7kHBBYZewRzer75FwtrkmkKk7iOAwSaWdBmw=="],
+
+ "lit-element": ["lit-element@4.2.0", "", { "dependencies": { "@lit-labs/ssr-dom-shim": "^1.2.0", "@lit/reactive-element": "^2.1.0", "lit-html": "^3.3.0" } }, "sha512-MGrXJVAI5x+Bfth/pU9Kst1iWID6GHDLEzFEnyULB/sFiRLgkd8NPK/PeeXxktA3T6EIIaq8U3KcbTU5XFcP2Q=="],
+
+ "lit-html": ["lit-html@3.3.0", "", { "dependencies": { "@types/trusted-types": "^2.0.2" } }, "sha512-RHoswrFAxY2d8Cf2mm4OZ1DgzCoBKUKSPvA1fhtSELxUERq2aQQ2h05pO9j81gS1o7RIRJ+CePLogfyahwmynw=="],
+
+ "local-pkg": ["local-pkg@1.1.1", "", { "dependencies": { "mlly": "^1.7.4", "pkg-types": "^2.0.1", "quansync": "^0.2.8" } }, "sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg=="],
+
+ "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
+
+ "lodash-es": ["lodash-es@4.17.21", "", {}, "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="],
+
+ "lodash.debounce": ["lodash.debounce@4.0.8", "", {}, "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="],
+
+ "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
+
+ "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
+
+ "lottie-web": ["lottie-web@5.12.2", "", {}, "sha512-uvhvYPC8kGPjXT3MyKMrL3JitEAmDMp30lVkuq/590Mw9ok6pWcFCwXJveo0t5uqYw1UREQHofD+jVpdjBv8wg=="],
+
+ "lowlight": ["lowlight@3.3.0", "", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.0.0", "highlight.js": "~11.11.0" } }, "sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ=="],
+
+ "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
+
+ "lucide-react": ["lucide-react@0.511.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-VK5a2ydJ7xm8GvBeKLS9mu1pVK6ucef9780JVUjw6bAjJL/QXnd4Y0p7SPeOUMC27YhzNCZvm5d/QX0Tp3rc0w=="],
+
+ "markdown-extensions": ["markdown-extensions@2.0.0", "", {}, "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q=="],
+
+ "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="],
+
+ "marked": ["marked@4.3.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A=="],
+
+ "mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="],
+
+ "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA=="],
+
+ "mdast-util-gfm": ["mdast-util-gfm@3.0.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-dgQEX5Amaq+DuUqf26jJqSK9qgixgd6rYDHAv4aTBuA92cTknZlKpPfa86Z/s8Dj8xsAQpFfBmPUHWJBWqS4Bw=="],
+
+ "mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="],
+
+ "mdast-util-gfm-footnote": ["mdast-util-gfm-footnote@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0" } }, "sha512-5jOT2boTSVkMnQ7LTrd6n/18kqwjmuYqo7JUPe+tRCY6O7dAuTFMtTPauYYrMPpox9hlN0uOx/FL8XvEfG9/mQ=="],
+
+ "mdast-util-gfm-strikethrough": ["mdast-util-gfm-strikethrough@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg=="],
+
+ "mdast-util-gfm-table": ["mdast-util-gfm-table@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "markdown-table": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg=="],
+
+ "mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="],
+
+ "mdast-util-math": ["mdast-util-math@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "longest-streak": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.1.0", "unist-util-remove-position": "^5.0.0" } }, "sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w=="],
+
+ "mdast-util-mdx": ["mdast-util-mdx@3.0.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w=="],
+
+ "mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="],
+
+ "mdast-util-mdx-jsx": ["mdast-util-mdx-jsx@3.1.3", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-bfOjvNt+1AcbPLTFMFWY149nJz0OjmewJs3LQQ5pIyVGxP4CdOqNVJL6kTaM5c68p8q82Xv3nCyFfUnuEcH3UQ=="],
+
+ "mdast-util-mdxjs-esm": ["mdast-util-mdxjs-esm@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg=="],
+
+ "mdast-util-newline-to-break": ["mdast-util-newline-to-break@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-find-and-replace": "^3.0.0" } }, "sha512-MbgeFca0hLYIEx/2zGsszCSEJJ1JSCdiY5xQxRcLDDGa8EPvlLPupJ4DSajbMPAnC0je8jfb9TiUATnxxrHUog=="],
+
+ "mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="],
+
+ "mdast-util-to-hast": ["mdast-util-to-hast@13.2.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA=="],
+
+ "mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="],
+
+ "mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="],
+
+ "memoize-one": ["memoize-one@5.2.1", "", {}, "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q=="],
+
+ "merge-value": ["merge-value@1.0.0", "", { "dependencies": { "get-value": "^2.0.6", "is-extendable": "^1.0.0", "mixin-deep": "^1.2.0", "set-value": "^2.0.0" } }, "sha512-fJMmvat4NeKz63Uv9iHWcPDjCWcCkoiRoajRTEO8hlhUC6rwaHg0QCF9hBOTjZmm4JuglPckPSTtcuJL5kp0TQ=="],
+
+ "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
+
+ "mermaid": ["mermaid@11.6.0", "", { "dependencies": { "@braintree/sanitize-url": "^7.0.4", "@iconify/utils": "^2.1.33", "@mermaid-js/parser": "^0.4.0", "@types/d3": "^7.4.3", "cytoscape": "^3.29.3", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.11", "dayjs": "^1.11.13", "dompurify": "^3.2.4", "katex": "^0.16.9", "khroma": "^2.1.0", "lodash-es": "^4.17.21", "marked": "^15.0.7", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", "uuid": "^11.1.0" } }, "sha512-PE8hGUy1LDlWIHWBP05SFdqUHGmRcCcK4IzpOKPE35eOw+G9zZgcnMpyunJVUEOgb//KBORPjysKndw8bFLuRg=="],
+
+ "micromark": ["micromark@4.0.1", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-eBPdkcoCNvYcxQOAKAlceo5SNdzZWfF+FcSupREAzdAh9rRmE239CEQAiTwIgblwnoM8zzj35sZ5ZwvSEOF6Kw=="],
+
+ "micromark-core-commonmark": ["micromark-core-commonmark@2.0.2", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-FKjQKbxd1cibWMM1P9N+H8TwlgGgSkWZMmfuVucLCHaYqeSvJ0hFeHsIa65pA2nYbes0f8LDHPMrd9X7Ujxg9w=="],
+
+ "micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="],
+
+ "micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="],
+
+ "micromark-extension-gfm-footnote": ["micromark-extension-gfm-footnote@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw=="],
+
+ "micromark-extension-gfm-strikethrough": ["micromark-extension-gfm-strikethrough@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw=="],
+
+ "micromark-extension-gfm-table": ["micromark-extension-gfm-table@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ub2ncQv+fwD70/l4ou27b4YzfNaCJOvyX4HxXU15m7mpYY+rjuWzsLIPZHJL253Z643RpbcP1oeIJlQ/SKW67g=="],
+
+ "micromark-extension-gfm-tagfilter": ["micromark-extension-gfm-tagfilter@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg=="],
+
+ "micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="],
+
+ "micromark-extension-math": ["micromark-extension-math@3.1.0", "", { "dependencies": { "@types/katex": "^0.16.0", "devlop": "^1.0.0", "katex": "^0.16.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg=="],
+
+ "micromark-extension-mdx-expression": ["micromark-extension-mdx-expression@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-sI0nwhUDz97xyzqJAbHQhp5TfaxEvZZZ2JDqUo+7NvyIYG6BZ5CPPqj2ogUoPJlmXHBnyZUzISg9+oUmU6tUjQ=="],
+
+ "micromark-extension-mdx-jsx": ["micromark-extension-mdx-jsx@3.0.1", "", { "dependencies": { "@types/acorn": "^4.0.0", "@types/estree": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-vNuFb9czP8QCtAQcEJn0UJQJZA8Dk6DXKBqx+bg/w0WGuSxDxNr7hErW89tHUY31dUW4NqEOWwmEUNhjTFmHkg=="],
+
+ "micromark-extension-mdx-md": ["micromark-extension-mdx-md@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ=="],
+
+ "micromark-extension-mdxjs": ["micromark-extension-mdxjs@3.0.0", "", { "dependencies": { "acorn": "^8.0.0", "acorn-jsx": "^5.0.0", "micromark-extension-mdx-expression": "^3.0.0", "micromark-extension-mdx-jsx": "^3.0.0", "micromark-extension-mdx-md": "^2.0.0", "micromark-extension-mdxjs-esm": "^3.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ=="],
+
+ "micromark-extension-mdxjs-esm": ["micromark-extension-mdxjs-esm@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-position-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A=="],
+
+ "micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="],
+
+ "micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="],
+
+ "micromark-factory-mdx-expression": ["micromark-factory-mdx-expression@2.0.2", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-position-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-5E5I2pFzJyg2CtemqAbcyCktpHXuJbABnsb32wX2U8IQKhhVFBqkcZR5LRm1WVoFqa4kTueZK4abep7wdo9nrw=="],
+
+ "micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="],
+
+ "micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="],
+
+ "micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="],
+
+ "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="],
+
+ "micromark-util-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="],
+
+ "micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="],
+
+ "micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="],
+
+ "micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="],
+
+ "micromark-util-decode-string": ["micromark-util-decode-string@2.0.1", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ=="],
+
+ "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="],
+
+ "micromark-util-events-to-acorn": ["micromark-util-events-to-acorn@2.0.2", "", { "dependencies": { "@types/acorn": "^4.0.0", "@types/estree": "^1.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-Fk+xmBrOv9QZnEDguL9OI9/NQQp6Hz4FuQ4YmCb/5V7+9eAh1s6AYSvL20kHkD67YIg7EpE54TiSlcsf3vyZgA=="],
+
+ "micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="],
+
+ "micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="],
+
+ "micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="],
+
+ "micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="],
+
+ "micromark-util-subtokenize": ["micromark-util-subtokenize@2.0.3", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VXJJuNxYWSoYL6AJ6OQECCFGhIU2GGHMw8tahogePBrjkG8aCCas3ibkp7RnVOSTClg2is05/R7maAhF1XyQMg=="],
+
+ "micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="],
+
+ "micromark-util-types": ["micromark-util-types@2.0.1", "", {}, "sha512-534m2WhVTddrcKVepwmVEVnUAmtrx9bfIjNoQHRqfnvdaHQiFytEhJoTgpWJvDEXCO5gLTQh3wYC1PgOJA4NSQ=="],
+
+ "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
+
+ "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
+
+ "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
+
+ "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
+
+ "minimist": ["minimist@1.2.6", "", {}, "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="],
+
+ "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
+
+ "mixin-deep": ["mixin-deep@1.3.2", "", { "dependencies": { "for-in": "^1.0.2", "is-extendable": "^1.0.1" } }, "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA=="],
+
+ "mlly": ["mlly@1.7.4", "", { "dependencies": { "acorn": "^8.14.0", "pathe": "^2.0.1", "pkg-types": "^1.3.0", "ufo": "^1.5.4" } }, "sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw=="],
+
+ "motion-dom": ["motion-dom@12.12.1", "", { "dependencies": { "motion-utils": "^12.12.1" } }, "sha512-GXq/uUbZBEiFFE+K1Z/sxdPdadMdfJ/jmBALDfIuHGi0NmtealLOfH9FqT+6aNPgVx8ilq0DtYmyQlo6Uj9LKQ=="],
+
+ "motion-utils": ["motion-utils@12.12.1", "", {}, "sha512-f9qiqUHm7hWSLlNW8gS9pisnsN7CRFRD58vNjptKdsqFLpkVnX00TNeD6Q0d27V9KzT7ySFyK1TZ/DShfVOv6w=="],
+
+ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
+
+ "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="],
+
+ "nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="],
+
+ "node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="],
+
+ "node-releases": ["node-releases@2.0.19", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="],
+
+ "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
+
+ "normalize-range": ["normalize-range@0.1.2", "", {}, "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA=="],
+
+ "numeral": ["numeral@2.0.6", "", {}, "sha512-qaKRmtYPZ5qdw4jWJD6bxEf1FJEqllJrwxCLIm0sQU/A7v2/czigzOb+C2uSiFsa9lBUzeH7M1oK+Q+OLxL3kA=="],
+
+ "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
+
+ "object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="],
+
+ "on-change": ["on-change@4.0.2", "", {}, "sha512-cMtCyuJmTx/bg2HCpHo3ZLeF7FZnBOapLqZHr2AlLeJ5Ul0Zu2mUJJz051Fdwu/Et2YW04ZD+TtU+gVy0ACNCA=="],
+
+ "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
+
+ "oniguruma-parser": ["oniguruma-parser@0.12.1", "", {}, "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w=="],
+
+ "oniguruma-to-es": ["oniguruma-to-es@4.3.3", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-rPiZhzC3wXwE59YQMRDodUwwT9FZ9nNBwQQfsd1wfdtlKEyCdRV0avrTcSZ5xlIvGRVPd/cx6ZN45ECmS39xvg=="],
+
+ "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="],
+
+ "package-manager-detector": ["package-manager-detector@1.3.0", "", {}, "sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ=="],
+
+ "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
+
+ "parse-author": ["parse-author@2.0.0", "", { "dependencies": { "author-regex": "^1.0.0" } }, "sha512-yx5DfvkN8JsHL2xk2Os9oTia467qnvRgey4ahSm2X8epehBLx/gWLcy5KI+Y36ful5DzGbCS6RazqZGgy1gHNw=="],
+
+ "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="],
+
+ "parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="],
+
+ "parse-svg-path": ["parse-svg-path@0.1.2", "", {}, "sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ=="],
+
+ "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
+
+ "path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="],
+
+ "path-data-parser": ["path-data-parser@0.1.0", "", {}, "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w=="],
+
+ "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="],
+
+ "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
+
+ "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
+
+ "path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
+
+ "path-source": ["path-source@0.1.3", "", { "dependencies": { "array-source": "0.0", "file-source": "0.6" } }, "sha512-dWRHm5mIw5kw0cs3QZLNmpUWty48f5+5v9nWD2dw3Y0Hf+s01Ag8iJEWV0Sm0kocE8kK27DrIowha03e1YR+Qw=="],
+
+ "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="],
+
+ "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
+
+ "pbf": ["pbf@3.3.0", "", { "dependencies": { "ieee754": "^1.1.12", "resolve-protobuf-schema": "^2.1.0" }, "bin": { "pbf": "bin/pbf" } }, "sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q=="],
+
+ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
+
+ "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
+
+ "pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="],
+
+ "pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="],
+
+ "pkg-types": ["pkg-types@2.1.0", "", { "dependencies": { "confbox": "^0.2.1", "exsolve": "^1.0.1", "pathe": "^2.0.3" } }, "sha512-wmJwA+8ihJixSoHKxZJRBQG1oY8Yr9pGLzRmSsNms0iNWyHHAlZCa7mmKiFR10YPZuz/2k169JiS/inOjBCZ2A=="],
+
+ "point-at-length": ["point-at-length@1.1.0", "", { "dependencies": { "abs-svg-path": "~0.1.1", "isarray": "~0.0.1", "parse-svg-path": "~0.1.1" } }, "sha512-nNHDk9rNEh/91o2Y8kHLzBLNpLf80RYd2gCun9ss+V0ytRSf6XhryBTx071fesktjbachRmGuUbId+JQmzhRXw=="],
+
+ "points-on-curve": ["points-on-curve@0.2.0", "", {}, "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A=="],
+
+ "points-on-path": ["points-on-path@0.2.1", "", { "dependencies": { "path-data-parser": "0.1.0", "points-on-curve": "0.2.0" } }, "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g=="],
+
+ "polished": ["polished@4.3.1", "", { "dependencies": { "@babel/runtime": "^7.17.8" } }, "sha512-OBatVyC/N7SCW/FaDHrSd+vn0o5cS855TOmYi4OkdWUMSJCET/xip//ch8xGUvtr3i44X9LVyWwQlRMTN3pwSA=="],
+
+ "postcss": ["postcss@8.5.3", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A=="],
+
+ "postcss-import": ["postcss-import@15.1.0", "", { "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", "resolve": "^1.1.7" }, "peerDependencies": { "postcss": "^8.0.0" } }, "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew=="],
+
+ "postcss-js": ["postcss-js@4.0.1", "", { "dependencies": { "camelcase-css": "^2.0.1" }, "peerDependencies": { "postcss": "^8.4.21" } }, "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw=="],
+
+ "postcss-load-config": ["postcss-load-config@4.0.2", "", { "dependencies": { "lilconfig": "^3.0.0", "yaml": "^2.3.4" }, "peerDependencies": { "postcss": ">=8.0.9", "ts-node": ">=9.0.0" }, "optionalPeers": ["postcss", "ts-node"] }, "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ=="],
+
+ "postcss-nested": ["postcss-nested@6.2.0", "", { "dependencies": { "postcss-selector-parser": "^6.1.1" }, "peerDependencies": { "postcss": "^8.2.14" } }, "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ=="],
+
+ "postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="],
+
+ "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
+
+ "prettier": ["prettier@3.4.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ=="],
+
+ "prettier-package-json": ["prettier-package-json@2.8.0", "", { "dependencies": { "@types/parse-author": "^2.0.0", "commander": "^4.0.1", "cosmiconfig": "^7.0.0", "fs-extra": "^10.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4", "parse-author": "^2.0.0", "sort-object-keys": "^1.1.3", "sort-order": "^1.0.1" }, "bin": { "prettier-package-json": "bin/prettier-package-json" } }, "sha512-WxtodH/wWavfw3MR7yK/GrS4pASEQ+iSTkdtSxPJWvqzG55ir5nvbLt9rw5AOiEcqqPCRM92WCtR1rk3TG3JSQ=="],
+
+ "prettier-plugin-astro": ["prettier-plugin-astro@0.14.1", "", { "dependencies": { "@astrojs/compiler": "^2.9.1", "prettier": "^3.0.0", "sass-formatter": "^0.7.6" } }, "sha512-RiBETaaP9veVstE4vUwSIcdATj6dKmXljouXc/DDNwBSPTp8FRkLGDSGFClKsAFeeg+13SB0Z1JZvbD76bigJw=="],
+
+ "prettier-plugin-curly-and-jsdoc": ["prettier-plugin-curly-and-jsdoc@3.1.0", "", { "peerDependencies": { "prettier": "^3.0.0" } }, "sha512-4QMOHnLlkP2jTRWS0MFH6j+cuOiXLvXOqCLKbtwwVd8PPyq8NenW5AAwfwqiTNHBQG/DmzViPphRrwgN0XkUVQ=="],
+
+ "prettier-plugin-pkgsort": ["prettier-plugin-pkgsort@0.2.1", "", { "dependencies": { "prettier-package-json": "^2.8.0" }, "peerDependencies": { "prettier": "^3.0.0" } }, "sha512-/k5MIw84EhgoH7dmq4+6ozHjJ0VYbxbw17g4C+WPGHODkLivGwJoA6U1YPR/KObyRDMQJHXAfXKu++9smg7Jyw=="],
+
+ "prismjs": ["prismjs@1.29.0", "", {}, "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q=="],
+
+ "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
+
+ "property-information": ["property-information@6.5.0", "", {}, "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig=="],
+
+ "protocol-buffers-schema": ["protocol-buffers-schema@3.6.0", "", {}, "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw=="],
+
+ "quansync": ["quansync@0.2.10", "", {}, "sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A=="],
+
+ "query-string": ["query-string@9.2.0", "", { "dependencies": { "decode-uri-component": "^0.4.1", "filter-obj": "^5.1.0", "split-on-first": "^3.0.0" } }, "sha512-YIRhrHujoQxhexwRLxfy3VSjOXmvZRd2nyw1PwL1UUqZ/ys1dEZd1+NSgXkne2l/4X/7OXkigEAuhTX0g/ivJQ=="],
+
+ "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
+
+ "rc-cascader": ["rc-cascader@3.34.0", "", { "dependencies": { "@babel/runtime": "^7.25.7", "classnames": "^2.3.1", "rc-select": "~14.16.2", "rc-tree": "~5.13.0", "rc-util": "^5.43.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-KpXypcvju9ptjW9FaN2NFcA2QH9E9LHKq169Y0eWtH4e/wHQ5Wh5qZakAgvb8EKZ736WZ3B0zLLOBsrsja5Dag=="],
+
+ "rc-checkbox": ["rc-checkbox@3.5.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "^2.3.2", "rc-util": "^5.25.2" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-aOAQc3E98HteIIsSqm6Xk2FPKIER6+5vyEFMZfo73TqM+VVAIqOkHoPjgKLqSNtVLWScoaM7vY2ZrGEheI79yg=="],
+
+ "rc-collapse": ["rc-collapse@3.9.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", "rc-motion": "^2.3.4", "rc-util": "^5.27.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-swDdz4QZ4dFTo4RAUMLL50qP0EY62N2kvmk2We5xYdRwcRn8WcYtuetCJpwpaCbUfUt5+huLpVxhvmnK+PHrkA=="],
+
+ "rc-dialog": ["rc-dialog@9.6.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/portal": "^1.0.0-8", "classnames": "^2.2.6", "rc-motion": "^2.3.0", "rc-util": "^5.21.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-ApoVi9Z8PaCQg6FsUzS8yvBEQy0ZL2PkuvAgrmohPkN3okps5WZ5WQWPc1RNuiOKaAYv8B97ACdsFU5LizzCqg=="],
+
+ "rc-drawer": ["rc-drawer@7.2.0", "", { "dependencies": { "@babel/runtime": "^7.23.9", "@rc-component/portal": "^1.1.1", "classnames": "^2.2.6", "rc-motion": "^2.6.1", "rc-util": "^5.38.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-9lOQ7kBekEJRdEpScHvtmEtXnAsy+NGDXiRWc2ZVC7QXAazNVbeT4EraQKYwCME8BJLa8Bxqxvs5swwyOepRwg=="],
+
+ "rc-dropdown": ["rc-dropdown@4.2.1", "", { "dependencies": { "@babel/runtime": "^7.18.3", "@rc-component/trigger": "^2.0.0", "classnames": "^2.2.6", "rc-util": "^5.44.1" }, "peerDependencies": { "react": ">=16.11.0", "react-dom": ">=16.11.0" } }, "sha512-YDAlXsPv3I1n42dv1JpdM7wJ+gSUBfeyPK59ZpBD9jQhK9jVuxpjj3NmWQHOBceA1zEPVX84T2wbdb2SD0UjmA=="],
+
+ "rc-field-form": ["rc-field-form@2.7.0", "", { "dependencies": { "@babel/runtime": "^7.18.0", "@rc-component/async-validator": "^5.0.3", "rc-util": "^5.32.2" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-hgKsCay2taxzVnBPZl+1n4ZondsV78G++XVsMIJCAoioMjlMQR9YwAp7JZDIECzIu2Z66R+f4SFIRrO2DjDNAA=="],
+
+ "rc-footer": ["rc-footer@0.6.8", "", { "dependencies": { "@babel/runtime": "^7.11.1", "classnames": "^2.2.1" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-JBZ+xcb6kkex8XnBd4VHw1ZxjV6kmcwUumSHaIFdka2qzMCo7Klcy4sI6G0XtUpG/vtpislQCc+S9Bc+NLHYMg=="],
+
+ "rc-image": ["rc-image@7.12.0", "", { "dependencies": { "@babel/runtime": "^7.11.2", "@rc-component/portal": "^1.0.2", "classnames": "^2.2.6", "rc-dialog": "~9.6.0", "rc-motion": "^2.6.2", "rc-util": "^5.34.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-cZ3HTyyckPnNnUb9/DRqduqzLfrQRyi+CdHjdqgsyDpI3Ln5UX1kXnAhPBSJj9pVRzwRFgqkN7p9b6HBDjmu/Q=="],
+
+ "rc-input": ["rc-input@1.8.0", "", { "dependencies": { "@babel/runtime": "^7.11.1", "classnames": "^2.2.1", "rc-util": "^5.18.1" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-KXvaTbX+7ha8a/k+eg6SYRVERK0NddX8QX7a7AnRvUa/rEH0CNMlpcBzBkhI0wp2C8C4HlMoYl8TImSN+fuHKA=="],
+
+ "rc-input-number": ["rc-input-number@9.5.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/mini-decimal": "^1.0.1", "classnames": "^2.2.5", "rc-input": "~1.8.0", "rc-util": "^5.40.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-bKaEvB5tHebUURAEXw35LDcnRZLq3x1k7GxfAqBMzmpHkDGzjAtnUL8y4y5N15rIFIg5IJgwr211jInl3cipag=="],
+
+ "rc-mentions": ["rc-mentions@2.20.0", "", { "dependencies": { "@babel/runtime": "^7.22.5", "@rc-component/trigger": "^2.0.0", "classnames": "^2.2.6", "rc-input": "~1.8.0", "rc-menu": "~9.16.0", "rc-textarea": "~1.10.0", "rc-util": "^5.34.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-w8HCMZEh3f0nR8ZEd466ATqmXFCMGMN5UFCzEUL0bM/nGw/wOS2GgRzKBcm19K++jDyuWCOJOdgcKGXU3fXfbQ=="],
+
+ "rc-menu": ["rc-menu@9.16.1", "", { "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/trigger": "^2.0.0", "classnames": "2.x", "rc-motion": "^2.4.3", "rc-overflow": "^1.3.1", "rc-util": "^5.27.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-ghHx6/6Dvp+fw8CJhDUHFHDJ84hJE3BXNCzSgLdmNiFErWSOaZNsihDAsKq9ByTALo/xkNIwtDFGIl6r+RPXBg=="],
+
+ "rc-motion": ["rc-motion@2.9.5", "", { "dependencies": { "@babel/runtime": "^7.11.1", "classnames": "^2.2.1", "rc-util": "^5.44.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-w+XTUrfh7ArbYEd2582uDrEhmBHwK1ZENJiSJVb7uRxdE7qJSYjbO2eksRXmndqyKqKoYPc9ClpPh5242mV1vA=="],
+
+ "rc-notification": ["rc-notification@5.6.4", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", "rc-motion": "^2.9.0", "rc-util": "^5.20.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-KcS4O6B4qzM3KH7lkwOB7ooLPZ4b6J+VMmQgT51VZCeEcmghdeR4IrMcFq0LG+RPdnbe/ArT086tGM8Snimgiw=="],
+
+ "rc-overflow": ["rc-overflow@1.4.1", "", { "dependencies": { "@babel/runtime": "^7.11.1", "classnames": "^2.2.1", "rc-resize-observer": "^1.0.0", "rc-util": "^5.37.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-3MoPQQPV1uKyOMVNd6SZfONi+f3st0r8PksexIdBTeIYbMX0Jr+k7pHEDvsXtR4BpCv90/Pv2MovVNhktKrwvw=="],
+
+ "rc-pagination": ["rc-pagination@5.1.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "^2.3.2", "rc-util": "^5.38.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-8416Yip/+eclTFdHXLKTxZvn70duYVGTvUUWbckCCZoIl3jagqke3GLsFrMs0bsQBikiYpZLD9206Ej4SOdOXQ=="],
+
+ "rc-picker": ["rc-picker@4.11.3", "", { "dependencies": { "@babel/runtime": "^7.24.7", "@rc-component/trigger": "^2.0.0", "classnames": "^2.2.1", "rc-overflow": "^1.3.2", "rc-resize-observer": "^1.4.0", "rc-util": "^5.43.0" }, "peerDependencies": { "date-fns": ">= 2.x", "dayjs": ">= 1.x", "luxon": ">= 3.x", "moment": ">= 2.x", "react": ">=16.9.0", "react-dom": ">=16.9.0" }, "optionalPeers": ["date-fns", "dayjs", "luxon", "moment"] }, "sha512-MJ5teb7FlNE0NFHTncxXQ62Y5lytq6sh5nUw0iH8OkHL/TjARSEvSHpr940pWgjGANpjCwyMdvsEV55l5tYNSg=="],
+
+ "rc-progress": ["rc-progress@4.0.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "^2.2.6", "rc-util": "^5.16.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-oofVMMafOCokIUIBnZLNcOZFsABaUw8PPrf1/y0ZBvKZNpOiu5h4AO9vv11Sw0p4Hb3D0yGWuEattcQGtNJ/aw=="],
+
+ "rc-rate": ["rc-rate@2.13.1", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "^2.2.5", "rc-util": "^5.0.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-QUhQ9ivQ8Gy7mtMZPAjLbxBt5y9GRp65VcUyGUMF3N3fhiftivPHdpuDIaWIMOTEprAjZPC08bls1dQB+I1F2Q=="],
+
+ "rc-resize-observer": ["rc-resize-observer@1.4.3", "", { "dependencies": { "@babel/runtime": "^7.20.7", "classnames": "^2.2.1", "rc-util": "^5.44.1", "resize-observer-polyfill": "^1.5.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-YZLjUbyIWox8E9i9C3Tm7ia+W7euPItNWSPX5sCcQTYbnwDb5uNpnLHQCG1f22oZWUhLw4Mv2tFmeWe68CDQRQ=="],
+
+ "rc-segmented": ["rc-segmented@2.7.0", "", { "dependencies": { "@babel/runtime": "^7.11.1", "classnames": "^2.2.1", "rc-motion": "^2.4.4", "rc-util": "^5.17.0" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-liijAjXz+KnTRVnxxXG2sYDGd6iLL7VpGGdR8gwoxAXy2KglviKCxLWZdjKYJzYzGSUwKDSTdYk8brj54Bn5BA=="],
+
+ "rc-select": ["rc-select@14.16.8", "", { "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/trigger": "^2.1.1", "classnames": "2.x", "rc-motion": "^2.0.1", "rc-overflow": "^1.3.1", "rc-util": "^5.16.1", "rc-virtual-list": "^3.5.2" }, "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-NOV5BZa1wZrsdkKaiK7LHRuo5ZjZYMDxPP6/1+09+FB4KoNi8jcG1ZqLE3AVCxEsYMBe65OBx71wFoHRTP3LRg=="],
+
+ "rc-slider": ["rc-slider@11.1.8", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "^2.2.5", "rc-util": "^5.36.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-2gg/72YFSpKP+Ja5AjC5DPL1YnV8DEITDQrcc1eASrUYjl0esptaBVJBh5nLTXCCp15eD8EuGjwezVGSHhs9tQ=="],
+
+ "rc-steps": ["rc-steps@6.0.1", "", { "dependencies": { "@babel/runtime": "^7.16.7", "classnames": "^2.2.3", "rc-util": "^5.16.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-lKHL+Sny0SeHkQKKDJlAjV5oZ8DwCdS2hFhAkIjuQt1/pB81M0cA0ErVFdHq9+jmPmFw1vJB2F5NBzFXLJxV+g=="],
+
+ "rc-switch": ["rc-switch@4.1.0", "", { "dependencies": { "@babel/runtime": "^7.21.0", "classnames": "^2.2.1", "rc-util": "^5.30.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-TI8ufP2Az9oEbvyCeVE4+90PDSljGyuwix3fV58p7HV2o4wBnVToEyomJRVyTaZeqNPAp+vqeo4Wnj5u0ZZQBg=="],
+
+ "rc-table": ["rc-table@7.50.5", "", { "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/context": "^1.4.0", "classnames": "^2.2.5", "rc-resize-observer": "^1.1.0", "rc-util": "^5.44.3", "rc-virtual-list": "^3.14.2" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-FDZu8aolhSYd3v9KOc3lZOVAU77wmRRu44R0Wfb8Oj1dXRUsloFaXMSl6f7yuWZUxArJTli7k8TEOX2mvhDl4A=="],
+
+ "rc-tabs": ["rc-tabs@15.6.1", "", { "dependencies": { "@babel/runtime": "^7.11.2", "classnames": "2.x", "rc-dropdown": "~4.2.0", "rc-menu": "~9.16.0", "rc-motion": "^2.6.2", "rc-resize-observer": "^1.0.0", "rc-util": "^5.34.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-/HzDV1VqOsUWyuC0c6AkxVYFjvx9+rFPKZ32ejxX0Uc7QCzcEjTA9/xMgv4HemPKwzBNX8KhGVbbumDjnj92aA=="],
+
+ "rc-textarea": ["rc-textarea@1.10.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "^2.2.1", "rc-input": "~1.8.0", "rc-resize-observer": "^1.0.0", "rc-util": "^5.27.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-ai9IkanNuyBS4x6sOL8qu/Ld40e6cEs6pgk93R+XLYg0mDSjNBGey6/ZpDs5+gNLD7urQ14po3V6Ck2dJLt9SA=="],
+
+ "rc-tooltip": ["rc-tooltip@6.4.0", "", { "dependencies": { "@babel/runtime": "^7.11.2", "@rc-component/trigger": "^2.0.0", "classnames": "^2.3.1", "rc-util": "^5.44.3" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-kqyivim5cp8I5RkHmpsp1Nn/Wk+1oeloMv9c7LXNgDxUpGm+RbXJGL+OPvDlcRnx9DBeOe4wyOIl4OKUERyH1g=="],
+
+ "rc-tree": ["rc-tree@5.13.1", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", "rc-motion": "^2.0.1", "rc-util": "^5.16.1", "rc-virtual-list": "^3.5.1" }, "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-FNhIefhftobCdUJshO7M8uZTA9F4OPGVXqGfZkkD/5soDeOhwO06T/aKTrg0WD8gRg/pyfq+ql3aMymLHCTC4A=="],
+
+ "rc-tree-select": ["rc-tree-select@5.27.0", "", { "dependencies": { "@babel/runtime": "^7.25.7", "classnames": "2.x", "rc-select": "~14.16.2", "rc-tree": "~5.13.0", "rc-util": "^5.43.0" }, "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-2qTBTzwIT7LRI1o7zLyrCzmo5tQanmyGbSaGTIf7sYimCklAToVVfpMC6OAldSKolcnjorBYPNSKQqJmN3TCww=="],
+
+ "rc-upload": ["rc-upload@4.9.0", "", { "dependencies": { "@babel/runtime": "^7.18.3", "classnames": "^2.2.5", "rc-util": "^5.2.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-pAzlPnyiFn1GCtEybEG2m9nXNzQyWXqWV2xFYCmDxjN9HzyjS5Pz2F+pbNdYw8mMJsixLEKLG0wVy9vOGxJMJA=="],
+
+ "rc-util": ["rc-util@5.44.4", "", { "dependencies": { "@babel/runtime": "^7.18.3", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-resueRJzmHG9Q6rI/DfK6Kdv9/Lfls05vzMs1Sk3M2P+3cJa+MakaZyWY8IPfehVuhPJFKrIY1IK4GqbiaiY5w=="],
+
+ "rc-virtual-list": ["rc-virtual-list@3.18.6", "", { "dependencies": { "@babel/runtime": "^7.20.0", "classnames": "^2.2.6", "rc-resize-observer": "^1.0.0", "rc-util": "^5.36.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-TQ5SsutL3McvWmmxqQtMIbfeoE3dGjJrRSfKekgby7WQMpPIFvv4ghytp5Z0s3D8Nik9i9YNOCqHBfk86AwgAA=="],
+
+ "re-resizable": ["re-resizable@6.11.2", "", { "peerDependencies": { "react": "^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-2xI2P3OHs5qw7K0Ud1aLILK6MQxW50TcO+DetD9eIV58j84TqYeHoZcL9H4GXFXXIh7afhH8mv5iUCXII7OW7A=="],
+
+ "react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="],
+
+ "react-avatar-editor": ["react-avatar-editor@13.0.2", "", { "dependencies": { "@babel/plugin-transform-runtime": "^7.12.1", "@babel/runtime": "^7.12.5", "prop-types": "^15.7.2" }, "peerDependencies": { "react": "^0.14.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", "react-dom": "^0.14.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" } }, "sha512-a4ajbi7lwDh98kgEtSEeKMu0vs0CHTczkq4Xcxr1EiwMFH1GlgHCEtwGU8q/H5W8SeLnH4KPK8LUjEEaZXklxQ=="],
+
+ "react-colorful": ["react-colorful@5.6.1", "", { "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw=="],
+
+ "react-dom": ["react-dom@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" }, "peerDependencies": { "react": "^18.3.1" } }, "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw=="],
+
+ "react-draggable": ["react-draggable@4.4.6", "", { "dependencies": { "clsx": "^1.1.1", "prop-types": "^15.8.1" }, "peerDependencies": { "react": ">= 16.3.0", "react-dom": ">= 16.3.0" } }, "sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw=="],
+
+ "react-dropzone": ["react-dropzone@14.3.5", "", { "dependencies": { "attr-accept": "^2.2.4", "file-selector": "^2.1.0", "prop-types": "^15.8.1" }, "peerDependencies": { "react": ">= 16.8 || 18.0.0" } }, "sha512-9nDUaEEpqZLOz5v5SUcFA0CjM4vq8YbqO0WRls+EYT7+DvxUdzDPKNCPLqGfj3YL9MsniCLCD4RFA6M95V6KMQ=="],
+
+ "react-error-boundary": ["react-error-boundary@5.0.0", "", { "dependencies": { "@babel/runtime": "^7.12.5" }, "peerDependencies": { "react": ">=16.13.1" } }, "sha512-tnjAxG+IkpLephNcePNA7v6F/QpWLH8He65+DmedchDwg162JZqx4NmbXj0mlAYVVEd81OW7aFhmbsScYfiAFQ=="],
+
+ "react-fast-compare": ["react-fast-compare@3.2.2", "", {}, "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ=="],
+
+ "react-fireworks": ["react-fireworks@1.0.4", "", {}, "sha512-jj1a+HTicB4pR6g2lqhVyAox0GTE0TOrZK2XaJFRYOwltgQWeYErZxnvU9+zH/blY+Hpmu9IKyb39OD3KcCMJw=="],
+
+ "react-hotkeys-hook": ["react-hotkeys-hook@5.1.0", "", { "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-GCNGXjBzV9buOS3REoQFmSmE4WTvBhYQ0YrAeeMZI83bhXg3dRWsLHXDutcVDdEjwJqJCxk5iewWYX5LtFUd7g=="],
+
+ "react-i18next": ["react-i18next@13.5.0", "", { "dependencies": { "@babel/runtime": "^7.22.5", "html-parse-stringify": "^3.0.1" }, "peerDependencies": { "i18next": ">= 23.2.3", "react": ">= 16.8.0" } }, "sha512-CFJ5NDGJ2MUyBohEHxljOq/39NQ972rh1ajnadG9BjTk+UXbHLq4z5DKEbEQBDoIhUmmbuS/fIMJKo6VOax1HA=="],
+
+ "react-icons": ["react-icons@5.5.0", "", { "peerDependencies": { "react": "*" } }, "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw=="],
+
+ "react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
+
+ "react-layout-kit": ["react-layout-kit@1.9.1", "", { "dependencies": { "@babel/runtime": "^7", "@emotion/css": "^11" }, "peerDependencies": { "react": ">=18" } }, "sha512-tQO5J+Ajppu2JCdhgFaFbWCg01WJXXaQ5vg8cxzsv8vVeogJKGFgoJm9OI2saDFchfKP3RABd+aRY5vB++poqw=="],
+
+ "react-markdown": ["react-markdown@10.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ=="],
+
+ "react-merge-refs": ["react-merge-refs@3.0.2", "", { "peerDependencies": { "react": ">=16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["react"] }, "sha512-MSZAfwFfdbEvwkKWP5EI5chuLYnNUxNS7vyS0i1Jp+wtd8J4Ga2ddzhaE68aMol2Z4vCnRM/oGOo1a3V75UPlw=="],
+
+ "react-refresh": ["react-refresh@0.14.2", "", {}, "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA=="],
+
+ "react-resizable": ["react-resizable@3.0.5", "", { "dependencies": { "prop-types": "15.x", "react-draggable": "^4.0.3" }, "peerDependencies": { "react": ">= 16.3" } }, "sha512-vKpeHhI5OZvYn82kXOs1bC8aOXktGU5AmKAgaZS4F5JPburCtbmDPqE7Pzp+1kN4+Wb81LlF33VpGwWwtXem+w=="],
+
+ "react-rnd": ["react-rnd@10.5.2", "", { "dependencies": { "re-resizable": "6.11.2", "react-draggable": "4.4.6", "tslib": "2.6.2" }, "peerDependencies": { "react": ">=16.3.0", "react-dom": ">=16.3.0" } }, "sha512-0Tm4x7k7pfHf2snewJA8x7Nwgt3LV+58MVEWOVsFjk51eYruFEa6Wy7BNdxt4/lH0wIRsu7Gm3KjSXY2w7YaNw=="],
+
+ "react-router": ["react-router@6.28.1", "", { "dependencies": { "@remix-run/router": "1.21.0" }, "peerDependencies": { "react": ">=16.8" } }, "sha512-2omQTA3rkMljmrvvo6WtewGdVh45SpL9hGiCI9uUrwGGfNFDIvGK4gYJsKlJoNVi6AQZcopSCballL+QGOm7fA=="],
+
+ "react-router-dom": ["react-router-dom@6.28.1", "", { "dependencies": { "@remix-run/router": "1.21.0", "react-router": "6.28.1" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-YraE27C/RdjcZwl5UCqF/ffXnZDxpJdk9Q6jw38SZHjXs7NNdpViq2l2c7fO7+4uWaEfcwfGCv3RSg4e1By/fQ=="],
+
+ "react-telegram-login": ["react-telegram-login@1.1.2", "", { "dependencies": { "react": "^16.13.1" } }, "sha512-pDP+bvfaklWgnK5O6yvZnIwgky0nnYUU6Zhk0EjdMSkPsLQoOzZRsXIoZnbxyBXhi7346bsxMH+EwwJPTxClDw=="],
+
+ "react-toastify": ["react-toastify@9.1.3", "", { "dependencies": { "clsx": "^1.1.1" }, "peerDependencies": { "react": ">=16", "react-dom": ">=16" } }, "sha512-fPfb8ghtn/XMxw3LkxQBk3IyagNpF/LIKjOBflbexr2AWxAH1MJgvnESwEwBn9liLFXgTKWgBSdZpw9m4OTHTg=="],
+
+ "react-turnstile": ["react-turnstile@1.1.4", "", { "peerDependencies": { "react": ">= 16.13.1", "react-dom": ">= 16.13.1" } }, "sha512-oluyRWADdsufCt5eMqacW4gfw8/csr6Tk+fmuaMx0PWMKP1SX1iCviLvD2D5w92eAzIYDHi/krUWGHhlfzxTpQ=="],
+
+ "react-window": ["react-window@1.8.11", "", { "dependencies": { "@babel/runtime": "^7.0.0", "memoize-one": ">=3.1.1 <6" }, "peerDependencies": { "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-+SRbUVT2scadgFSWx+R1P754xHPEqvcfSfVX10QYg6POOz+WNgkN48pS+BtZNIMGiL1HYrSEiCkwsMS15QogEQ=="],
+
+ "react-zoom-pan-pinch": ["react-zoom-pan-pinch@3.7.0", "", { "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-UmReVZ0TxlKzxSbYiAj+LeGRW8s8LraAFTXRAxzMYnNRgGPsxCudwZKVkjvGmjtx7SW/hZamt69NUmGf4xrkXA=="],
+
+ "read-cache": ["read-cache@1.0.0", "", { "dependencies": { "pify": "^2.3.0" } }, "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="],
+
+ "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
+
+ "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
+
+ "recma-build-jsx": ["recma-build-jsx@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-build-jsx": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew=="],
+
+ "recma-jsx": ["recma-jsx@1.0.0", "", { "dependencies": { "acorn-jsx": "^5.0.0", "estree-util-to-js": "^2.0.0", "recma-parse": "^1.0.0", "recma-stringify": "^1.0.0", "unified": "^11.0.0" } }, "sha512-5vwkv65qWwYxg+Atz95acp8DMu1JDSqdGkA2Of1j6rCreyFUE/gp15fC8MnGEuG1W68UKjM6x6+YTWIh7hZM/Q=="],
+
+ "recma-parse": ["recma-parse@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "esast-util-from-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ=="],
+
+ "recma-stringify": ["recma-stringify@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-to-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g=="],
+
+ "regenerator-runtime": ["regenerator-runtime@0.14.1", "", {}, "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="],
+
+ "regex": ["regex@6.0.1", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA=="],
+
+ "regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="],
+
+ "regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="],
+
+ "rehype-highlight": ["rehype-highlight@7.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-to-text": "^4.0.0", "lowlight": "^3.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-k158pK7wdC2qL3M5NcZROZ2tR/l7zOzjxXd5VGdcfIyoijjQqpHd3JKtYSBDpDZ38UI2WJWuFAtkMDxmx5kstA=="],
+
+ "rehype-katex": ["rehype-katex@7.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/katex": "^0.16.0", "hast-util-from-html-isomorphic": "^2.0.0", "hast-util-to-text": "^4.0.0", "katex": "^0.16.0", "unist-util-visit-parents": "^6.0.0", "vfile": "^6.0.0" } }, "sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA=="],
+
+ "rehype-raw": ["rehype-raw@7.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-raw": "^9.0.0", "vfile": "^6.0.0" } }, "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww=="],
+
+ "rehype-recma": ["rehype-recma@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "hast-util-to-estree": "^3.0.0" } }, "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw=="],
+
+ "remark-breaks": ["remark-breaks@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-newline-to-break": "^2.0.0", "unified": "^11.0.0" } }, "sha512-IjEjJOkH4FuJvHZVIW0QCDWxcG96kCq7An/KVH2NfJe6rKZU2AsHeB3OEjPNRxi4QC34Xdx7I2KGYn6IpT7gxQ=="],
+
+ "remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="],
+
+ "remark-math": ["remark-math@6.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-math": "^3.0.0", "micromark-extension-math": "^3.0.0", "unified": "^11.0.0" } }, "sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA=="],
+
+ "remark-mdx": ["remark-mdx@3.1.0", "", { "dependencies": { "mdast-util-mdx": "^3.0.0", "micromark-extension-mdxjs": "^3.0.0" } }, "sha512-Ngl/H3YXyBV9RcRNdlYsZujAmhsxwzxpDzpDEhFBVAGthS4GDgnctpDjgFl/ULx5UEDzqtW1cyBSNKqYYrqLBA=="],
+
+ "remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="],
+
+ "remark-rehype": ["remark-rehype@11.1.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-g/osARvjkBXb6Wo0XvAeXQohVta8i84ACbenPpoSsxTOQH/Ae0/RGP4WZgnMH5pMLpsj4FG7OHmcIcXxpza8eQ=="],
+
+ "remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="],
+
+ "resize-observer-polyfill": ["resize-observer-polyfill@1.5.1", "", {}, "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg=="],
+
+ "resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="],
+
+ "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
+
+ "resolve-protobuf-schema": ["resolve-protobuf-schema@2.1.0", "", { "dependencies": { "protocol-buffers-schema": "^3.3.1" } }, "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ=="],
+
+ "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
+
+ "robust-predicates": ["robust-predicates@3.0.2", "", {}, "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg=="],
+
+ "rollup": ["rollup@4.30.0", "", { "dependencies": { "@types/estree": "1.0.6" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.30.0", "@rollup/rollup-android-arm64": "4.30.0", "@rollup/rollup-darwin-arm64": "4.30.0", "@rollup/rollup-darwin-x64": "4.30.0", "@rollup/rollup-freebsd-arm64": "4.30.0", "@rollup/rollup-freebsd-x64": "4.30.0", "@rollup/rollup-linux-arm-gnueabihf": "4.30.0", "@rollup/rollup-linux-arm-musleabihf": "4.30.0", "@rollup/rollup-linux-arm64-gnu": "4.30.0", "@rollup/rollup-linux-arm64-musl": "4.30.0", "@rollup/rollup-linux-loongarch64-gnu": "4.30.0", "@rollup/rollup-linux-powerpc64le-gnu": "4.30.0", "@rollup/rollup-linux-riscv64-gnu": "4.30.0", "@rollup/rollup-linux-s390x-gnu": "4.30.0", "@rollup/rollup-linux-x64-gnu": "4.30.0", "@rollup/rollup-linux-x64-musl": "4.30.0", "@rollup/rollup-win32-arm64-msvc": "4.30.0", "@rollup/rollup-win32-ia32-msvc": "4.30.0", "@rollup/rollup-win32-x64-msvc": "4.30.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-sDnr1pcjTgUT69qBksNF1N1anwfbyYG6TBQ22b03bII8EdiUQ7J0TlozVaTMjT/eEJAO49e1ndV7t+UZfL1+vA=="],
+
+ "roughjs": ["roughjs@4.6.6", "", { "dependencies": { "hachure-fill": "^0.5.2", "path-data-parser": "^0.1.0", "points-on-curve": "^0.2.0", "points-on-path": "^0.2.1" } }, "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ=="],
+
+ "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
+
+ "rw": ["rw@1.3.3", "", {}, "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="],
+
+ "s.color": ["s.color@0.0.15", "", {}, "sha512-AUNrbEUHeKY8XsYr/DYpl+qk5+aM+DChopnWOPEzn8YKzOhv4l2zH6LzZms3tOZP3wwdOyc0RmTciyi46HLIuA=="],
+
+ "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
+
+ "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
+
+ "sass": ["sass@1.89.1", "", { "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", "source-map-js": ">=0.6.2 <2.0.0" }, "optionalDependencies": { "@parcel/watcher": "^2.4.1" }, "bin": { "sass": "sass.js" } }, "sha512-eMLLkl+qz7tx/0cJ9wI+w09GQ2zodTkcE/aVfywwdlRcI3EO19xGnbmJwg/JMIm+5MxVJ6outddLZ4Von4E++Q=="],
+
+ "sass-formatter": ["sass-formatter@0.7.9", "", { "dependencies": { "suf-log": "^2.5.3" } }, "sha512-CWZ8XiSim+fJVG0cFLStwDvft1VI7uvXdCNJYXhDvowiv+DsbD1nXLiQ4zrE5UBvj5DWZJ93cwN0NX5PMsr1Pw=="],
+
+ "scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
+
+ "screenfull": ["screenfull@5.2.0", "", {}, "sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA=="],
+
+ "scroll-into-view-if-needed": ["scroll-into-view-if-needed@2.2.31", "", { "dependencies": { "compute-scroll-into-view": "^1.0.20" } }, "sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA=="],
+
+ "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
+
+ "semver-compare": ["semver-compare@1.0.0", "", {}, "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="],
+
+ "set-value": ["set-value@2.0.1", "", { "dependencies": { "extend-shallow": "^2.0.1", "is-extendable": "^0.1.1", "is-plain-object": "^2.0.3", "split-string": "^3.0.1" } }, "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw=="],
+
+ "shapefile": ["shapefile@0.6.6", "", { "dependencies": { "array-source": "0.0", "commander": "2", "path-source": "0.1", "slice-source": "0.4", "stream-source": "0.3", "text-encoding": "^0.6.4" }, "bin": { "dbf2json": "bin/dbf2json", "shp2json": "bin/shp2json" } }, "sha512-rLGSWeK2ufzCVx05wYd+xrWnOOdSV7xNUW5/XFgx3Bc02hBkpMlrd2F1dDII7/jhWzv0MSyBFh5uJIy9hLdfuw=="],
+
+ "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
+
+ "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
+
+ "shiki": ["shiki@3.4.2", "", { "dependencies": { "@shikijs/core": "3.4.2", "@shikijs/engine-javascript": "3.4.2", "@shikijs/engine-oniguruma": "3.4.2", "@shikijs/langs": "3.4.2", "@shikijs/themes": "3.4.2", "@shikijs/types": "3.4.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-wuxzZzQG8kvZndD7nustrNFIKYJ1jJoWIPaBpVe2+KHSvtzMi4SBjOxrigs8qeqce/l3U0cwiC+VAkLKSunHQQ=="],
+
+ "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
+
+ "simple-statistics": ["simple-statistics@7.8.7", "", {}, "sha512-ed5FwTNYvkMTfbCai1U+r3symP+lIPKWCqKdudpN4NFNMn9RtDlFtSyAQhCp4oPH0YBjWu/qnW+5q5ZkPB3uHQ=="],
+
+ "simplify-geojson": ["simplify-geojson@1.0.5", "", { "dependencies": { "concat-stream": "~1.4.1", "minimist": "1.2.6", "simplify-geometry": "0.0.2" }, "bin": { "simplify-geojson": "cli.js" } }, "sha512-02l1W4UipP5ivNVq6kX15mAzCRIV1oI3tz0FUEyOsNiv1ltuFDjbNhO+nbv/xhbDEtKqWLYuzpWhUsJrjR/ypA=="],
+
+ "simplify-geometry": ["simplify-geometry@0.0.2", "", {}, "sha512-ZEyrplkqgCqDlL7V8GbbYgTLlcnNF+MWWUdy8s8ZeJru50bnI71rDew/I+HG36QS2mPOYAq1ZjwNXxHJ8XOVBw=="],
+
+ "slice-source": ["slice-source@0.4.1", "", {}, "sha512-YiuPbxpCj4hD9Qs06hGAz/OZhQ0eDuALN0lRWJez0eD/RevzKqGdUx1IOMUnXgpr+sXZLq3g8ERwbAH0bCb8vg=="],
+
+ "sort-object-keys": ["sort-object-keys@1.1.3", "", {}, "sha512-855pvK+VkU7PaKYPc+Jjnmt4EzejQHyhhF33q31qG8x7maDzkeFhAAThdCYay11CISO+qAMwjOBP+fPZe0IPyg=="],
+
+ "sort-order": ["sort-order@1.1.2", "", {}, "sha512-Q8tOrwB1TSv9fNUXym9st3TZJODtmcOIi2JWCkVNQPrRg17KPwlpwweTEb7pMwUIFMTAgx2/JsQQXEPFzYQj3A=="],
+
+ "source-map": ["source-map@0.7.4", "", {}, "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA=="],
+
+ "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
+
+ "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
+
+ "split-on-first": ["split-on-first@3.0.0", "", {}, "sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA=="],
+
+ "split-string": ["split-string@3.1.0", "", { "dependencies": { "extend-shallow": "^3.0.0" } }, "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw=="],
+
+ "sse.js": ["sse.js@2.6.0", "", {}, "sha512-eGEqOwiPX9Cm+KsOYkcz7HIEqWUSOFeChr0sT515hDOBLvQy5yxaLSZx9JWMhwjf75CXJq+7cgG1MKNh9GQ36w=="],
+
+ "stream-source": ["stream-source@0.3.5", "", {}, "sha512-ZuEDP9sgjiAwUVoDModftG0JtYiLUV8K4ljYD1VyUMRWtbVf92474o4kuuul43iZ8t/hRuiDAx1dIJSvirrK/g=="],
+
+ "string-convert": ["string-convert@0.2.1", "", {}, "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A=="],
+
+ "string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
+
+ "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
+
+ "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
+
+ "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
+
+ "strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
+
+ "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
+
+ "style-to-object": ["style-to-object@1.0.8", "", { "dependencies": { "inline-style-parser": "0.2.4" } }, "sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g=="],
+
+ "stylis": ["stylis@4.3.6", "", {}, "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ=="],
+
+ "sucrase": ["sucrase@3.35.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "glob": "^10.3.10", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA=="],
+
+ "suf-log": ["suf-log@2.5.3", "", { "dependencies": { "s.color": "0.0.15" } }, "sha512-KvC8OPjzdNOe+xQ4XWJV2whQA0aM1kGVczMQ8+dStAO6KfEB140JEVQ9dE76ONZ0/Ylf67ni4tILPJB41U0eow=="],
+
+ "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
+
+ "swr": ["swr@2.3.3", "", { "dependencies": { "dequal": "^2.0.3", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-dshNvs3ExOqtZ6kJBaAsabhPdHyeY4P2cKwRCniDVifBMoG/SVI7tfLWqPXriVspf2Rg4tPzXJTnwaihIeFw2A=="],
+
+ "tabbable": ["tabbable@6.2.0", "", {}, "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew=="],
+
+ "tailwindcss": ["tailwindcss@3.4.17", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.6.0", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.3.2", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.21.6", "lilconfig": "^3.1.3", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.1.1", "postcss": "^8.4.47", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.2", "postcss-nested": "^6.2.0", "postcss-selector-parser": "^6.1.2", "resolve": "^1.22.8", "sucrase": "^3.35.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og=="],
+
+ "text-encoding": ["text-encoding@0.6.4", "", {}, "sha512-hJnc6Qg3dWoOMkqP53F0dzRIgtmsAge09kxUIqGrEUS4qr5rWLckGYaQAVr+opBrIMRErGgy6f5aPnyPpyGRfg=="],
+
+ "thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="],
+
+ "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="],
+
+ "throttle-debounce": ["throttle-debounce@5.0.2", "", {}, "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A=="],
+
+ "tinyexec": ["tinyexec@1.0.1", "", {}, "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw=="],
+
+ "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
+
+ "toggle-selection": ["toggle-selection@1.0.6", "", {}, "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ=="],
+
+ "topojson-client": ["topojson-client@3.1.0", "", { "dependencies": { "commander": "2" }, "bin": { "topo2geo": "bin/topo2geo", "topomerge": "bin/topomerge", "topoquantize": "bin/topoquantize" } }, "sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw=="],
+
+ "topojson-server": ["topojson-server@3.0.1", "", { "dependencies": { "commander": "2" }, "bin": { "geo2topo": "bin/geo2topo" } }, "sha512-/VS9j/ffKr2XAOjlZ9CgyyeLmgJ9dMwq6Y0YEON8O7p/tGGk+dCWnrE03zEdu7i4L7YsFZLEPZPzCvcB7lEEXw=="],
+
+ "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
+
+ "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
+
+ "ts-dedent": ["ts-dedent@2.2.0", "", {}, "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ=="],
+
+ "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="],
+
+ "ts-md5": ["ts-md5@1.3.1", "", {}, "sha512-DiwiXfwvcTeZ5wCE0z+2A9EseZsztaiZtGrtSaY5JOD7ekPnR/GoIVD5gXZAlK9Na9Kvpo9Waz5rW64WKAWApg=="],
+
+ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+
+ "typedarray": ["typedarray@0.0.6", "", {}, "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="],
+
+ "typescript": ["typescript@4.4.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-gzP+t5W4hdy4c+68bfcv0t400HVJMMd2+H9B7gae1nQlBzCqvrXX+6GL/b3GAgyTH966pzrZ70/fRjwAtZksSQ=="],
+
+ "ufo": ["ufo@1.6.1", "", {}, "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA=="],
+
+ "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="],
+
+ "unist-util-find-after": ["unist-util-find-after@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ=="],
+
+ "unist-util-is": ["unist-util-is@6.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw=="],
+
+ "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="],
+
+ "unist-util-position-from-estree": ["unist-util-position-from-estree@2.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ=="],
+
+ "unist-util-remove-position": ["unist-util-remove-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q=="],
+
+ "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="],
+
+ "unist-util-visit": ["unist-util-visit@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="],
+
+ "unist-util-visit-parents": ["unist-util-visit-parents@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw=="],
+
+ "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
+
+ "update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="],
+
+ "url-join": ["url-join@5.0.0", "", {}, "sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA=="],
+
+ "use-debounce": ["use-debounce@10.0.4", "", { "peerDependencies": { "react": "*" } }, "sha512-6Cf7Yr7Wk7Kdv77nnJMf6de4HuDE4dTxKij+RqE9rufDsI6zsbjyAxcH5y2ueJCQAnfgKbzXbZHYlkFwmBlWkw=="],
+
+ "use-isomorphic-layout-effect": ["use-isomorphic-layout-effect@1.2.1", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA=="],
+
+ "use-merge-value": ["use-merge-value@1.2.0", "", { "peerDependencies": { "react": ">= 16.x" } }, "sha512-DXgG0kkgJN45TcyoXL49vJnn55LehnrmoHc7MbKi+QDBvr8dsesqws8UlyIWGHMR+JXgxc1nvY+jDGMlycsUcw=="],
+
+ "use-sync-external-store": ["use-sync-external-store@1.5.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A=="],
+
+ "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
+
+ "utility-types": ["utility-types@3.11.0", "", {}, "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw=="],
+
+ "uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="],
+
+ "v8n": ["v8n@1.5.1", "", {}, "sha512-LdabyT4OffkyXFCe9UT+uMkxNBs5rcTVuZClvxQr08D5TUgo1OFKkoT65qYRCsiKBl/usHjpXvP4hHMzzDRj3A=="],
+
+ "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
+
+ "vfile-location": ["vfile-location@5.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg=="],
+
+ "vfile-message": ["vfile-message@4.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw=="],
+
+ "vite": ["vite@5.4.11", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q=="],
+
+ "void-elements": ["void-elements@3.1.0", "", {}, "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w=="],
+
+ "vscode-jsonrpc": ["vscode-jsonrpc@8.2.0", "", {}, "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA=="],
+
+ "vscode-languageserver": ["vscode-languageserver@9.0.1", "", { "dependencies": { "vscode-languageserver-protocol": "3.17.5" }, "bin": { "installServerIntoExtension": "bin/installServerIntoExtension" } }, "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g=="],
+
+ "vscode-languageserver-protocol": ["vscode-languageserver-protocol@3.17.5", "", { "dependencies": { "vscode-jsonrpc": "8.2.0", "vscode-languageserver-types": "3.17.5" } }, "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg=="],
+
+ "vscode-languageserver-textdocument": ["vscode-languageserver-textdocument@1.0.12", "", {}, "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA=="],
+
+ "vscode-languageserver-types": ["vscode-languageserver-types@3.17.5", "", {}, "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg=="],
+
+ "vscode-uri": ["vscode-uri@3.0.8", "", {}, "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw=="],
+
+ "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="],
+
+ "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
+
+ "wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
+
+ "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
+
+ "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
+
+ "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
+
+ "yaml": ["yaml@2.8.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ=="],
+
+ "zustand": ["zustand@3.7.2", "", { "peerDependencies": { "react": ">=16.8" }, "optionalPeers": ["react"] }, "sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA=="],
+
+ "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
+
+ "@babel/helper-compilation-targets/browserslist": ["browserslist@4.24.3", "", { "dependencies": { "caniuse-lite": "^1.0.30001688", "electron-to-chromium": "^1.5.73", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.1" }, "bin": { "browserslist": "cli.js" } }, "sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA=="],
+
+ "@babel/helper-define-polyfill-provider/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.27.1", "", {}, "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw=="],
+
+ "@babel/plugin-transform-runtime/@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="],
+
+ "@babel/plugin-transform-runtime/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.27.1", "", {}, "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw=="],
+
+ "@babel/traverse/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="],
+
+ "@douyinfe/semi-foundation/remark-gfm": ["remark-gfm@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-U92vJgBPkbw4Zfu/IiW2oTZLSL3Zpv+uI7My2eq8JxKgqraFdU8YUGicEJCEgSbeaG+QDFqIcwwfMTOEelPxuA=="],
+
+ "@emotion/babel-plugin/@emotion/hash": ["@emotion/hash@0.9.2", "", {}, "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g=="],
+
+ "@emotion/babel-plugin/convert-source-map": ["convert-source-map@1.9.0", "", {}, "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="],
+
+ "@emotion/babel-plugin/escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
+
+ "@emotion/babel-plugin/source-map": ["source-map@0.5.7", "", {}, "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ=="],
+
+ "@emotion/babel-plugin/stylis": ["stylis@4.2.0", "", {}, "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw=="],
+
+ "@emotion/cache/stylis": ["stylis@4.2.0", "", {}, "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw=="],
+
+ "@emotion/serialize/@emotion/hash": ["@emotion/hash@0.9.2", "", {}, "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g=="],
+
+ "@emotion/serialize/@emotion/unitless": ["@emotion/unitless@0.10.0", "", {}, "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg=="],
+
+ "@lobehub/fluent-emoji/lucide-react": ["lucide-react@0.469.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-28vvUnnKQ/dBwiCQtwJw7QauYnE7yd2Cyp4tTTJpvglX4EMpbflcdBgrgToX2j71B3YvugK/NH3BGUk+E/p/Fw=="],
+
+ "@lobehub/icons/lucide-react": ["lucide-react@0.469.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-28vvUnnKQ/dBwiCQtwJw7QauYnE7yd2Cyp4tTTJpvglX4EMpbflcdBgrgToX2j71B3YvugK/NH3BGUk+E/p/Fw=="],
+
+ "@lobehub/ui/@dnd-kit/sortable": ["@dnd-kit/sortable@10.0.0", "", { "dependencies": { "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@dnd-kit/core": "^6.3.0", "react": ">=16.8.0" } }, "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg=="],
+
+ "@lobehub/ui/lucide-react": ["lucide-react@0.484.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-oZy8coK9kZzvqhSgfbGkPtTgyjpBvs3ukLgDPv14dSOZtBtboryWF5o8i3qen7QbGg7JhiJBz5mK1p8YoMZTLQ=="],
+
+ "@lobehub/ui/rc-collapse": ["rc-collapse@4.0.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", "rc-motion": "^2.3.4", "rc-util": "^5.27.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-SwoOByE39/3oIokDs/BnkqI+ltwirZbP8HZdq1/3SkPSBi7xDdvWHTp7cpNI9ullozkR6mwTWQi6/E/9huQVrA=="],
+
+ "@radix-ui/react-dismissable-layer/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA=="],
+
+ "@radix-ui/react-popper/@floating-ui/react-dom": ["@floating-ui/react-dom@0.7.2", "", { "dependencies": { "@floating-ui/dom": "^0.5.3", "use-isomorphic-layout-effect": "^1.1.1" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-1T0sJcpHgX/u4I1OzIEhlcrvkUN8ln39nz7fMoE/2HDHrPiMFoOGR7++GYyfUmIQHkkrTinaeQsO3XWubjSvGg=="],
+
+ "@radix-ui/react-popper/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA=="],
+
+ "@radix-ui/react-presence/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA=="],
+
+ "@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-compose-refs": "1.0.0" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw=="],
+
+ "@radix-ui/react-tooltip/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA=="],
+
+ "@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-compose-refs": "1.0.0" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw=="],
+
+ "@visactor/vrender-kits/roughjs": ["roughjs@4.5.2", "", { "dependencies": { "path-data-parser": "^0.1.0", "points-on-curve": "^0.2.0", "points-on-path": "^0.2.1" } }, "sha512-2xSlLDKdsWyFxrveYWk9YQ/Y9UfK38EAMRNkYkMqYBJvPX8abCa9PN0x3w02H8Oa6/0bcZICJU+U95VumPqseg=="],
+
+ "antd/scroll-into-view-if-needed": ["scroll-into-view-if-needed@3.1.0", "", { "dependencies": { "compute-scroll-into-view": "^3.0.2" } }, "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ=="],
+
+ "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
+
+ "cosmiconfig/yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="],
+
+ "cytoscape-fcose/cose-base": ["cose-base@2.2.0", "", { "dependencies": { "layout-base": "^2.0.0" } }, "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g=="],
+
+ "d3/d3-dsv": ["d3-dsv@3.0.1", "", { "dependencies": { "commander": "7", "iconv-lite": "0.6", "rw": "1" }, "bin": { "csv2json": "bin/dsv2json.js", "csv2tsv": "bin/dsv2dsv.js", "dsv2dsv": "bin/dsv2dsv.js", "dsv2json": "bin/dsv2json.js", "json2csv": "bin/json2dsv.js", "json2dsv": "bin/json2dsv.js", "json2tsv": "bin/json2dsv.js", "tsv2csv": "bin/dsv2dsv.js", "tsv2json": "bin/dsv2json.js" } }, "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q=="],
+
+ "d3/d3-geo": ["d3-geo@3.1.1", "", { "dependencies": { "d3-array": "2.5.0 - 3" } }, "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q=="],
+
+ "d3-dsv/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
+
+ "d3-fetch/d3-dsv": ["d3-dsv@3.0.1", "", { "dependencies": { "commander": "7", "iconv-lite": "0.6", "rw": "1" }, "bin": { "csv2json": "bin/dsv2json.js", "csv2tsv": "bin/dsv2dsv.js", "dsv2dsv": "bin/dsv2dsv.js", "dsv2json": "bin/dsv2json.js", "json2csv": "bin/json2dsv.js", "json2dsv": "bin/json2dsv.js", "json2tsv": "bin/json2dsv.js", "tsv2csv": "bin/dsv2dsv.js", "tsv2json": "bin/dsv2json.js" } }, "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q=="],
+
+ "d3-geo/d3-array": ["d3-array@1.2.4", "", {}, "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw=="],
+
+ "d3-sankey/d3-array": ["d3-array@1.2.4", "", {}, "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw=="],
+
+ "d3-sankey/d3-shape": ["d3-shape@1.3.7", "", { "dependencies": { "d3-path": "1" } }, "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw=="],
+
+ "extend-shallow/is-extendable": ["is-extendable@0.1.1", "", {}, "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw=="],
+
+ "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
+
+ "geojson-dissolve/@turf/meta": ["@turf/meta@3.14.0", "", {}, "sha512-OtXqLQuR9hlQ/HkAF/OdzRea7E0eZK1ay8y8CBXkoO2R6v34CsDrWYLMSo0ZzMsaQDpKo76NPP2GGo+PyG1cSg=="],
+
+ "geojson-flatten/minimist": ["minimist@1.2.0", "", {}, "sha512-7Wl+Jz+IGWuSdgsQEJ4JunV0si/iMhg42MnQQG6h1R6TNeVenp4U9x5CC5v/gYqz/fENLQITAWXidNtVL0NNbw=="],
+
+ "glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
+
+ "hast-util-from-parse5/property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],
+
+ "hast-util-to-html/property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],
+
+ "hastscript/property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],
+
+ "hoist-non-react-statics/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
+
+ "leva/react-dropzone": ["react-dropzone@12.1.0", "", { "dependencies": { "attr-accept": "^2.2.2", "file-selector": "^0.5.0", "prop-types": "^15.8.1" }, "peerDependencies": { "react": ">= 16.8" } }, "sha512-iBYHA1rbopIvtzokEX4QubO6qk5IF/x3BtKGu74rF2JkQDXnwC4uO/lHKpaw4PJIV6iIAYOlwLv2FpiGyqHNog=="],
+
+ "mermaid/marked": ["marked@15.0.12", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA=="],
+
+ "mlly/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
+
+ "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
+
+ "path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
+
+ "prettier-package-json/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="],
+
+ "prettier-package-json/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
+
+ "prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
+
+ "react-draggable/clsx": ["clsx@1.2.1", "", {}, "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg=="],
+
+ "react-rnd/tslib": ["tslib@2.6.2", "", {}, "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="],
+
+ "react-telegram-login/react": ["react@16.14.0", "", { "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", "prop-types": "^15.6.2" } }, "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g=="],
+
+ "react-toastify/clsx": ["clsx@1.2.1", "", {}, "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg=="],
+
+ "sass/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
+
+ "set-value/is-extendable": ["is-extendable@0.1.1", "", {}, "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw=="],
+
+ "shapefile/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
+
+ "simplify-geojson/concat-stream": ["concat-stream@1.4.11", "", { "dependencies": { "inherits": "~2.0.1", "readable-stream": "~1.1.9", "typedarray": "~0.0.5" } }, "sha512-X3JMh8+4je3U1cQpG87+f9lXHDrqcb2MVLg9L7o8b1UZ0DzhRrUpdn65ttzu10PpJPPI3MQNkis+oha6TSA9Mw=="],
+
+ "split-string/extend-shallow": ["extend-shallow@3.0.2", "", { "dependencies": { "assign-symbols": "^1.0.0", "is-extendable": "^1.0.1" } }, "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q=="],
+
+ "string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
+
+ "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
+
+ "string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
+
+ "strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
+
+ "sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="],
+
+ "topojson-client/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
+
+ "topojson-server/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
+
+ "vite/postcss": ["postcss@8.4.49", "", { "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA=="],
+
+ "wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
+
+ "wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
+
+ "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
+
+ "@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001690", "", {}, "sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w=="],
+
+ "@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.76", "", {}, "sha512-CjVQyG7n7Sr+eBXE86HIulnL5N8xZY1sgmOPGuq/F0Rr0FJq63lg0kEtOIDfZBk44FnDLf6FUJ+dsJcuiUDdDQ=="],
+
+ "@babel/helper-compilation-targets/browserslist/update-browserslist-db": ["update-browserslist-db@1.1.1", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.0" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A=="],
+
+ "@babel/plugin-transform-runtime/@babel/helper-module-imports/@babel/traverse": ["@babel/traverse@7.27.1", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.27.1", "@babel/parser": "^7.27.1", "@babel/template": "^7.27.1", "@babel/types": "^7.27.1", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg=="],
+
+ "@babel/plugin-transform-runtime/@babel/helper-module-imports/@babel/types": ["@babel/types@7.27.1", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q=="],
+
+ "@radix-ui/react-popper/@floating-ui/react-dom/@floating-ui/dom": ["@floating-ui/dom@0.5.4", "", { "dependencies": { "@floating-ui/core": "^0.7.3" } }, "sha512-419BMceRLq0RrmTSDxn8hf9R3VCJv2K9PUfugh5JyEFmdjzDo+e8U5EdR8nzKq8Yj1htzLm3b6eQEEam3/rrtg=="],
+
+ "@radix-ui/react-primitive/@radix-ui/react-slot/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA=="],
+
+ "antd/scroll-into-view-if-needed/compute-scroll-into-view": ["compute-scroll-into-view@3.1.1", "", {}, "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw=="],
+
+ "cytoscape-fcose/cose-base/layout-base": ["layout-base@2.0.1", "", {}, "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg=="],
+
+ "d3-fetch/d3-dsv/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="],
+
+ "d3-fetch/d3-dsv/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
+
+ "d3-sankey/d3-shape/d3-path": ["d3-path@1.0.9", "", {}, "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="],
+
+ "d3/d3-dsv/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="],
+
+ "d3/d3-dsv/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
+
+ "glob/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
+
+ "leva/react-dropzone/file-selector": ["file-selector@0.5.0", "", { "dependencies": { "tslib": "^2.0.3" } }, "sha512-s8KNnmIDTBoD0p9uJ9uD0XY38SCeBOtj0UMXyQSLg1Ypfrfj8+dAvwsLjYQkQ2GjhVtp2HrnF5cJzMhBjfD8HA=="],
+
+ "mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="],
+
+ "sass/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
+
+ "simplify-geojson/concat-stream/readable-stream": ["readable-stream@1.1.14", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.1", "isarray": "0.0.1", "string_decoder": "~0.10.x" } }, "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ=="],
+
+ "simplify-geojson/concat-stream/typedarray": ["typedarray@0.0.7", "", {}, "sha512-ueeb9YybpjhivjbHP2LdFDAjbS948fGEPj+ACAMs4xCMmh72OCOMQWBQKlaN4ZNQ04yfLSDLSx1tGRIoWimObQ=="],
+
+ "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
+
+ "wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
+
+ "wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
+
+ "@babel/plugin-transform-runtime/@babel/helper-module-imports/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
+
+ "@babel/plugin-transform-runtime/@babel/helper-module-imports/@babel/traverse/@babel/generator": ["@babel/generator@7.27.1", "", { "dependencies": { "@babel/parser": "^7.27.1", "@babel/types": "^7.27.1", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w=="],
+
+ "@babel/plugin-transform-runtime/@babel/helper-module-imports/@babel/traverse/@babel/parser": ["@babel/parser@7.27.2", "", { "dependencies": { "@babel/types": "^7.27.1" }, "bin": "./bin/babel-parser.js" }, "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw=="],
+
+ "@babel/plugin-transform-runtime/@babel/helper-module-imports/@babel/traverse/@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
+
+ "@babel/plugin-transform-runtime/@babel/helper-module-imports/@babel/traverse/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="],
+
+ "@babel/plugin-transform-runtime/@babel/helper-module-imports/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
+
+ "@babel/plugin-transform-runtime/@babel/helper-module-imports/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="],
+
+ "@radix-ui/react-popper/@floating-ui/react-dom/@floating-ui/dom/@floating-ui/core": ["@floating-ui/core@0.7.3", "", {}, "sha512-buc8BXHmG9l82+OQXOFU3Kr2XQx9ys01U/Q9HMIrZ300iLc8HLMgh7dcCqgYzAzf4BkoQvDcXf5Y+CuEZ5JBYg=="],
+
+ "simplify-geojson/concat-stream/readable-stream/string_decoder": ["string_decoder@0.10.31", "", {}, "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ=="],
+
+ "@babel/plugin-transform-runtime/@babel/helper-module-imports/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="],
+ }
+}
diff --git a/web/bun.lockb b/web/bun.lockb
deleted file mode 100755
index 4d0cdea90f8..00000000000
Binary files a/web/bun.lockb and /dev/null differ
diff --git a/web/src/components/auth/OAuth2Callback.js b/web/src/components/auth/OAuth2Callback.js
index 6d0bbe70863..7d435574f5d 100644
--- a/web/src/components/auth/OAuth2Callback.js
+++ b/web/src/components/auth/OAuth2Callback.js
@@ -1,4 +1,4 @@
-import React, { useContext, useEffect, useState } from 'react';
+import React, { useContext, useEffect } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { API, showError, showSuccess, updateAPI, setUserData } from '../../helpers';
@@ -7,22 +7,28 @@ import Loading from '../common/Loading';
const OAuth2Callback = (props) => {
const { t } = useTranslation();
- const [searchParams, setSearchParams] = useSearchParams();
+ const [searchParams] = useSearchParams();
+ const [, userDispatch] = useContext(UserContext);
+ const navigate = useNavigate();
- const [userState, userDispatch] = useContext(UserContext);
- const [prompt, setPrompt] = useState(t('处理中...'));
+ // 最大重试次数
+ const MAX_RETRIES = 3;
- let navigate = useNavigate();
+ const sendCode = async (code, state, retry = 0) => {
+ try {
+ const { data: resData } = await API.get(
+ `/api/oauth/${props.type}?code=${code}&state=${state}`,
+ );
+
+ const { success, message, data } = resData;
+
+ if (!success) {
+ throw new Error(message || 'OAuth2 callback error');
+ }
- const sendCode = async (code, state, count) => {
- const res = await API.get(
- `/api/oauth/${props.type}?code=${code}&state=${state}`,
- );
- const { success, message, data } = res.data;
- if (success) {
if (message === 'bind') {
showSuccess(t('绑定成功!'));
- navigate('/console/setting');
+ navigate('/console/personal');
} else {
userDispatch({ type: 'login', payload: data });
localStorage.setItem('user', JSON.stringify(data));
@@ -31,27 +37,34 @@ const OAuth2Callback = (props) => {
showSuccess(t('登录成功!'));
navigate('/console/token');
}
- } else {
- showError(message);
- if (count === 0) {
- setPrompt(t('操作失败,重定向至登录界面中...'));
- navigate('/console/setting'); // in case this is failed to bind GitHub
- return;
+ } catch (error) {
+ if (retry < MAX_RETRIES) {
+ // 递增的退避等待
+ await new Promise((resolve) => setTimeout(resolve, (retry + 1) * 2000));
+ return sendCode(code, state, retry + 1);
}
- count++;
- setPrompt(t('出现错误,第 ${count} 次重试中...', { count }));
- await new Promise((resolve) => setTimeout(resolve, count * 2000));
- await sendCode(code, state, count);
+
+ // 重试次数耗尽,提示错误并返回设置页面
+ showError(error.message || t('授权失败'));
+ navigate('/console/personal');
}
};
useEffect(() => {
- let code = searchParams.get('code');
- let state = searchParams.get('state');
- sendCode(code, state, 0).then();
+ const code = searchParams.get('code');
+ const state = searchParams.get('state');
+
+ // 参数缺失直接返回
+ if (!code) {
+ showError(t('未获取到授权码'));
+ navigate('/console/personal');
+ return;
+ }
+
+ sendCode(code, state);
}, []);
- return ;
+ return ;
};
export default OAuth2Callback;
diff --git a/web/src/components/common/Loading.js b/web/src/components/common/Loading.js
index a12be0538a0..738227550b8 100644
--- a/web/src/components/common/Loading.js
+++ b/web/src/components/common/Loading.js
@@ -1,22 +1,14 @@
import React from 'react';
import { Spin } from '@douyinfe/semi-ui';
-import { useTranslation } from 'react-i18next';
-const Loading = ({ prompt: name = '', size = 'large' }) => {
- const { t } = useTranslation();
+const Loading = ({ size = 'small' }) => {
return (
-
-
-
-
- {name ? t('{{name}}', { name }) : t('加载中...')}
-
-
+
+
);
};
diff --git a/web/src/components/layout/Footer.js b/web/src/components/layout/Footer.js
index 4f44c1dca47..d380e574507 100644
--- a/web/src/components/layout/Footer.js
+++ b/web/src/components/layout/Footer.js
@@ -22,7 +22,7 @@ const FooterBar = () => {
const currentYear = new Date().getFullYear();
const customFooter = useMemo(() => (
-
handleNavLinkClick('home')} className="flex items-center gap-2 group ml-2">
- {isLoading ? (
-
- ) : (
+
+ }
+ >
- )}
+
- {isLoading ? (
-
- ) : (
-
+
+ }
+ >
+
{systemName}
- )}
+
{(isSelfUseMode || isDemoSiteMode) && !isLoading && (
{
- const [userState, userDispatch] = useContext(UserContext);
- const [statusState, statusDispatch] = useContext(StatusContext);
- const { state: styleState } = useStyle();
+ const [, userDispatch] = useContext(UserContext);
+ const [, statusDispatch] = useContext(StatusContext);
+ const isMobile = useIsMobile();
+ const [collapsed, , setCollapsed] = useSidebarCollapsed();
+ const [drawerOpen, setDrawerOpen] = useState(false);
const { i18n } = useTranslation();
const location = useLocation();
@@ -26,6 +29,15 @@ const PageLayout = () => {
!location.pathname.startsWith('/console/chat') &&
location.pathname !== '/console/playground';
+ const isConsoleRoute = location.pathname.startsWith('/console');
+ const showSider = isConsoleRoute && (!isMobile || drawerOpen);
+
+ useEffect(() => {
+ if (isMobile && drawerOpen && collapsed) {
+ setCollapsed(false);
+ }
+ }, [isMobile, drawerOpen, collapsed, setCollapsed]);
+
const loadUser = () => {
let user = localStorage.getItem('user');
if (user) {
@@ -63,7 +75,6 @@ const PageLayout = () => {
linkElement.href = logo;
}
}
- // 从localStorage获取上次使用的语言
const savedLang = localStorage.getItem('i18nextLng');
if (savedLang) {
i18n.changeLanguage(savedLang);
@@ -76,7 +87,7 @@ const PageLayout = () => {
height: '100vh',
display: 'flex',
flexDirection: 'column',
- overflow: styleState.isMobile ? 'visible' : 'hidden',
+ overflow: isMobile ? 'visible' : 'hidden',
}}
>
{
zIndex: 100,
}}
>
-
+ setDrawerOpen(prev => !prev)} drawerOpen={drawerOpen} />
- {styleState.showSider && (
+ {showSider && (
{
border: 'none',
paddingRight: '0',
height: 'calc(100vh - 64px)',
+ width: 'var(--sidebar-current-width)',
}}
>
-
+ { if (isMobile) setDrawerOpen(false); }} />
)}
{
diff --git a/web/src/components/layout/SiderBar.js b/web/src/components/layout/SiderBar.js
index 8ede4bc8251..b18dad6ce1b 100644
--- a/web/src/components/layout/SiderBar.js
+++ b/web/src/components/layout/SiderBar.js
@@ -3,7 +3,7 @@ import { Link, useLocation } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { getLucideIcon, sidebarIconColors } from '../../helpers/render.js';
import { ChevronLeft } from 'lucide-react';
-import { useStyle, styleActions } from '../../context/Style/index.js';
+import { useSidebarCollapsed } from '../../hooks/useSidebarCollapsed.js';
import {
isAdmin,
isRoot,
@@ -13,7 +13,7 @@ import {
import {
Nav,
Divider,
- Tooltip,
+ Button,
} from '@douyinfe/semi-ui';
const routerMap = {
@@ -34,12 +34,11 @@ const routerMap = {
personal: '/console/personal',
};
-const SiderBar = () => {
+const SiderBar = ({ onNavigate = () => { } }) => {
const { t } = useTranslation();
- const { state: styleState, dispatch: styleDispatch } = useStyle();
+ const [collapsed, toggleCollapsed] = useSidebarCollapsed();
const [selectedKeys, setSelectedKeys] = useState(['home']);
- const [isCollapsed, setIsCollapsed] = useState(styleState.siderCollapsed);
const [chatItems, setChatItems] = useState([]);
const [openedKeys, setOpenedKeys] = useState([]);
const location = useLocation();
@@ -217,10 +216,14 @@ const SiderBar = () => {
}
}, [location.pathname, routerMapState]);
- // 同步折叠状态
+ // 监控折叠状态变化以更新 body class
useEffect(() => {
- setIsCollapsed(styleState.siderCollapsed);
- }, [styleState.siderCollapsed]);
+ if (collapsed) {
+ document.body.classList.add('sidebar-collapsed');
+ } else {
+ document.body.classList.remove('sidebar-collapsed');
+ }
+ }, [collapsed]);
// 获取菜单项对应的颜色
const getItemColor = (itemKey) => {
@@ -323,32 +326,13 @@ const SiderBar = () => {
return (
)}
diff --git a/web/src/components/table/ChannelsTable.js b/web/src/components/table/ChannelsTable.js
index 810993c4985..fba5db791d6 100644
--- a/web/src/components/table/ChannelsTable.js
+++ b/web/src/components/table/ChannelsTable.js
@@ -42,18 +42,22 @@ import {
IconTreeTriangleDown,
IconSearch,
IconMore,
+ IconDescend2
} from '@douyinfe/semi-icons';
-import { loadChannelModels, isMobile, copy } from '../../helpers';
+import { loadChannelModels, copy } from '../../helpers';
+import { useIsMobile } from '../../hooks/useIsMobile.js';
import EditTagModal from '../../pages/Channel/EditTagModal.js';
import { useTranslation } from 'react-i18next';
import { useTableCompactMode } from '../../hooks/useTableCompactMode';
+import { FaRandom } from 'react-icons/fa';
const ChannelsTable = () => {
const { t } = useTranslation();
+ const isMobile = useIsMobile();
let type2label = undefined;
- const renderType = (type) => {
+ const renderType = (type, channelInfo = undefined) => {
if (!type2label) {
type2label = new Map();
for (let i = 0; i < CHANNEL_OPTIONS.length; i++) {
@@ -61,12 +65,30 @@ const ChannelsTable = () => {
}
type2label[0] = { value: 0, label: t('未知类型'), color: 'grey' };
}
+
+ let icon = getChannelIcon(type);
+
+ if (channelInfo?.is_multi_key) {
+ icon = (
+ channelInfo?.multi_key_mode === 'random' ? (
+
+
+ {icon}
+
+ ) : (
+
+
+ {icon}
+
+ )
+ )
+ }
+
return (
{type2label[type]?.label}
@@ -77,7 +99,6 @@ const ChannelsTable = () => {
return (
@@ -86,65 +107,107 @@ const ChannelsTable = () => {
);
};
- const renderStatus = (status) => {
+ const renderStatus = (status, channelInfo = undefined) => {
+ if (channelInfo) {
+ if (channelInfo.is_multi_key) {
+ let keySize = channelInfo.multi_key_size;
+ let enabledKeySize = keySize;
+ if (channelInfo.multi_key_status_list) {
+ // multi_key_status_list is a map, key is key, value is status
+ // get multi_key_status_list length
+ enabledKeySize = keySize - Object.keys(channelInfo.multi_key_status_list).length;
+ }
+ return renderMultiKeyStatus(status, keySize, enabledKeySize);
+ }
+ }
switch (status) {
case 1:
return (
-
+
{t('已启用')}
);
case 2:
return (
-
+
{t('已禁用')}
);
case 3:
return (
-
+
{t('自动禁用')}
);
default:
return (
-
+
{t('未知状态')}
);
}
};
+ const renderMultiKeyStatus = (status, keySize, enabledKeySize) => {
+ switch (status) {
+ case 1:
+ return (
+
+ {t('已启用')} {enabledKeySize}/{keySize}
+
+ );
+ case 2:
+ return (
+
+ {t('已禁用')} {enabledKeySize}/{keySize}
+
+ );
+ case 3:
+ return (
+
+ {t('自动禁用')} {enabledKeySize}/{keySize}
+
+ );
+ default:
+ return (
+
+ {t('未知状态')} {enabledKeySize}/{keySize}
+
+ );
+ }
+ }
+
+
const renderResponseTime = (responseTime) => {
let time = responseTime / 1000;
time = time.toFixed(2) + t(' 秒');
if (responseTime === 0) {
return (
-
+
{t('未测试')}
);
} else if (responseTime <= 1000) {
return (
-
+
{time}
);
} else if (responseTime <= 3000) {
return (
-
+
{time}
);
} else if (responseTime <= 5000) {
return (
-
+
{time}
);
} else {
return (
-
+
{time}
);
@@ -281,6 +344,11 @@ const ChannelsTable = () => {
dataIndex: 'type',
render: (text, record, index) => {
if (record.children === undefined) {
+ if (record.channel_info) {
+ if (record.channel_info.is_multi_key) {
+ return <>{renderType(text, record.channel_info)}>;
+ }
+ }
return <>{renderType(text)}>;
} else {
return <>{renderTagType()}>;
@@ -304,12 +372,12 @@ const ChannelsTable = () => {
- {renderStatus(text)}
+ {renderStatus(text, record.channel_info)}
);
} else {
- return renderStatus(text);
+ return renderStatus(text, record.channel_info);
}
},
},
@@ -331,7 +399,7 @@ const ChannelsTable = () => {
-
+
{renderQuota(record.used_quota)}
@@ -339,7 +407,6 @@ const ChannelsTable = () => {
updateChannelBalance(record)}
>
@@ -352,7 +419,7 @@ const ChannelsTable = () => {
} else {
return (
-
+
{renderQuota(record.used_quota)}
@@ -482,9 +549,15 @@ const ChannelsTable = () => {
title: t('确定是否要删除此渠道?'),
content: t('此修改将不可逆'),
onOk: () => {
- manageChannel(record.id, 'delete', record).then(() => {
- removeRecord(record);
- });
+ (async () => {
+ await manageChannel(record.id, 'delete', record);
+ await refresh();
+ setTimeout(() => {
+ if (channels.length === 0 && activePage > 1) {
+ refresh(activePage - 1);
+ }
+ }, 100);
+ })();
},
});
},
@@ -492,7 +565,7 @@ const ChannelsTable = () => {
{
node: 'item',
name: t('复制'),
- type: 'primary',
+ type: 'tertiary',
onClick: () => {
Modal.confirm({
title: t('确定是否要复制此渠道?'),
@@ -510,15 +583,15 @@ const ChannelsTable = () => {
aria-label={t('测试单个渠道操作项目组')}
>
}
onClick={() => {
setCurrentTestChannel(record);
@@ -527,28 +600,66 @@ const ChannelsTable = () => {
/>
- {record.status === 1 ? (
-
+ {
+ record.status === 1 ? (
+
+ ) : (
+
+ )
+ }
+ manageChannel(record.id, 'enable_all', record),
+ }
+ ]}
+ >
+ }
+ />
+
+
) : (
-
+ record.status === 1 ? (
+
+ ) : (
+
+ )
)}
{
{
if (formApi) {
formApi.reset();
@@ -1885,7 +1970,6 @@ const ChannelsTable = () => {
placeholder={t('请输入标签名称')}
value={batchSetTagValue}
onChange={(v) => setBatchSetTagValue(v)}
- size='large'
/>
@@ -1907,9 +1991,55 @@ const ChannelsTable = () => {
{t('共')} {currentTestChannel.models.split(',').length} {t('个模型')}
-
+
+ )
+ }
+ visible={showModelTestModal && currentTestChannel !== null}
+ onCancel={handleCloseModal}
+ footer={
+
+ {isBatchTesting ? (
+
+ {t('停止测试')}
+
+ ) : (
+
+ {t('取消')}
+
+ )}
+
+ {isBatchTesting ? t('测试中...') : t('批量测试${count}个模型').replace(
+ '${count}',
+ currentTestChannel
+ ? currentTestChannel.models
+ .split(',')
+ .filter((model) =>
+ model.toLowerCase().includes(modelSearchKeyword.toLowerCase())
+ ).length
+ : 0
+ )}
+
+
+ }
+ maskClosable={!isBatchTesting}
+ className="!rounded-lg"
+ size={isMobile ? 'full-width' : 'large'}
+ >
+
+ {currentTestChannel && (
+
{/* 搜索与操作按钮 */}
-
+
{
/>
{
if (selectedModelKeys.length === 0) {
showError(t('请先选择模型!'));
@@ -1942,8 +2071,7 @@ const ChannelsTable = () => {
{
if (!currentTestChannel) return;
const successKeys = currentTestChannel.models
@@ -1962,57 +2090,6 @@ const ChannelsTable = () => {
{t('选择成功')}
-
- )
- }
- visible={showModelTestModal && currentTestChannel !== null}
- onCancel={handleCloseModal}
- footer={
-
- {isBatchTesting ? (
-
- {t('停止测试')}
-
- ) : (
-
- {t('取消')}
-
- )}
-
- {isBatchTesting ? t('测试中...') : t('批量测试${count}个模型').replace(
- '${count}',
- currentTestChannel
- ? currentTestChannel.models
- .split(',')
- .filter((model) =>
- model.toLowerCase().includes(modelSearchKeyword.toLowerCase())
- ).length
- : 0
- )}
-
-
- }
- maskClosable={!isBatchTesting}
- className="!rounded-lg"
- size={isMobile() ? 'full-width' : 'large'}
- >
-
- {currentTestChannel && (
-
{
if (isTesting) {
return (
-
+
{t('测试中')}
);
@@ -2041,7 +2118,7 @@ const ChannelsTable = () => {
if (!testResult) {
return (
-
+
{t('未开始')}
);
@@ -2050,7 +2127,6 @@ const ChannelsTable = () => {
return (
@@ -2072,8 +2148,7 @@ const ChannelsTable = () => {
const isTesting = testingModels.has(record.model);
return (
testChannel(currentTestChannel, record.model)}
loading={isTesting}
size='small'
diff --git a/web/src/components/table/LogsTable.js b/web/src/components/table/LogsTable.js
index cc1d208239e..bff87675168 100644
--- a/web/src/components/table/LogsTable.js
+++ b/web/src/components/table/LogsTable.js
@@ -20,7 +20,7 @@ import {
renderQuota,
stringToColor,
getLogOther,
- renderModelTag,
+ renderModelTag
} from '../../helpers';
import {
@@ -78,37 +78,37 @@ const LogsTable = () => {
switch (type) {
case 1:
return (
-
+
{t('充值')}
);
case 2:
return (
-
+
{t('消费')}
);
case 3:
return (
-
+
{t('管理')}
);
case 4:
return (
-
+
{t('系统')}
);
case 5:
return (
-
+
{t('错误')}
);
default:
return (
-
+
{t('未知')}
);
@@ -118,13 +118,13 @@ const LogsTable = () => {
function renderIsStream(bool) {
if (bool) {
return (
-
+
{t('流')}
);
} else {
return (
-
+
{t('非流')}
);
@@ -135,21 +135,21 @@ const LogsTable = () => {
const time = parseInt(type);
if (time < 101) {
return (
-
+
{' '}
{time} s{' '}
);
} else if (time < 300) {
return (
-
+
{' '}
{time} s{' '}
);
} else {
return (
-
+
{' '}
{time} s{' '}
@@ -162,21 +162,21 @@ const LogsTable = () => {
time = time.toFixed(1);
if (time < 3) {
return (
-
+
{' '}
{time} s{' '}
);
} else if (time < 10) {
return (
-
+
{' '}
{time} s{' '}
);
} else {
return (
-
+
{' '}
{time} s{' '}
@@ -356,28 +356,34 @@ const LogsTable = () => {
dataIndex: 'channel',
className: isAdmin() ? 'tableShow' : 'tableHiddle',
render: (text, record, index) => {
- return isAdminUser ? (
- record.type === 0 || record.type === 2 || record.type === 5 ? (
-
- {
-
-
- {' '}
- {text}{' '}
-
-
- }
-
- ) : (
- <>>
- )
- ) : (
- <>>
- );
+ let isMultiKey = false
+ let multiKeyIndex = -1;
+ let other = getLogOther(record.other);
+ if (other?.admin_info) {
+ let adminInfo = other.admin_info;
+ if (adminInfo?.is_multi_key) {
+ isMultiKey = true;
+ multiKeyIndex = adminInfo.multi_key_index;
+ }
+ }
+
+ return isAdminUser && (record.type === 0 || record.type === 2 || record.type === 5) ? (
+
+
+
+ {text}
+
+
+ {isMultiKey && (
+
+ {multiKeyIndex}
+
+ )}
+
+ ) : null;
},
},
{
@@ -389,7 +395,7 @@ const LogsTable = () => {
return isAdminUser ? (
{
@@ -415,7 +421,6 @@ const LogsTable = () => {
{
//cancel the row click event
@@ -567,7 +572,6 @@ const LogsTable = () => {
{
copyText(event, text);
@@ -693,22 +697,13 @@ const LogsTable = () => {
onCancel={() => setShowColumnSelector(false)}
footer={
- initDefaultColumns()}
- >
+ initDefaultColumns()}>
{t('重置')}
- setShowColumnSelector(false)}
- >
+ setShowColumnSelector(false)}>
{t('取消')}
- setShowColumnSelector(false)}
- >
+ setShowColumnSelector(false)}>
{t('确定')}
@@ -1215,11 +1210,10 @@ const LogsTable = () => {
@@ -1227,11 +1221,10 @@ const LogsTable = () => {
@@ -1239,12 +1232,11 @@ const LogsTable = () => {
@@ -1253,10 +1245,10 @@ const LogsTable = () => {
setCompactMode(!compactMode)}
+ size="small"
>
{compactMode ? t('自适应列表') : t('紧凑列表')}
@@ -1287,6 +1279,7 @@ const LogsTable = () => {
placeholder={[t('开始时间'), t('结束时间')]}
showClear
pure
+ size="small"
/>
@@ -1297,6 +1290,7 @@ const LogsTable = () => {
placeholder={t('令牌名称')}
showClear
pure
+ size="small"
/>
{
placeholder={t('模型名称')}
showClear
pure
+ size="small"
/>
{
placeholder={t('分组')}
showClear
pure
+ size="small"
/>
{isAdminUser && (
@@ -1323,6 +1319,7 @@ const LogsTable = () => {
placeholder={t('渠道 ID')}
showClear
pure
+ size="small"
/>
{
placeholder={t('用户名称')}
showClear
pure
+ size="small"
/>
>
)}
@@ -1351,6 +1349,7 @@ const LogsTable = () => {
refresh();
}, 0);
}}
+ size="small"
>
{t('全部')}
@@ -1375,14 +1374,15 @@ const LogsTable = () => {
{t('查询')}
{
if (formApi) {
formApi.reset();
@@ -1392,13 +1392,14 @@ const LogsTable = () => {
}, 100);
}
}}
+ size="small"
>
{t('重置')}
setShowColumnSelector(true)}
+ size="small"
>
{t('列设置')}
diff --git a/web/src/components/table/MjLogsTable.js b/web/src/components/table/MjLogsTable.js
index 66e52dd6b3c..57e221d9fcc 100644
--- a/web/src/components/table/MjLogsTable.js
+++ b/web/src/components/table/MjLogsTable.js
@@ -185,115 +185,115 @@ const LogsTable = () => {
switch (type) {
case 'IMAGINE':
return (
-
}>
+
}>
{t('绘图')}
);
case 'UPSCALE':
return (
-
}>
+
}>
{t('放大')}
);
case 'VIDEO':
return (
-
}>
+
}>
{t('视频')}
);
case 'EDITS':
return (
-
}>
+
}>
{t('编辑')}
);
case 'VARIATION':
return (
-
}>
+
}>
{t('变换')}
);
case 'HIGH_VARIATION':
return (
-
}>
+
}>
{t('强变换')}
);
case 'LOW_VARIATION':
return (
-
}>
+
}>
{t('弱变换')}
);
case 'PAN':
return (
-
}>
+
}>
{t('平移')}
);
case 'DESCRIBE':
return (
-
}>
+
}>
{t('图生文')}
);
case 'BLEND':
return (
-
}>
+
}>
{t('图混合')}
);
case 'UPLOAD':
return (
-
}>
+
}>
上传文件
);
case 'SHORTEN':
return (
-
}>
+
}>
{t('缩词')}
);
case 'REROLL':
return (
-
}>
+
}>
{t('重绘')}
);
case 'INPAINT':
return (
-
}>
+
}>
{t('局部重绘-提交')}
);
case 'ZOOM':
return (
-
}>
+
}>
{t('变焦')}
);
case 'CUSTOM_ZOOM':
return (
-
}>
+
}>
{t('自定义变焦-提交')}
);
case 'MODAL':
return (
-
}>
+
}>
{t('窗口处理')}
);
case 'SWAP_FACE':
return (
-
}>
+
}>
{t('换脸')}
);
default:
return (
-
}>
+
}>
{t('未知')}
);
@@ -304,31 +304,31 @@ const LogsTable = () => {
switch (code) {
case 1:
return (
-
}>
+
}>
{t('已提交')}
);
case 21:
return (
-
}>
+
}>
{t('等待中')}
);
case 22:
return (
-
}>
+
}>
{t('重复提交')}
);
case 0:
return (
-
}>
+
}>
{t('未提交')}
);
default:
return (
-
}>
+
}>
{t('未知')}
);
@@ -339,43 +339,43 @@ const LogsTable = () => {
switch (type) {
case 'SUCCESS':
return (
-
}>
+
}>
{t('成功')}
);
case 'NOT_START':
return (
-
}>
+
}>
{t('未启动')}
);
case 'SUBMITTED':
return (
-
}>
+
}>
{t('队列中')}
);
case 'IN_PROGRESS':
return (
-
}>
+
}>
{t('执行中')}
);
case 'FAILURE':
return (
-
}>
+
}>
{t('失败')}
);
case 'MODAL':
return (
-
}>
+
}>
{t('窗口等待')}
);
default:
return (
-
}>
+
}>
{t('未知')}
);
@@ -405,7 +405,7 @@ const LogsTable = () => {
const color = durationSec > 60 ? 'red' : 'green';
return (
-
}>
+
}>
{durationSec} {t('秒')}
);
@@ -439,7 +439,6 @@ const LogsTable = () => {
}
onClick={() => {
@@ -523,6 +522,7 @@ const LogsTable = () => {
}
return (
{
setModalImageUrl(text);
setIsModalOpenurl(true);
@@ -741,22 +741,13 @@ const LogsTable = () => {
onCancel={() => setShowColumnSelector(false)}
footer={
- initDefaultColumns()}
- >
+ initDefaultColumns()}>
{t('重置')}
- setShowColumnSelector(false)}
- >
+ setShowColumnSelector(false)}>
{t('取消')}
- setShowColumnSelector(false)}
- >
+ setShowColumnSelector(false)}>
{t('确定')}
@@ -831,10 +822,10 @@ const LogsTable = () => {
)}
setCompactMode(!compactMode)}
+ size="small"
>
{compactMode ? t('自适应列表') : t('紧凑列表')}
@@ -864,6 +855,7 @@ const LogsTable = () => {
placeholder={[t('开始时间'), t('结束时间')]}
showClear
pure
+ size="small"
/>
@@ -874,6 +866,7 @@ const LogsTable = () => {
placeholder={t('任务 ID')}
showClear
pure
+ size="small"
/>
{/* 渠道 ID - 仅管理员可见 */}
@@ -884,6 +877,7 @@ const LogsTable = () => {
placeholder={t('渠道 ID')}
showClear
pure
+ size="small"
/>
)}
@@ -893,14 +887,15 @@ const LogsTable = () => {
{t('查询')}
{
if (formApi) {
formApi.reset();
@@ -910,13 +905,14 @@ const LogsTable = () => {
}, 100);
}
}}
+ size="small"
>
{t('重置')}
setShowColumnSelector(true)}
+ size="small"
>
{t('列设置')}
diff --git a/web/src/components/table/ModelPricing.js b/web/src/components/table/ModelPricing.js
index bf9df911073..e3f68a764d0 100644
--- a/web/src/components/table/ModelPricing.js
+++ b/web/src/components/table/ModelPricing.js
@@ -16,7 +16,9 @@ import {
Card,
Tabs,
TabPane,
- Empty
+ Empty,
+ Switch,
+ Select
} from '@douyinfe/semi-ui';
import {
IllustrationNoResult,
@@ -32,6 +34,7 @@ import {
} from '@douyinfe/semi-icons';
import { UserContext } from '../../context/User/index.js';
import { AlertCircle } from 'lucide-react';
+import { StatusContext } from '../../context/Status/index.js';
const ModelPricing = () => {
const { t } = useTranslation();
@@ -44,6 +47,14 @@ const ModelPricing = () => {
const [activeKey, setActiveKey] = useState('all');
const [pageSize, setPageSize] = useState(10);
+ const [currency, setCurrency] = useState('USD');
+ const [showWithRecharge, setShowWithRecharge] = useState(false);
+ const [tokenUnit, setTokenUnit] = useState('M');
+ const [statusState] = useContext(StatusContext);
+ // 充值汇率(price)与美元兑人民币汇率(usd_exchange_rate)
+ const priceRate = useMemo(() => statusState?.status?.price ?? 1, [statusState]);
+ const usdExchangeRate = useMemo(() => statusState?.status?.usd_exchange_rate ?? priceRate, [statusState, priceRate]);
+
const rowSelection = useMemo(
() => ({
onChange: (selectedRowKeys, selectedRows) => {
@@ -76,13 +87,13 @@ const ModelPricing = () => {
switch (type) {
case 1:
return (
-
+
{t('按次计费')}
);
case 0:
return (
-
+
{t('按量计费')}
);
@@ -116,7 +127,6 @@ const ModelPricing = () => {
{endpoint}
@@ -126,6 +136,18 @@ const ModelPricing = () => {
);
}
+ const displayPrice = (usdPrice) => {
+ let priceInUSD = usdPrice;
+ if (showWithRecharge) {
+ priceInUSD = usdPrice * priceRate / usdExchangeRate;
+ }
+
+ if (currency === 'CNY') {
+ return `¥${(priceInUSD * usdExchangeRate).toFixed(3)}`;
+ }
+ return `$${priceInUSD.toFixed(3)}`;
+ };
+
const columns = [
{
title: t('可用性'),
@@ -179,7 +201,7 @@ const ModelPricing = () => {
if (usableGroup[group]) {
if (group === selectedGroup) {
return (
- }>
+ }>
{group}
);
@@ -187,7 +209,6 @@ const ModelPricing = () => {
return (
{
setSelectedGroup(group);
@@ -247,33 +268,54 @@ const ModelPricing = () => {
},
},
{
- title: t('模型价格'),
+ title: (
+
+ {t('模型价格')}
+ {/* 计费单位切换 */}
+ setTokenUnit(checked ? 'K' : 'M')}
+ checkedText="K"
+ uncheckedText="M"
+ />
+
+ ),
dataIndex: 'model_price',
render: (text, record, index) => {
let content = text;
if (record.quota_type === 0) {
- let inputRatioPrice =
- record.model_ratio * 2 * groupRatio[selectedGroup];
- let completionRatioPrice =
- record.model_ratio *
- record.completion_ratio *
- 2 *
- groupRatio[selectedGroup];
+ let inputRatioPriceUSD = record.model_ratio * 2 * groupRatio[selectedGroup];
+ let completionRatioPriceUSD =
+ record.model_ratio * record.completion_ratio * 2 * groupRatio[selectedGroup];
+
+ const unitDivisor = tokenUnit === 'K' ? 1000 : 1;
+ const unitLabel = tokenUnit === 'K' ? 'K' : 'M';
+
+ let displayInput = displayPrice(inputRatioPriceUSD);
+ let displayCompletion = displayPrice(completionRatioPriceUSD);
+
+ const divisor = unitDivisor;
+ const numInput = parseFloat(displayInput.replace(/[^0-9.]/g, '')) / divisor;
+ const numCompletion = parseFloat(displayCompletion.replace(/[^0-9.]/g, '')) / divisor;
+
+ displayInput = `${currency === 'CNY' ? '¥' : '$'}${numInput.toFixed(3)}`;
+ displayCompletion = `${currency === 'CNY' ? '¥' : '$'}${numCompletion.toFixed(3)}`;
content = (
- {t('提示')} ${inputRatioPrice.toFixed(3)} / 1M tokens
+ {t('提示')} {displayInput} / 1{unitLabel} tokens
- {t('补全')} ${completionRatioPrice.toFixed(3)} / 1M tokens
+ {t('补全')} {displayCompletion} / 1{unitLabel} tokens
);
} else {
- let price = parseFloat(text) * groupRatio[selectedGroup];
+ let priceUSD = parseFloat(text) * groupRatio[selectedGroup];
+ let displayVal = displayPrice(priceUSD);
content = (
- {t('模型价格')}:${price.toFixed(3)}
+ {t('模型价格')}:{displayVal}
);
}
@@ -392,7 +434,6 @@ const ModelPricing = () => {
{category.label}
{modelCount}
@@ -436,7 +477,6 @@ const ModelPricing = () => {
onCompositionEnd={handleCompositionEnd}
onChange={handleChange}
showClear
- size="large"
/>
{
onClick={() => copyText(selectedRowKeys)}
disabled={selectedRowKeys.length === 0}
className="!bg-blue-500 hover:!bg-blue-600 text-white"
- size="large"
>
{t('复制选中模型')}
+
+ {/* 充值价格显示开关 */}
+
+ {t('以充值价格显示')}
+
+ {showWithRecharge && (
+
+ )}
+
- ), [selectedRowKeys, t]);
+ ), [selectedRowKeys, t, showWithRecharge, currency]);
const ModelTable = useMemo(() => (
diff --git a/web/src/components/table/RedemptionsTable.js b/web/src/components/table/RedemptionsTable.js
index 9bdb603f476..b463294e83c 100644
--- a/web/src/components/table/RedemptionsTable.js
+++ b/web/src/components/table/RedemptionsTable.js
@@ -53,31 +53,31 @@ const RedemptionsTable = () => {
const renderStatus = (status, record) => {
if (isExpired(record)) {
return (
- {t('已过期')}
+ {t('已过期')}
);
}
switch (status) {
case 1:
return (
-
+
{t('未使用')}
);
case 2:
return (
-
+
{t('已禁用')}
);
case 3:
return (
-
+
{t('已使用')}
);
default:
return (
-
+
{t('未知状态')}
);
@@ -107,7 +107,7 @@ const RedemptionsTable = () => {
render: (text, record, index) => {
return (
-
+
{renderQuota(parseInt(text))}
@@ -139,6 +139,7 @@ const RedemptionsTable = () => {
title: '',
dataIndex: 'operate',
fixed: 'right',
+ width: 205,
render: (text, record, index) => {
// 创建更多操作的下拉菜单项
const moreMenuItems = [
@@ -151,9 +152,15 @@ const RedemptionsTable = () => {
title: t('确定是否要删除此兑换码?'),
content: t('此修改将不可逆'),
onOk: () => {
- manageRedemption(record.id, 'delete', record).then(() => {
- removeRecord(record.key);
- });
+ (async () => {
+ await manageRedemption(record.id, 'delete', record);
+ await refresh();
+ setTimeout(() => {
+ if (redemptions.length === 0 && activePage > 1) {
+ refresh(activePage - 1);
+ }
+ }, 100);
+ })();
},
});
},
@@ -185,7 +192,6 @@ const RedemptionsTable = () => {
@@ -193,8 +199,6 @@ const RedemptionsTable = () => {
{
await copyText(record.key);
@@ -203,7 +207,6 @@ const RedemptionsTable = () => {
{t('复制')}
{
@@ -220,7 +223,6 @@ const RedemptionsTable = () => {
menu={moreMenuItems}
>
}
@@ -320,8 +322,13 @@ const RedemptionsTable = () => {
});
}, [pageSize]);
- const refresh = async () => {
- await loadRedemptions(activePage - 1, pageSize);
+ const refresh = async (page = activePage) => {
+ const { searchKeyword } = getFormValues();
+ if (searchKeyword === '') {
+ await loadRedemptions(page, pageSize);
+ } else {
+ await searchRedemptions(searchKeyword, page, pageSize);
+ }
};
const manageRedemption = async (id, action, record) => {
@@ -424,10 +431,10 @@ const RedemptionsTable = () => {
{t('兑换码可以批量生成和分发,适合用于推广活动或批量充值。')}
setCompactMode(!compactMode)}
+ size="small"
>
{compactMode ? t('自适应列表') : t('紧凑列表')}
@@ -440,7 +447,6 @@ const RedemptionsTable = () => {
{
@@ -449,11 +455,12 @@ const RedemptionsTable = () => {
});
setShowEdit(true);
}}
+ size="small"
>
{t('添加兑换码')}
{
if (selectedKeys.length === 0) {
@@ -467,6 +474,7 @@ const RedemptionsTable = () => {
}
await copyText(keys);
}}
+ size="small"
>
{t('复制所选兑换码到剪贴板')}
@@ -492,6 +500,7 @@ const RedemptionsTable = () => {
},
});
}}
+ size="small"
>
{t('清除失效兑换码')}
@@ -519,23 +528,24 @@ const RedemptionsTable = () => {
placeholder={t('关键字(id或者名称)')}
showClear
pure
+ size="small"
/>
{t('查询')}
{
if (formApi) {
formApi.reset();
- // 重置后立即查询,使用setTimeout确保表单重置完成
setTimeout(() => {
setActivePage(1);
loadRedemptions(1, pageSize);
@@ -543,6 +553,7 @@ const RedemptionsTable = () => {
}
}}
className="flex-1 md:flex-initial md:w-auto"
+ size="small"
>
{t('重置')}
diff --git a/web/src/components/table/TaskLogsTable.js b/web/src/components/table/TaskLogsTable.js
index de41478ec60..ba4dcd506fd 100644
--- a/web/src/components/table/TaskLogsTable.js
+++ b/web/src/components/table/TaskLogsTable.js
@@ -34,7 +34,6 @@ import {
Layout,
Modal,
Progress,
- Skeleton,
Table,
Tag,
Typography
@@ -106,7 +105,7 @@ function renderDuration(submit_time, finishTime) {
// 返回带有样式的颜色标签
return (
-
}>
+
}>
{durationSec} 秒
);
@@ -198,31 +197,31 @@ const LogsTable = () => {
switch (type) {
case 'MUSIC':
return (
-
}>
+
}>
{t('生成音乐')}
);
case 'LYRICS':
return (
-
}>
+
}>
{t('生成歌词')}
);
case TASK_ACTION_GENERATE:
return (
-
}>
+
}>
{t('图生视频')}
);
case TASK_ACTION_TEXT_GENERATE:
return (
-
}>
+
}>
{t('文生视频')}
);
default:
return (
-
}>
+
}>
{t('未知')}
);
@@ -233,25 +232,25 @@ const LogsTable = () => {
switch (platform) {
case 'suno':
return (
-
}>
+
}>
Suno
);
case 'kling':
return (
-
}>
+
}>
Kling
);
case 'jimeng':
return (
-
}>
+
}>
Jimeng
);
default:
return (
-
}>
+
}>
{t('未知')}
);
@@ -262,55 +261,55 @@ const LogsTable = () => {
switch (type) {
case 'SUCCESS':
return (
-
}>
+
}>
{t('成功')}
);
case 'NOT_START':
return (
-
}>
+
}>
{t('未启动')}
);
case 'SUBMITTED':
return (
-
}>
+
}>
{t('队列中')}
);
case 'IN_PROGRESS':
return (
-
}>
+
}>
{t('执行中')}
);
case 'FAILURE':
return (
-
}>
+
}>
{t('失败')}
);
case 'QUEUED':
return (
-
}>
+
}>
{t('排队中')}
);
case 'UNKNOWN':
return (
-
}>
+
}>
{t('未知')}
);
case '':
return (
-
}>
+
}>
{t('正在提交')}
);
default:
return (
-
}>
+
}>
{t('未知')}
);
@@ -596,22 +595,13 @@ const LogsTable = () => {
onCancel={() => setShowColumnSelector(false)}
footer={
- initDefaultColumns()}
- >
+ initDefaultColumns()}>
{t('重置')}
- setShowColumnSelector(false)}
- >
+ setShowColumnSelector(false)}>
{t('取消')}
- setShowColumnSelector(false)}
- >
+ setShowColumnSelector(false)}>
{t('确定')}
@@ -665,23 +655,13 @@ const LogsTable = () => {
- {loading ? (
-
- ) : (
- {t('任务记录')}
- )}
+ {t('任务记录')}
setCompactMode(!compactMode)}
+ size="small"
>
{compactMode ? t('自适应列表') : t('紧凑列表')}
@@ -711,6 +691,7 @@ const LogsTable = () => {
placeholder={[t('开始时间'), t('结束时间')]}
showClear
pure
+ size="small"
/>
@@ -721,6 +702,7 @@ const LogsTable = () => {
placeholder={t('任务 ID')}
showClear
pure
+ size="small"
/>
{/* 渠道 ID - 仅管理员可见 */}
@@ -731,6 +713,7 @@ const LogsTable = () => {
placeholder={t('渠道 ID')}
showClear
pure
+ size="small"
/>
)}
@@ -740,14 +723,15 @@ const LogsTable = () => {
{t('查询')}
{
if (formApi) {
formApi.reset();
@@ -757,13 +741,14 @@ const LogsTable = () => {
}, 100);
}
}}
+ size="small"
>
{t('重置')}
setShowColumnSelector(true)}
+ size="small"
>
{t('列设置')}
diff --git a/web/src/components/table/TokensTable.js b/web/src/components/table/TokensTable.js
index a6d669a626e..ac7fca9271d 100644
--- a/web/src/components/table/TokensTable.js
+++ b/web/src/components/table/TokensTable.js
@@ -7,7 +7,7 @@ import {
timestamp2string,
renderGroup,
renderQuota,
- getQuotaPerUnit
+ getModelCategories
} from '../../helpers';
import { ITEMS_PER_PAGE } from '../../constants';
import {
@@ -22,6 +22,12 @@ import {
SplitButtonGroup,
Table,
Tag,
+ AvatarGroup,
+ Avatar,
+ Tooltip,
+ Progress,
+ Switch,
+ Input,
Typography
} from '@douyinfe/semi-ui';
import {
@@ -31,7 +37,9 @@ import {
import {
IconSearch,
IconTreeTriangleDown,
- IconMore,
+ IconCopy,
+ IconEyeOpened,
+ IconEyeClosed,
} from '@douyinfe/semi-icons';
import { Key } from 'lucide-react';
import EditToken from '../../pages/Token/EditToken';
@@ -47,49 +55,6 @@ function renderTimestamp(timestamp) {
const TokensTable = () => {
const { t } = useTranslation();
- const renderStatus = (status, model_limits_enabled = false) => {
- switch (status) {
- case 1:
- if (model_limits_enabled) {
- return (
-
- {t('已启用:限制模型')}
-
- );
- } else {
- return (
-
- {t('已启用')}
-
- );
- }
- case 2:
- return (
-
- {t('已禁用')}
-
- );
- case 3:
- return (
-
- {t('已过期')}
-
- );
- case 4:
- return (
-
- {t('已耗尽')}
-
- );
- default:
- return (
-
- {t('未知状态')}
-
- );
- }
- };
-
const columns = [
{
title: t('名称'),
@@ -99,64 +64,253 @@ const TokensTable = () => {
title: t('状态'),
dataIndex: 'status',
key: 'status',
- render: (text, record, index) => {
- return (
-
-
- {renderStatus(text, record.model_limits_enabled)}
- {renderGroup(record.group)}
-
+ render: (text, record) => {
+ const enabled = text === 1;
+ const handleToggle = (checked) => {
+ if (checked) {
+ manageToken(record.id, 'enable', record);
+ } else {
+ manageToken(record.id, 'disable', record);
+ }
+ };
+
+ let tagColor = 'black';
+ let tagText = t('未知状态');
+ if (enabled) {
+ tagColor = 'green';
+ tagText = t('已启用');
+ } else if (text === 2) {
+ tagColor = 'red';
+ tagText = t('已禁用');
+ } else if (text === 3) {
+ tagColor = 'yellow';
+ tagText = t('已过期');
+ } else if (text === 4) {
+ tagColor = 'grey';
+ tagText = t('已耗尽');
+ }
+
+ const used = parseInt(record.used_quota) || 0;
+ const remain = parseInt(record.remain_quota) || 0;
+ const total = used + remain;
+ const percent = total > 0 ? (remain / total) * 100 : 0;
+
+ const getProgressColor = (pct) => {
+ if (pct === 100) return 'var(--semi-color-success)';
+ if (pct <= 10) return 'var(--semi-color-danger)';
+ if (pct <= 30) return 'var(--semi-color-warning)';
+ return undefined;
+ };
+
+ const quotaSuffix = record.unlimited_quota ? (
+
{t('无限额度')}
+ ) : (
+
+
{`${renderQuota(remain)} / ${renderQuota(total)}`}
+
);
+
+ const content = (
+
+ }
+ suffixIcon={quotaSuffix}
+ >
+ {tagText}
+
+ );
+
+ if (record.unlimited_quota) {
+ return content;
+ }
+
+ return (
+
+ {t('已用额度')}: {renderQuota(used)}
+ {t('剩余额度')}: {renderQuota(remain)} ({percent.toFixed(0)}%)
+ {t('总额度')}: {renderQuota(total)}
+
+ }
+ >
+ {content}
+
+ );
},
},
{
- title: t('已用额度'),
- dataIndex: 'used_quota',
- render: (text, record, index) => {
+ title: t('分组'),
+ dataIndex: 'group',
+ key: 'group',
+ render: (text) => {
+ if (text === 'auto') {
+ return (
+
+ {t('智能熔断')}
+
+ );
+ }
+ return renderGroup(text);
+ },
+ },
+ {
+ title: t('密钥'),
+ key: 'token_key',
+ render: (text, record) => {
+ const fullKey = 'sk-' + record.key;
+ const maskedKey = 'sk-' + record.key.slice(0, 4) + '**********' + record.key.slice(-4);
+ const revealed = !!showKeys[record.id];
+
return (
-
-
- {renderQuota(parseInt(text))}
-
+
+
+ : }
+ aria-label='toggle token visibility'
+ onClick={(e) => {
+ e.stopPropagation();
+ setShowKeys(prev => ({ ...prev, [record.id]: !revealed }));
+ }}
+ />
+ }
+ aria-label='copy token key'
+ onClick={async (e) => {
+ e.stopPropagation();
+ await copyText(fullKey);
+ }}
+ />
+
+ }
+ />
);
},
},
{
- title: t('剩余额度'),
- dataIndex: 'remain_quota',
- render: (text, record, index) => {
- const getQuotaColor = (quotaValue) => {
- const quotaPerUnit = getQuotaPerUnit();
- const dollarAmount = quotaValue / quotaPerUnit;
-
- if (dollarAmount <= 0) {
- return 'red';
- } else if (dollarAmount <= 100) {
- return 'yellow';
- } else {
- return 'green';
+ title: t('可用模型'),
+ dataIndex: 'model_limits',
+ render: (text, record) => {
+ if (record.model_limits_enabled && text) {
+ const models = text.split(',').filter(Boolean);
+ const categories = getModelCategories(t);
+
+ const vendorAvatars = [];
+ const matchedModels = new Set();
+ Object.entries(categories).forEach(([key, category]) => {
+ if (key === 'all') return;
+ if (!category.icon || !category.filter) return;
+ const vendorModels = models.filter((m) => category.filter({ model_name: m }));
+ if (vendorModels.length > 0) {
+ vendorAvatars.push(
+
+
+ {category.icon}
+
+
+ );
+ vendorModels.forEach((m) => matchedModels.add(m));
+ }
+ });
+
+ const unmatchedModels = models.filter((m) => !matchedModels.has(m));
+ if (unmatchedModels.length > 0) {
+ vendorAvatars.push(
+
+
+ {t('其他')}
+
+
+ );
}
- };
- return (
-
- {record.unlimited_quota ? (
-
- {t('无限制')}
-
- ) : (
-
- {renderQuota(parseInt(text))}
+ return (
+
+ {vendorAvatars}
+
+ );
+ } else {
+ return (
+
+ {t('无限制')}
+
+ );
+ }
+ },
+ },
+ {
+ title: t('IP限制'),
+ dataIndex: 'allow_ips',
+ render: (text) => {
+ if (!text || text.trim() === '') {
+ return (
+
+ {t('无限制')}
+
+ );
+ }
+
+ const ips = text
+ .split('\n')
+ .map((ip) => ip.trim())
+ .filter(Boolean);
+
+ const displayIps = ips.slice(0, 1);
+ const extraCount = ips.length - displayIps.length;
+
+ const ipTags = displayIps.map((ip, idx) => (
+
+ {ip}
+
+ ));
+
+ if (extraCount > 0) {
+ ipTags.push(
+
+
+ {'+' + extraCount}
- )}
-
- );
+
+ );
+ }
+
+ return
{ipTags};
},
},
{
@@ -211,58 +365,6 @@ const TokensTable = () => {
}
}
- // 创建更多操作的下拉菜单项
- const moreMenuItems = [
- {
- node: 'item',
- name: t('查看'),
- onClick: () => {
- Modal.info({
- title: t('令牌详情'),
- content: 'sk-' + record.key,
- size: 'large',
- });
- },
- },
- {
- node: 'item',
- name: t('删除'),
- type: 'danger',
- onClick: () => {
- Modal.confirm({
- title: t('确定是否要删除此令牌?'),
- content: t('此修改将不可逆'),
- onOk: () => {
- manageToken(record.id, 'delete', record).then(() => {
- removeRecord(record.key);
- });
- },
- });
- },
- }
- ];
-
- // 动态添加启用/禁用按钮
- if (record.status === 1) {
- moreMenuItems.push({
- node: 'item',
- name: t('禁用'),
- type: 'warning',
- onClick: () => {
- manageToken(record.id, 'disable', record);
- },
- });
- } else {
- moreMenuItems.push({
- node: 'item',
- name: t('启用'),
- type: 'secondary',
- onClick: () => {
- manageToken(record.id, 'enable', record);
- },
- });
- }
-
return (
{
aria-label={t('项目操作按钮组')}
>
{
if (chatsArray.length === 0) {
showError(t('请联系管理员配置聊天链接'));
@@ -293,11 +394,7 @@ const TokensTable = () => {
menu={chatsArray}
>
}
size="small"
>
@@ -305,18 +402,6 @@ const TokensTable = () => {
{
- await copyText('sk-' + record.key);
- }}
- >
- {t('复制')}
-
-
- {
@@ -327,18 +412,24 @@ const TokensTable = () => {
{t('编辑')}
- {
+ Modal.confirm({
+ title: t('确定是否要删除此令牌?'),
+ content: t('此修改将不可逆'),
+ onOk: () => {
+ (async () => {
+ await manageToken(record.id, 'delete', record);
+ await refresh();
+ })();
+ },
+ });
+ }}
>
- }
- theme='light'
- type='tertiary'
- size="small"
- />
-
+ {t('删除')}
+
);
},
@@ -357,6 +448,7 @@ const TokensTable = () => {
id: undefined,
});
const [compactMode, setCompactMode] = useTableCompactMode('tokens');
+ const [showKeys, setShowKeys] = useState({});
// Form 初始值
const formInitValues = {
@@ -405,8 +497,8 @@ const TokensTable = () => {
setLoading(false);
};
- const refresh = async () => {
- await loadTokens(1);
+ const refresh = async (page = activePage) => {
+ await loadTokens(page);
setSelectedKeys([]);
};
@@ -582,6 +674,11 @@ const TokensTable = () => {
const count = res.data.data || 0;
showSuccess(t('已删除 {{count}} 个令牌!', { count }));
await refresh();
+ setTimeout(() => {
+ if (tokens.length === 0 && activePage > 1) {
+ refresh(activePage - 1);
+ }
+ }, 100);
} else {
showError(res?.data?.message || t('删除失败'));
}
@@ -601,10 +698,10 @@ const TokensTable = () => {
{t('令牌用于API访问认证,可以设置额度限制和模型权限。')}
setCompactMode(!compactMode)}
+ size="small"
>
{compactMode ? t('自适应列表') : t('紧凑列表')}
@@ -616,7 +713,6 @@ const TokensTable = () => {
{
@@ -625,12 +721,12 @@ const TokensTable = () => {
});
setShowEdit(true);
}}
+ size="small"
>
{t('添加令牌')}
{
if (selectedKeys.length === 0) {
@@ -644,8 +740,7 @@ const TokensTable = () => {
footer: (
{
let content = '';
for (let i = 0; i < selectedKeys.length; i++) {
@@ -659,7 +754,6 @@ const TokensTable = () => {
{t('名称+密钥')}
{
let content = '';
for (let i = 0; i < selectedKeys.length; i++) {
@@ -675,12 +769,12 @@ const TokensTable = () => {
),
});
}}
+ size="small"
>
{t('复制所选令牌')}
{
if (selectedKeys.length === 0) {
@@ -697,6 +791,7 @@ const TokensTable = () => {
onOk: () => batchDeleteTokens(),
});
}}
+ size="small"
>
{t('删除所选令牌')}
@@ -721,6 +816,7 @@ const TokensTable = () => {
placeholder={t('搜索关键字')}
showClear
pure
+ size="small"
/>
@@ -730,19 +826,21 @@ const TokensTable = () => {
placeholder={t('密钥')}
showClear
pure
+ size="small"
/>
{t('查询')}
{
if (formApi) {
formApi.reset();
@@ -753,6 +851,7 @@ const TokensTable = () => {
}
}}
className="flex-1 md:flex-initial md:w-auto"
+ size="small"
>
{t('重置')}
diff --git a/web/src/components/table/UsersTable.js b/web/src/components/table/UsersTable.js
index 02a19b80a39..193e1d65716 100644
--- a/web/src/components/table/UsersTable.js
+++ b/web/src/components/table/UsersTable.js
@@ -54,25 +54,25 @@ const UsersTable = () => {
switch (role) {
case 1:
return (
-
}>
+
}>
{t('普通用户')}
);
case 10:
return (
-
}>
+
}>
{t('管理员')}
);
case 100:
return (
-
}>
+
}>
{t('超级管理员')}
);
default:
return (
-
}>
+
}>
{t('未知身份')}
);
@@ -82,16 +82,16 @@ const UsersTable = () => {
const renderStatus = (status) => {
switch (status) {
case 1:
- return
}>{t('已激活')};
+ return
}>{t('已激活')};
case 2:
return (
-
}>
+
}>
{t('已封禁')}
);
default:
return (
-
}>
+
}>
{t('未知状态')}
);
@@ -117,7 +117,7 @@ const UsersTable = () => {
{text}
-
+
{displayRemark}
@@ -142,13 +142,13 @@ const UsersTable = () => {
return (
- }>
+ }>
{t('剩余')}: {renderQuota(record.quota)}
- }>
+ }>
{t('已用')}: {renderQuota(record.used_quota)}
- }>
+ }>
{t('调用')}: {renderNumber(record.request_count)}
@@ -163,13 +163,13 @@ const UsersTable = () => {
return (
- }>
+ }>
{t('邀请')}: {renderNumber(record.aff_count)}
- }>
+ }>
{t('收益')}: {renderQuota(record.aff_history_quota)}
- }>
+ }>
{record.inviter_id === 0 ? t('无邀请人') : `邀请人: ${record.inviter_id}`}
@@ -247,9 +247,15 @@ const UsersTable = () => {
title: t('确定是否要注销此用户?'),
content: t('相当于删除用户,此修改将不可逆'),
onOk: () => {
- manageUser(record.id, 'delete', record).then(() => {
- removeRecord(record.id);
- });
+ (async () => {
+ await manageUser(record.id, 'delete', record);
+ await refresh();
+ setTimeout(() => {
+ if (users.length === 0 && activePage > 1) {
+ refresh(activePage - 1);
+ }
+ }, 100);
+ })();
},
});
},
@@ -281,7 +287,6 @@ const UsersTable = () => {
return (
{
@@ -297,7 +302,6 @@ const UsersTable = () => {
menu={moreMenuItems}
>
}
@@ -459,13 +463,12 @@ const UsersTable = () => {
});
};
- const refresh = async () => {
- setActivePage(1);
+ const refresh = async (page = activePage) => {
const { searchKeyword, searchGroup } = getFormValues();
if (searchKeyword === '' && searchGroup === '') {
- await loadUsers(1, pageSize);
+ await loadUsers(page, pageSize);
} else {
- await searchUsers(1, pageSize, searchKeyword, searchGroup);
+ await searchUsers(page, pageSize, searchKeyword, searchGroup);
}
};
@@ -520,10 +523,10 @@ const UsersTable = () => {
{t('用户管理页面,可以查看和管理所有注册用户的信息、权限和状态。')}
setCompactMode(!compactMode)}
+ size="small"
>
{compactMode ? t('自适应列表') : t('紧凑列表')}
@@ -535,12 +538,11 @@ const UsersTable = () => {
{
setShowAddUser(true);
}}
+ size="small"
>
{t('添加用户')}
@@ -568,6 +570,7 @@ const UsersTable = () => {
placeholder={t('支持搜索用户的 ID、用户名、显示名称和邮箱地址')}
showClear
pure
+ size="small"
/>
@@ -585,23 +588,24 @@ const UsersTable = () => {
className="w-full"
showClear
pure
+ size="small"
/>
{t('查询')}
{
if (formApi) {
formApi.reset();
- // 重置后立即查询,使用setTimeout确保表单重置完成
setTimeout(() => {
setActivePage(1);
loadUsers(1, pageSize);
@@ -609,6 +613,7 @@ const UsersTable = () => {
}
}}
className="flex-1 md:flex-initial md:w-auto"
+ size="small"
>
{t('重置')}
diff --git a/web/src/context/Style/index.js b/web/src/context/Style/index.js
deleted file mode 100644
index 7bfe0ef7087..00000000000
--- a/web/src/context/Style/index.js
+++ /dev/null
@@ -1,227 +0,0 @@
-// contexts/Style/index.js
-
-import React, { useReducer, useEffect, useMemo, createContext } from 'react';
-import { useLocation } from 'react-router-dom';
-import { isMobile as getIsMobile } from '../../helpers';
-
-// Action Types
-const ACTION_TYPES = {
- TOGGLE_SIDER: 'TOGGLE_SIDER',
- SET_SIDER: 'SET_SIDER',
- SET_MOBILE: 'SET_MOBILE',
- SET_SIDER_COLLAPSED: 'SET_SIDER_COLLAPSED',
- BATCH_UPDATE: 'BATCH_UPDATE',
-};
-
-// Constants
-const STORAGE_KEYS = {
- SIDEBAR_COLLAPSED: 'default_collapse_sidebar',
-};
-
-const ROUTE_PATTERNS = {
- CONSOLE: '/console',
-};
-
-/**
- * 判断路径是否为控制台路由
- * @param {string} pathname - 路由路径
- * @returns {boolean} 是否为控制台路由
- */
-const isConsoleRoute = (pathname) => {
- return pathname === ROUTE_PATTERNS.CONSOLE ||
- pathname.startsWith(ROUTE_PATTERNS.CONSOLE + '/');
-};
-
-/**
- * 获取初始状态
- * @param {string} pathname - 当前路由路径
- * @returns {Object} 初始状态对象
- */
-const getInitialState = (pathname) => {
- const isMobile = getIsMobile();
- const isConsole = isConsoleRoute(pathname);
- const isCollapsed = localStorage.getItem(STORAGE_KEYS.SIDEBAR_COLLAPSED) === 'true';
-
- return {
- isMobile,
- showSider: isConsole && !isMobile,
- siderCollapsed: isCollapsed,
- isManualSiderControl: false,
- };
-};
-
-/**
- * Style reducer
- * @param {Object} state - 当前状态
- * @param {Object} action - action 对象
- * @returns {Object} 新状态
- */
-const styleReducer = (state, action) => {
- switch (action.type) {
- case ACTION_TYPES.TOGGLE_SIDER:
- return {
- ...state,
- showSider: !state.showSider,
- isManualSiderControl: true,
- };
-
- case ACTION_TYPES.SET_SIDER:
- return {
- ...state,
- showSider: action.payload,
- isManualSiderControl: action.isManualControl ?? false,
- };
-
- case ACTION_TYPES.SET_MOBILE:
- return {
- ...state,
- isMobile: action.payload,
- };
-
- case ACTION_TYPES.SET_SIDER_COLLAPSED:
- // 自动保存到 localStorage
- localStorage.setItem(STORAGE_KEYS.SIDEBAR_COLLAPSED, action.payload.toString());
- return {
- ...state,
- siderCollapsed: action.payload,
- };
-
- case ACTION_TYPES.BATCH_UPDATE:
- return {
- ...state,
- ...action.payload,
- };
-
- default:
- return state;
- }
-};
-
-// Context (内部使用,不导出)
-const StyleContext = createContext(null);
-
-/**
- * 自定义 Hook - 处理窗口大小变化
- * @param {Function} dispatch - dispatch 函数
- * @param {Object} state - 当前状态
- * @param {string} pathname - 当前路径
- */
-const useWindowResize = (dispatch, state, pathname) => {
- useEffect(() => {
- const handleResize = () => {
- const isMobile = getIsMobile();
- dispatch({ type: ACTION_TYPES.SET_MOBILE, payload: isMobile });
-
- // 只有在非手动控制的情况下,才根据屏幕大小自动调整侧边栏
- if (!state.isManualSiderControl && isConsoleRoute(pathname)) {
- dispatch({
- type: ACTION_TYPES.SET_SIDER,
- payload: !isMobile,
- isManualControl: false
- });
- }
- };
-
- let timeoutId;
- const debouncedResize = () => {
- clearTimeout(timeoutId);
- timeoutId = setTimeout(handleResize, 150);
- };
-
- window.addEventListener('resize', debouncedResize);
- return () => {
- window.removeEventListener('resize', debouncedResize);
- clearTimeout(timeoutId);
- };
- }, [dispatch, state.isManualSiderControl, pathname]);
-};
-
-/**
- * 自定义 Hook - 处理路由变化
- * @param {Function} dispatch - dispatch 函数
- * @param {string} pathname - 当前路径
- */
-const useRouteChange = (dispatch, pathname) => {
- useEffect(() => {
- const isMobile = getIsMobile();
- const isConsole = isConsoleRoute(pathname);
-
- dispatch({
- type: ACTION_TYPES.BATCH_UPDATE,
- payload: {
- showSider: isConsole && !isMobile,
- isManualSiderControl: false,
- },
- });
- }, [pathname, dispatch]);
-};
-
-/**
- * 自定义 Hook - 处理移动设备侧边栏自动收起
- * @param {Object} state - 当前状态
- * @param {Function} dispatch - dispatch 函数
- */
-const useMobileSiderAutoHide = (state, dispatch) => {
- useEffect(() => {
- // 移动设备上,如果不是手动控制且侧边栏是打开的,则自动关闭
- if (state.isMobile && state.showSider && !state.isManualSiderControl) {
- dispatch({ type: ACTION_TYPES.SET_SIDER, payload: false });
- }
- }, [state.isMobile, state.showSider, state.isManualSiderControl, dispatch]);
-};
-
-/**
- * Style Provider 组件
- */
-export const StyleProvider = ({ children }) => {
- const location = useLocation();
- const pathname = location.pathname;
-
- const [state, dispatch] = useReducer(
- styleReducer,
- pathname,
- getInitialState
- );
-
- useWindowResize(dispatch, state, pathname);
- useRouteChange(dispatch, pathname);
- useMobileSiderAutoHide(state, dispatch);
-
- const contextValue = useMemo(
- () => ({ state, dispatch }),
- [state]
- );
-
- return (
-
- {children}
-
- );
-};
-
-/**
- * 自定义 Hook - 使用 StyleContext
- * @returns {{state: Object, dispatch: Function}} context value
- */
-export const useStyle = () => {
- const context = React.useContext(StyleContext);
- if (!context) {
- throw new Error('useStyle must be used within StyleProvider');
- }
- return context;
-};
-
-// 导出 action creators 以便外部使用
-export const styleActions = {
- toggleSider: () => ({ type: ACTION_TYPES.TOGGLE_SIDER }),
- setSider: (show, isManualControl = false) => ({
- type: ACTION_TYPES.SET_SIDER,
- payload: show,
- isManualControl
- }),
- setMobile: (isMobile) => ({ type: ACTION_TYPES.SET_MOBILE, payload: isMobile }),
- setSiderCollapsed: (collapsed) => ({
- type: ACTION_TYPES.SET_SIDER_COLLAPSED,
- payload: collapsed
- }),
-};
diff --git a/web/src/helpers/api.js b/web/src/helpers/api.js
index e00a5bdb705..cad1dd134bf 100644
--- a/web/src/helpers/api.js
+++ b/web/src/helpers/api.js
@@ -59,7 +59,12 @@ export function updateAPI() {
API.interceptors.response.use(
(response) => response,
(error) => {
+ // 如果请求配置中显式要求跳过全局错误处理,则不弹出默认错误提示
+ if (error.config && error.config.skipErrorHandler) {
+ return Promise.reject(error);
+ }
showError(error);
+ return Promise.reject(error);
},
);
@@ -82,8 +87,9 @@ export const buildApiPayload = (messages, systemPrompt, inputs, parameterEnabled
const payload = {
model: inputs.model,
+ group: inputs.group,
messages: processedMessages,
- group: inputs.group,
+ group: inputs.group,
stream: inputs.stream,
};
diff --git a/web/src/helpers/boolean.js b/web/src/helpers/boolean.js
new file mode 100644
index 00000000000..692196e0919
--- /dev/null
+++ b/web/src/helpers/boolean.js
@@ -0,0 +1,10 @@
+export const toBoolean = (value) => {
+ // 兼容字符串、数字以及布尔原生类型
+ if (typeof value === 'boolean') return value;
+ if (typeof value === 'number') return value === 1;
+ if (typeof value === 'string') {
+ const v = value.toLowerCase();
+ return v === 'true' || v === '1';
+ }
+ return false;
+};
\ No newline at end of file
diff --git a/web/src/helpers/index.js b/web/src/helpers/index.js
index 1524afbed48..507a3df1344 100644
--- a/web/src/helpers/index.js
+++ b/web/src/helpers/index.js
@@ -6,3 +6,4 @@ export * from './render';
export * from './log';
export * from './data';
export * from './token';
+export * from './boolean';
diff --git a/web/src/helpers/render.js b/web/src/helpers/render.js
index 6f00b9143a6..34ba78d7a2d 100644
--- a/web/src/helpers/render.js
+++ b/web/src/helpers/render.js
@@ -1,6 +1,7 @@
import i18next from 'i18next';
import { Modal, Tag, Typography } from '@douyinfe/semi-ui';
-import { copy, isMobile, showSuccess } from './utils';
+import { copy, showSuccess } from './utils';
+import { MOBILE_BREAKPOINT } from '../hooks/useIsMobile.js';
import { visit } from 'unist-util-visit';
import {
OpenAI,
@@ -31,6 +32,8 @@ import {
Coze,
SiliconCloud,
FastGPT,
+ Kling,
+ Jimeng,
} from '@lobehub/icons';
import {
@@ -51,18 +54,18 @@ import {
// 侧边栏图标颜色映射
export const sidebarIconColors = {
- dashboard: '#4F46E5', // 紫蓝色
+ dashboard: '#10B981', // 绿色
terminal: '#10B981', // 绿色
message: '#06B6D4', // 青色
key: '#3B82F6', // 蓝色
- chart: '#8B5CF6', // 紫色
+ chart: '#F59E0B', // 琥珀色
image: '#EC4899', // 粉色
check: '#F59E0B', // 琥珀色
credit: '#F97316', // 橙色
layers: '#EF4444', // 红色
gift: '#F43F5E', // 玫红色
- user: '#6366F1', // 靛蓝色
- settings: '#6B7280', // 灰色
+ user: '#10B981', // 绿色
+ settings: '#F97316', // 橙色
};
// 获取侧边栏Lucide图标组件
@@ -386,6 +389,10 @@ export function getChannelIcon(channelType) {
return
;
case 49: // Coze
return
;
+ case 50: // 可灵 Kling
+ return
;
+ case 51: // 即梦 Jimeng
+ return
;
case 8: // 自定义渠道
case 22: // 知识库:FastGPT
return
;
@@ -533,7 +540,7 @@ export function stringToColor(str) {
export function renderModelTag(modelName, options = {}) {
const {
color,
- size = 'large',
+ size = 'default',
shape = 'circle',
onClick,
suffixIcon,
@@ -578,7 +585,7 @@ export function renderText(text, limit) {
export function renderGroup(group) {
if (group === '') {
return (
-
+
{i18next.t('用户分组')}
);
@@ -597,7 +604,6 @@ export function renderGroup(group) {
{groups.map((group) => (
{
return ;
@@ -67,9 +68,7 @@ export async function copy(text) {
return okay;
}
-export function isMobile() {
- return window.innerWidth <= 600;
-}
+// isMobile 函数已移除,请改用 useIsMobile Hook
let showErrorOptions = { autoClose: toastConstants.ERROR_TIMEOUT };
let showWarningOptions = { autoClose: toastConstants.WARNING_TIMEOUT };
@@ -77,7 +76,8 @@ let showSuccessOptions = { autoClose: toastConstants.SUCCESS_TIMEOUT };
let showInfoOptions = { autoClose: toastConstants.INFO_TIMEOUT };
let showNoticeOptions = { autoClose: false };
-if (isMobile()) {
+const isMobileScreen = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`).matches;
+if (isMobileScreen) {
showErrorOptions.position = 'top-center';
// showErrorOptions.transition = 'flip';
diff --git a/web/src/hooks/useIsMobile.js b/web/src/hooks/useIsMobile.js
new file mode 100644
index 00000000000..08f9c5e2b45
--- /dev/null
+++ b/web/src/hooks/useIsMobile.js
@@ -0,0 +1,16 @@
+export const MOBILE_BREAKPOINT = 768;
+
+import { useSyncExternalStore } from 'react';
+
+export const useIsMobile = () => {
+ const query = `(max-width: ${MOBILE_BREAKPOINT - 1}px)`;
+ return useSyncExternalStore(
+ (callback) => {
+ const mql = window.matchMedia(query);
+ mql.addEventListener('change', callback);
+ return () => mql.removeEventListener('change', callback);
+ },
+ () => window.matchMedia(query).matches,
+ () => false,
+ );
+};
\ No newline at end of file
diff --git a/web/src/hooks/useSidebarCollapsed.js b/web/src/hooks/useSidebarCollapsed.js
new file mode 100644
index 00000000000..2982ff9b2da
--- /dev/null
+++ b/web/src/hooks/useSidebarCollapsed.js
@@ -0,0 +1,22 @@
+import { useState, useCallback } from 'react';
+
+const KEY = 'default_collapse_sidebar';
+
+export const useSidebarCollapsed = () => {
+ const [collapsed, setCollapsed] = useState(() => localStorage.getItem(KEY) === 'true');
+
+ const toggle = useCallback(() => {
+ setCollapsed(prev => {
+ const next = !prev;
+ localStorage.setItem(KEY, next.toString());
+ return next;
+ });
+ }, []);
+
+ const set = useCallback((value) => {
+ setCollapsed(value);
+ localStorage.setItem(KEY, value.toString());
+ }, []);
+
+ return [collapsed, toggle, set];
+};
\ No newline at end of file
diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json
index 80f7f3cd231..1ff11e1f67d 100644
--- a/web/src/i18n/locales/en.json
+++ b/web/src/i18n/locales/en.json
@@ -179,7 +179,6 @@
"注销": "Logout",
"登录": "Sign in",
"注册": "Sign up",
- "加载{name}中...": "Loading {name}...",
"未登录或登录已过期,请重新登录!": "Not logged in or session expired. Please login again!",
"用户登录": "User Login",
"密码": "Password",
@@ -373,6 +372,9 @@
"搜索令牌的名称 ...": "Search for the name of the token...",
"已用额度": "Quota used",
"剩余额度": "Remaining quota",
+ "总额度": "Total quota",
+ "智能熔断": "Smart fallback",
+ "当前分组为 auto,会自动选择最优分组,当一个组不可用时自动降级到下一个组(熔断机制)": "The current group is auto, it will automatically select the optimal group, and automatically downgrade to the next group when a group is unavailable (breakage mechanism)",
"过期时间": "Expiration time",
"无": "None",
"无限制": "Unlimited",
@@ -421,6 +423,8 @@
"微信身份验证": "WeChat Authentication",
"Turnstile 用户校验": "Turnstile User Verification",
"创建新的渠道": "Create New Channel",
+ "是否自动禁用": "Whether to automatically disable",
+ "仅当自动禁用开启时有效,关闭后不会自动禁用该渠道": "Only effective when automatic disabling is enabled, after closing, the channel will not be automatically disabled",
"镜像": "Mirror",
"请输入镜像站地址,格式为:https://domain.com,可不填,不填则使用渠道默认值": "Please enter the mirror site address, the format is: https://domain.com, it can be left blank, if left blank, the default value of the channel will be used",
"模型": "Model",
@@ -507,6 +511,7 @@
"创建新的兑换码": "Create a new redemption code",
"未找到所请求的页面": "The requested page was not found",
"过期时间格式错误!": "Expiration time format error!",
+ "过期时间不能早于当前时间!": "Expiration time cannot be earlier than the current time!",
"请输入过期时间,格式为 yyyy-MM-dd HH:mm:ss,-1 表示无限制": "Please enter the expiration time, the format is yyyy-MM-dd HH:mm:ss, -1 means no limit",
"此项可选,为一个 JSON 文本,键为用户请求的模型名称,值为要替换的模型名称,例如:": "This is optional, it's a JSON text, the key is the model name requested by the user, and the value is the model name to be replaced, for example:",
"此项可选,输入镜像站地址,格式为:": "This is optional, enter the mirror site address, the format is:",
@@ -927,7 +932,6 @@
"更新令牌后需等待几分钟生效": "It will take a few minutes to take effect after updating the token.",
"一小时": "One hour",
"新建数量": "New quantity",
- "加载失败,请稍后重试": "Loading failed, please try again later",
"未设置": "Not set",
"API文档": "API documentation",
"不是合法的 JSON 字符串": "Not a valid JSON string",
@@ -960,6 +964,7 @@
"启用突发备用号池(建议勾选,极大降低故障率)": "Enable burst backup number pool (it is recommended to check this box to greatly reduce the failure rate)",
"查看说明": "View instructions",
"添加令牌": "Create token",
+ "IP限制": "IP restrictions",
"令牌纬度控制 Midjouney 配置,设置优先级:令牌 > 路径参数 > 系统默认": "Token latitude controls Midjouney configuration, setting priority: token > path parameter > system default",
"启用速率限制": "Enable rate limiting",
"复制BaseURL": "Copy BaseURL",
@@ -1142,7 +1147,7 @@
"鉴权json": "Authentication JSON",
"请输入鉴权json": "Please enter authentication JSON",
"组织": "Organization",
- "组织,可选,不填则为默认组织": "Organization (optional), default if empty",
+ "组织,不填则为默认组织": "Organization, default if empty",
"请输入组织org-xxx": "Please enter organization org-xxx",
"默认测试模型": "Default Test Model",
"不填则为模型列表第一个": "First model in list if empty",
@@ -1623,6 +1628,7 @@
"请联系管理员在系统设置中配置Uptime": "Please contact the administrator to configure Uptime in the system settings.",
"确定要删除此API信息吗?": "Are you sure you want to delete this API information?",
"测速": "Speed Test",
+ "跳转": "Jump",
"批量删除": "Batch Delete",
"常见问答": "FAQ",
"进行中": "Ongoing",
@@ -1754,7 +1760,26 @@
"批量创建时会在名称后自动添加随机后缀": "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",
- "创建后可在编辑渠道时获取上游模型列表": "After creation, you can get the upstream model list when editing the channel",
"可用端点类型": "Supported endpoint types",
- "未登录,使用默认分组倍率:": "Not logged in, using default group ratio: "
+ "未登录,使用默认分组倍率:": "Not logged in, using default group ratio: ",
+ "该服务器地址将影响支付回调地址以及默认首页展示的地址,请确保正确配置": "This server address will affect the payment callback address and the address displayed on the default homepage, please ensure correct configuration",
+ "密钥聚合模式": "Key aggregation mode",
+ "随机": "Random",
+ "轮询": "Polling",
+ "密钥文件 (.json)": "Key file (.json)",
+ "点击上传文件或拖拽文件到这里": "Click to upload file or drag and drop file here",
+ "仅支持 JSON 文件": "Only JSON files are supported",
+ "仅支持 JSON 文件,支持多文件": "Only JSON files are supported, multiple files are supported",
+ "请上传密钥文件": "Please upload the key file",
+ "请填写部署地区": "Please fill in the deployment region",
+ "请输入部署地区,例如:us-central1\n支持使用模型映射格式\n{\n \"default\": \"us-central1\",\n \"claude-3-5-sonnet-20240620\": \"europe-west1\"\n}": "Please enter the deployment region, for example: us-central1\nSupports using model mapping format\n{\n \"default\": \"us-central1\",\n \"claude-3-5-sonnet-20240620\": \"europe-west1\"\n}",
+ "其他": "Other",
+ "未知渠道": "Unknown channel",
+ "切换为单密钥模式": "Switch to single key mode",
+ "将仅保留第一个密钥文件,其余文件将被移除,是否继续?": "Only the first key file will be retained, and the remaining files will be removed. Continue?",
+ "自定义模型名称": "Custom model name",
+ "启用全部密钥": "Enable all keys",
+ "以充值价格显示": "Show with recharge price",
+ "美元汇率(非充值汇率,仅用于定价页面换算)": "USD exchange rate (not recharge rate, only used for pricing page conversion)",
+ "美元汇率": "USD exchange rate"
}
\ No newline at end of file
diff --git a/web/src/index.css b/web/src/index.css
index 8e71536a7b8..791b853e3dd 100644
--- a/web/src/index.css
+++ b/web/src/index.css
@@ -14,6 +14,22 @@
}
/* ==================== 全局基础样式 ==================== */
+/* 侧边栏宽度相关的 CSS 变量,配合 .sidebar-collapsed 类和媒体查询实现响应式布局 */
+:root {
+ --sidebar-width: 180px;
+ /* 展开时宽度 */
+ --sidebar-width-collapsed: 60px; /* 折叠后宽度,显示图标栏 */
+ /* 折叠后宽度 */
+ --sidebar-current-width: var(--sidebar-width);
+}
+
+/* 当 body 上存在 .sidebar-collapsed 类时,使用折叠宽度 */
+body.sidebar-collapsed {
+ --sidebar-current-width: var(--sidebar-width-collapsed);
+}
+
+/* 移除了在移动端强制设为 0 的限制,改由 React 控制是否渲染侧边栏以实现显示/隐藏 */
+
body {
font-family: Lato, 'Helvetica Neue', Arial, Helvetica, 'Microsoft YaHei', sans-serif;
color: var(--semi-color-text-0);
@@ -59,6 +75,11 @@ code {
.semi-navigation-item {
margin-bottom: 4px !important;
+ padding: 4px 12px !important;
+}
+
+.semi-navigation-sub-title {
+ padding: 0 !important;
}
.semi-navigation-item-icon {
@@ -70,6 +91,7 @@ code {
margin-right: 0;
}
+.semi-navigation-item,
.semi-navigation-sub-title {
height: 100% !important;
}
diff --git a/web/src/index.js b/web/src/index.js
index ef299ea2c29..2a09702378e 100644
--- a/web/src/index.js
+++ b/web/src/index.js
@@ -6,11 +6,18 @@ import { UserProvider } from './context/User';
import 'react-toastify/dist/ReactToastify.css';
import { StatusProvider } from './context/Status';
import { ThemeProvider } from './context/Theme';
-import { StyleProvider } from './context/Style/index.js';
import PageLayout from './components/layout/PageLayout.js';
import './i18n/i18n.js';
import './index.css';
+// 欢迎信息(二次开发者不准将此移除)
+// Welcome message (Secondary developers are not allowed to remove this)
+if (typeof window !== 'undefined') {
+ console.log('%cWe ❤ NewAPI%c Github: https://github.com/QuantumNous/new-api',
+ 'color: #10b981; font-weight: bold; font-size: 24px;',
+ 'color: inherit; font-size: 14px;');
+}
+
// initialization
const root = ReactDOM.createRoot(document.getElementById('root'));
@@ -18,11 +25,14 @@ root.render(
-
+
-
-
-
+
diff --git a/web/src/pages/About/index.js b/web/src/pages/About/index.js
index 032562cabe3..5ca96d97825 100644
--- a/web/src/pages/About/index.js
+++ b/web/src/pages/About/index.js
@@ -105,7 +105,7 @@ const About = () => {
);
return (
-
+
{aboutLoaded && about === '' ? (
{
const channelId = props.editingChannel.id;
const isEdit = channelId !== undefined;
const [loading, setLoading] = useState(isEdit);
+ const isMobile = useIsMobile();
const handleCancel = () => {
props.handleClose();
};
@@ -100,10 +103,12 @@ const EditChannel = (props) => {
priority: 0,
weight: 0,
tag: '',
+ multi_key_mode: 'random',
};
const [batch, setBatch] = useState(false);
+ const [multiToSingle, setMultiToSingle] = useState(false);
+ const [multiKeyMode, setMultiKeyMode] = useState('random');
const [autoBan, setAutoBan] = useState(true);
- // const [autoBan, setAutoBan] = useState(true);
const [inputs, setInputs] = useState(originInputs);
const [originModelOptions, setOriginModelOptions] = useState([]);
const [modelOptions, setModelOptions] = useState([]);
@@ -114,6 +119,12 @@ const EditChannel = (props) => {
const [modalImageUrl, setModalImageUrl] = useState('');
const [isModalOpenurl, setIsModalOpenurl] = useState(false);
const formApiRef = useRef(null);
+ const [vertexKeys, setVertexKeys] = useState([]);
+ const [vertexFileList, setVertexFileList] = useState([]);
+ const vertexErroredNames = useRef(new Set()); // 避免重复报错
+ const [isMultiKeyChannel, setIsMultiKeyChannel] = useState(false);
+ const [channelSearchValue, setChannelSearchValue] = useState('');
+ const [useManualInput, setUseManualInput] = useState(false); // 是否使用手动输入模式
const getInitValues = () => ({ ...originInputs });
const handleInputChange = (name, value) => {
if (formApiRef.current) {
@@ -182,6 +193,9 @@ const EditChannel = (props) => {
setInputs((inputs) => ({ ...inputs, models: localModels }));
}
setBasicModels(localModels);
+
+ // 重置手动输入模式状态
+ setUseManualInput(false);
}
//setAutoBan
};
@@ -211,6 +225,19 @@ const EditChannel = (props) => {
2,
);
}
+ const chInfo = data.channel_info || {};
+ const isMulti = chInfo.is_multi_key === true;
+ setIsMultiKeyChannel(isMulti);
+ if (isMulti) {
+ setBatch(true);
+ setMultiToSingle(true);
+ const modeVal = chInfo.multi_key_mode || 'random';
+ setMultiKeyMode(modeVal);
+ data.multi_key_mode = modeVal;
+ } else {
+ setBatch(false);
+ setMultiToSingle(false);
+ }
setInputs(data);
if (formApiRef.current) {
formApiRef.current.setValues(data);
@@ -238,9 +265,9 @@ const EditChannel = (props) => {
let err = false;
if (isEdit) {
- // 如果是编辑模式,使用已有的channel id获取模型列表
- const res = await API.get('/api/channel/fetch_models/' + channelId);
- if (res.data && res.data.success) {
+ // 如果是编辑模式,使用已有的 channelId 获取模型列表
+ const res = await API.get('/api/channel/fetch_models/' + channelId, { skipErrorHandler: true });
+ if (res && res.data && res.data.success) {
models.push(...res.data.data);
} else {
err = true;
@@ -252,13 +279,17 @@ const EditChannel = (props) => {
err = true;
} else {
try {
- const res = await API.post('/api/channel/fetch_models', {
- base_url: inputs['base_url'],
- type: inputs['type'],
- key: inputs['key'],
- });
+ const res = await API.post(
+ '/api/channel/fetch_models',
+ {
+ base_url: inputs['base_url'],
+ type: inputs['type'],
+ key: inputs['key'],
+ },
+ { skipErrorHandler: true },
+ );
- if (res.data && res.data.success) {
+ if (res && res.data && res.data.success) {
models.push(...res.data.data);
} else {
err = true;
@@ -324,14 +355,14 @@ const EditChannel = (props) => {
useEffect(() => {
const modelMap = new Map();
- originModelOptions.forEach(option => {
+ originModelOptions.forEach((option) => {
const v = (option.value || '').trim();
if (!modelMap.has(v)) {
modelMap.set(v, option);
}
});
- inputs.models.forEach(model => {
+ inputs.models.forEach((model) => {
const v = (model || '').trim();
if (!modelMap.has(v)) {
modelMap.set(v, {
@@ -342,8 +373,29 @@ const EditChannel = (props) => {
}
});
- setModelOptions(Array.from(modelMap.values()));
- }, [originModelOptions, inputs.models]);
+ const categories = getModelCategories(t);
+ const optionsWithIcon = Array.from(modelMap.values()).map((opt) => {
+ const modelName = opt.value;
+ let icon = null;
+ for (const [key, category] of Object.entries(categories)) {
+ if (key !== 'all' && category.filter({ model_name: modelName })) {
+ icon = category.icon;
+ break;
+ }
+ }
+ return {
+ ...opt,
+ label: (
+
+ {icon}
+ {modelName}
+
+ ),
+ };
+ });
+
+ setModelOptions(optionsWithIcon);
+ }, [originModelOptions, inputs.models, t]);
useEffect(() => {
fetchModels().then();
@@ -372,15 +424,122 @@ const EditChannel = (props) => {
} else {
formApiRef.current?.setValues(getInitValues());
}
+ // 重置手动输入模式状态
+ setUseManualInput(false);
} else {
formApiRef.current?.reset();
}
}, [props.visible, channelId]);
+ const handleVertexUploadChange = ({ fileList }) => {
+ vertexErroredNames.current.clear();
+ (async () => {
+ let validFiles = [];
+ let keys = [];
+ const errorNames = [];
+ for (const item of fileList) {
+ const fileObj = item.fileInstance;
+ if (!fileObj) continue;
+ try {
+ const txt = await fileObj.text();
+ keys.push(JSON.parse(txt));
+ validFiles.push(item);
+ } catch (err) {
+ if (!vertexErroredNames.current.has(item.name)) {
+ errorNames.push(item.name);
+ vertexErroredNames.current.add(item.name);
+ }
+ }
+ }
+
+ // 非批量模式下只保留一个文件(最新选择的),避免重复叠加
+ if (!batch && validFiles.length > 1) {
+ validFiles = [validFiles[validFiles.length - 1]];
+ keys = [keys[keys.length - 1]];
+ }
+
+ setVertexKeys(keys);
+ setVertexFileList(validFiles);
+ if (formApiRef.current) {
+ formApiRef.current.setValue('vertex_files', validFiles);
+ }
+ setInputs((prev) => ({ ...prev, vertex_files: validFiles }));
+
+ if (errorNames.length > 0) {
+ showError(t('以下文件解析失败,已忽略:{{list}}', { list: errorNames.join(', ') }));
+ }
+ })();
+ };
+
const submit = async () => {
const formValues = formApiRef.current ? formApiRef.current.getValues() : {};
let localInputs = { ...formValues };
+ if (localInputs.type === 41) {
+ if (useManualInput) {
+ // 手动输入模式
+ if (localInputs.key && localInputs.key.trim() !== '') {
+ try {
+ // 验证 JSON 格式
+ const parsedKey = JSON.parse(localInputs.key);
+ // 确保是有效的密钥格式
+ localInputs.key = JSON.stringify(parsedKey);
+ } catch (err) {
+ showError(t('密钥格式无效,请输入有效的 JSON 格式密钥'));
+ return;
+ }
+ } else if (!isEdit) {
+ showInfo(t('请输入密钥!'));
+ return;
+ }
+ } else {
+ // 文件上传模式
+ let keys = vertexKeys;
+
+ // 若当前未选择文件,尝试从已上传文件列表解析(异步读取)
+ if (keys.length === 0 && vertexFileList.length > 0) {
+ try {
+ const parsed = await Promise.all(
+ vertexFileList.map(async (item) => {
+ const fileObj = item.fileInstance;
+ if (!fileObj) return null;
+ const txt = await fileObj.text();
+ return JSON.parse(txt);
+ })
+ );
+ keys = parsed.filter(Boolean);
+ } catch (err) {
+ showError(t('解析密钥文件失败: {{msg}}', { msg: err.message }));
+ return;
+ }
+ }
+
+ // 创建模式必须上传密钥;编辑模式可选
+ if (keys.length === 0) {
+ if (!isEdit) {
+ showInfo(t('请上传密钥文件!'));
+ return;
+ } else {
+ // 编辑模式且未上传新密钥,不修改 key
+ delete localInputs.key;
+ }
+ } else {
+ // 有新密钥,则覆盖
+ if (batch) {
+ localInputs.key = JSON.stringify(keys);
+ } else {
+ localInputs.key = JSON.stringify(keys[0]);
+ }
+ }
+ }
+ }
+
+ // 如果是编辑模式且 key 为空字符串,避免提交空值覆盖旧密钥
+ if (isEdit && (!localInputs.key || localInputs.key.trim() === '')) {
+ delete localInputs.key;
+ }
+ delete localInputs.vertex_files;
+
if (!isEdit && (!localInputs.name || !localInputs.key)) {
showInfo(t('请填写渠道名称和渠道密钥!'));
return;
@@ -406,13 +565,23 @@ const EditChannel = (props) => {
localInputs.auto_ban = localInputs.auto_ban ? 1 : 0;
localInputs.models = localInputs.models.join(',');
localInputs.group = (localInputs.groups || []).join(',');
+
+ let mode = 'single';
+ if (batch) {
+ mode = multiToSingle ? 'multi_to_single' : 'batch';
+ }
+
if (isEdit) {
res = await API.put(`/api/channel/`, {
...localInputs,
id: parseInt(channelId),
});
} else {
- res = await API.post(`/api/channel/`, localInputs);
+ res = await API.post(`/api/channel/`, {
+ mode: mode,
+ multi_key_mode: mode === 'multi_to_single' ? multiKeyMode : undefined,
+ channel: localInputs,
+ });
}
const { success, message } = res.data;
if (success) {
@@ -465,11 +634,141 @@ const EditChannel = (props) => {
}
};
- const batchAllowed = !isEdit && inputs.type !== 41;
+ const batchAllowed = !isEdit || isMultiKeyChannel;
const batchExtra = batchAllowed ? (
- setBatch(!batch)}>{t('批量创建')}
+
+ {
+ const checked = e.target.checked;
+
+ if (!checked && vertexFileList.length > 1) {
+ Modal.confirm({
+ title: t('切换为单密钥模式'),
+ content: t('将仅保留第一个密钥文件,其余文件将被移除,是否继续?'),
+ onOk: () => {
+ const firstFile = vertexFileList[0];
+ const firstKey = vertexKeys[0] ? [vertexKeys[0]] : [];
+
+ setVertexFileList([firstFile]);
+ setVertexKeys(firstKey);
+
+ formApiRef.current?.setValue('vertex_files', [firstFile]);
+ setInputs((prev) => ({ ...prev, vertex_files: [firstFile] }));
+
+ setBatch(false);
+ setMultiToSingle(false);
+ setMultiKeyMode('random');
+ },
+ onCancel: () => {
+ setBatch(true);
+ },
+ centered: true,
+ });
+ return;
+ }
+
+ setBatch(checked);
+ if (!checked) {
+ setMultiToSingle(false);
+ setMultiKeyMode('random');
+ } else {
+ // 批量模式下禁用手动输入,并清空手动输入的内容
+ setUseManualInput(false);
+ if (inputs.type === 41) {
+ // 清空手动输入的密钥内容
+ if (formApiRef.current) {
+ formApiRef.current.setValue('key', '');
+ }
+ handleInputChange('key', '');
+ }
+ }
+ }}
+ >{t('批量创建')}
+ {/*{batch && (*/}
+ {/* {*/}
+ {/* setMultiToSingle(prev => !prev);*/}
+ {/* setInputs(prev => {*/}
+ {/* const newInputs = { ...prev };*/}
+ {/* if (!multiToSingle) {*/}
+ {/* newInputs.multi_key_mode = multiKeyMode;*/}
+ {/* } else {*/}
+ {/* delete newInputs.multi_key_mode;*/}
+ {/* }*/}
+ {/* return newInputs;*/}
+ {/* });*/}
+ {/* }}>{t('密钥聚合模式')}*/}
+ {/*)}*/}
+
) : null;
+ const channelOptionList = useMemo(
+ () =>
+ CHANNEL_OPTIONS.map((opt) => ({
+ ...opt,
+ // 保持 label 为纯文本以支持搜索
+ label: opt.label,
+ })),
+ [],
+ );
+
+ const renderChannelOption = (renderProps) => {
+ const {
+ disabled,
+ selected,
+ label,
+ value,
+ focused,
+ className,
+ style,
+ onMouseEnter,
+ onClick,
+ ...rest
+ } = renderProps;
+
+ const searchWords = channelSearchValue ? [channelSearchValue] : [];
+
+ // 构建样式类名
+ const optionClassName = [
+ 'flex items-center gap-3 px-3 py-2 transition-all duration-200 rounded-lg mx-2 my-1',
+ focused && 'bg-blue-50 shadow-sm',
+ selected && 'bg-blue-100 text-blue-700 shadow-lg ring-2 ring-blue-200 ring-opacity-50',
+ disabled && 'opacity-50 cursor-not-allowed',
+ !disabled && 'hover:bg-gray-50 hover:shadow-md cursor-pointer',
+ className
+ ].filter(Boolean).join(' ');
+
+ return (
+ !disabled && onClick()}
+ onMouseEnter={e => onMouseEnter()}
+ >
+
+
+ {getChannelIcon(value)}
+
+
+
+
+ {selected && (
+
+ )}
+
+
+ );
+ };
+
return (
<>
{
}
bodyStyle={{ padding: '0' }}
visible={props.visible}
- width={isMobile() ? '100%' : 600}
+ width={isMobile ? '100%' : 600}
footer={
@@ -535,10 +834,12 @@ const EditChannel = (props) => {
label={t('类型')}
placeholder={t('请选择渠道类型')}
rules={[{ required: true, message: t('请选择渠道类型') }]}
- optionList={CHANNEL_OPTIONS}
+ optionList={channelOptionList}
style={{ width: '100%' }}
filter
searchPosition='dropdown'
+ onSearch={(value) => setChannelSearchValue(value)}
+ renderOptionItem={renderChannelOption}
onChange={(value) => handleInputChange('type', value)}
/>
@@ -553,56 +854,239 @@ const EditChannel = (props) => {
/>
{batch ? (
- handleInputChange('key', value)}
- extraText={batchExtra}
- />
+ inputs.type === 41 ? (
+ }
+ dragMainText={t('点击上传文件或拖拽文件到这里')}
+ dragSubText={t('仅支持 JSON 文件,支持多文件')}
+ style={{ marginTop: 10 }}
+ uploadTrigger='custom'
+ beforeUpload={() => false}
+ onChange={handleVertexUploadChange}
+ fileList={vertexFileList}
+ rules={isEdit ? [] : [{ required: true, message: t('请上传密钥文件') }]}
+ extraText={batchExtra}
+ />
+ ) : (
+ handleInputChange('key', value)}
+ extraText={batchExtra}
+ showClear
+ />
+ )
) : (
<>
{inputs.type === 41 ? (
- handleInputChange('key', value)}
- extraText={batchExtra}
- />
+ <>
+ {!batch && (
+
+ {t('密钥输入方式')}
+
+ {
+ setUseManualInput(false);
+ // 切换到文件上传模式时清空手动输入的密钥
+ if (formApiRef.current) {
+ formApiRef.current.setValue('key', '');
+ }
+ handleInputChange('key', '');
+ }}
+ >
+ {t('文件上传')}
+
+ {
+ setUseManualInput(true);
+ // 切换到手动输入模式时清空文件上传相关状态
+ setVertexKeys([]);
+ setVertexFileList([]);
+ if (formApiRef.current) {
+ formApiRef.current.setValue('vertex_files', []);
+ }
+ setInputs((prev) => ({ ...prev, vertex_files: [] }));
+ }}
+ >
+ {t('手动输入')}
+
+
+
+ )}
+
+ {batch && (
+
+ )}
+
+ {useManualInput && !batch ? (
+ handleInputChange('key', value)}
+ extraText={
+
+
+ {t('请输入完整的 JSON 格式密钥内容')}
+
+ {batchExtra}
+
+ }
+ autosize
+ showClear
+ />
+ ) : (
+ }
+ dragMainText={t('点击上传文件或拖拽文件到这里')}
+ dragSubText={t('仅支持 JSON 文件')}
+ style={{ marginTop: 10 }}
+ uploadTrigger='custom'
+ beforeUpload={() => false}
+ onChange={handleVertexUploadChange}
+ fileList={vertexFileList}
+ rules={isEdit ? [] : [{ required: true, message: t('请上传密钥文件') }]}
+ extraText={batchExtra}
+ />
+ )}
+ >
) : (
handleInputChange('key', value)}
extraText={batchExtra}
+ showClear
/>
)}
>
)}
+
+ {batch && multiToSingle && (
+ <>
+ {
+ setMultiKeyMode(value);
+ handleInputChange('multi_key_mode', value);
+ }}
+ />
+ {inputs.multi_key_mode === 'polling' && (
+
+ )}
+ >
+ )}
+
+ {inputs.type === 18 && (
+ handleInputChange('other', value)}
+ showClear
+ />
+ )}
+
+ {inputs.type === 41 && (
+ handleInputChange('other', value)}
+ rules={[{ required: true, message: t('请填写部署地区') }]}
+ extraText={
+ handleInputChange('other', JSON.stringify(REGION_EXAMPLE, null, 2))}
+ >
+ {t('填入模板')}
+
+ }
+ showClear
+ />
+ )}
+
+ {inputs.type === 21 && (
+ handleInputChange('other', value)}
+ showClear
+ />
+ )}
+
+ {inputs.type === 39 && (
+ handleInputChange('other', value)}
+ showClear
+ />
+ )}
+
+ {inputs.type === 49 && (
+ handleInputChange('other', value)}
+ showClear
+ />
+ )}
+
+ {inputs.type === 1 && (
+ handleInputChange('openai_organization', value)}
+ />
+ )}
{/* API Configuration Card */}
@@ -747,7 +1231,7 @@ const EditChannel = (props) => {
{
handleInputChange('models', fullModels)}>
{t('填入所有模型')}
- {isEdit && (
- fetchUpstreamModelList('models')}>
- {t('获取模型列表')}
-
- )}
+ fetchUpstreamModelList('models')}>
+ {t('获取模型列表')}
+
handleInputChange('models', [])}>
{t('清除所有模型')}
@@ -860,77 +1342,6 @@ const EditChannel = (props) => {
onChange={(value) => handleInputChange('groups', value)}
/>
- {inputs.type === 18 && (
- handleInputChange('other', value)}
- showClear
- />
- )}
-
- {inputs.type === 41 && (
- handleInputChange('other', value)}
- extraText={
- handleInputChange('other', JSON.stringify(REGION_EXAMPLE, null, 2))}
- >
- {t('填入模板')}
-
- }
- />
- )}
-
- {inputs.type === 21 && (
- handleInputChange('other', value)}
- showClear
- />
- )}
-
- {inputs.type === 39 && (
- handleInputChange('other', value)}
- showClear
- />
- )}
-
- {inputs.type === 49 && (
- handleInputChange('other', value)}
- showClear
- />
- )}
-
- {inputs.type === 1 && (
- handleInputChange('openai_organization', value)}
- />
- )}
-
{
return (
-
+
);
diff --git a/web/src/pages/Chat2Link/index.js b/web/src/pages/Chat2Link/index.js
index 28a48535d9d..30ec785a927 100644
--- a/web/src/pages/Chat2Link/index.js
+++ b/web/src/pages/Chat2Link/index.js
@@ -17,7 +17,7 @@ const chat2page = () => {
}
return (
-
+
正在加载,请稍候...
);
diff --git a/web/src/pages/Detail/index.js b/web/src/pages/Detail/index.js
index f455abb69d4..b5553cbf9c5 100644
--- a/web/src/pages/Detail/index.js
+++ b/web/src/pages/Detail/index.js
@@ -1,14 +1,14 @@
import React, { useContext, useEffect, useRef, useState, useMemo, useCallback } from 'react';
import { initVChartSemiTheme } from '@visactor/vchart-semi-theme';
import { useNavigate } from 'react-router-dom';
-import { Wallet, Activity, Zap, Gauge, PieChart, Server, Bell, HelpCircle } from 'lucide-react';
+import { Wallet, Activity, Zap, Gauge, PieChart, Server, Bell, HelpCircle, ExternalLink } from 'lucide-react';
import { marked } from 'marked';
import {
Card,
Form,
Spin,
- IconButton,
+ Button,
Modal,
Avatar,
Tabs,
@@ -18,14 +18,14 @@ import {
Timeline,
Collapse,
Progress,
- Divider
+ Divider,
+ Skeleton
} from '@douyinfe/semi-ui';
import {
IconRefresh,
IconSearch,
IconMoneyExchangeStroked,
IconHistogram,
- IconRotate,
IconCoinMoneyStroked,
IconTextStroked,
IconPulse,
@@ -33,15 +33,17 @@ import {
IconTypograph,
IconPieChart2Stroked,
IconPlus,
- IconMinus
+ IconMinus,
+ IconSend
} from '@douyinfe/semi-icons';
import { IllustrationConstruction, IllustrationConstructionDark } from '@douyinfe/semi-illustrations';
import { VChart } from '@visactor/react-vchart';
import {
API,
isAdmin,
- isMobile,
showError,
+ showSuccess,
+ showWarning,
timestamp2string,
timestamp2string1,
getQuotaWithUnit,
@@ -50,9 +52,9 @@ import {
renderQuota,
modelToColor,
copy,
- showSuccess,
getRelativeTime
} from '../../helpers';
+import { useIsMobile } from '../../hooks/useIsMobile.js';
import { UserContext } from '../../context/User/index.js';
import { StatusContext } from '../../context/Status/index.js';
import { useTranslation } from 'react-i18next';
@@ -65,6 +67,7 @@ const Detail = (props) => {
// ========== Hooks - Navigation & Translation ==========
const { t } = useTranslation();
const navigate = useNavigate();
+ const isMobile = useIsMobile();
// ========== Hooks - Refs ==========
const formRef = useRef();
@@ -192,6 +195,7 @@ const Detail = (props) => {
const [dataExportDefaultTime, setDataExportDefaultTime] = useState(getDefaultTime());
const [loading, setLoading] = useState(false);
+ const [greetingVisible, setGreetingVisible] = useState(false);
const [quotaData, setQuotaData] = useState([]);
const [consumeQuota, setConsumeQuota] = useState(0);
const [consumeTokens, setConsumeTokens] = useState(0);
@@ -449,7 +453,7 @@ const Detail = (props) => {
// ========== Hooks - Memoized Values ==========
const performanceMetrics = useMemo(() => {
const timeDiff = (Date.parse(end_timestamp) - Date.parse(start_timestamp)) / 60000;
- const avgRPM = (times / timeDiff).toFixed(3);
+ const avgRPM = isNaN(times / timeDiff) ? '0' : (times / timeDiff).toFixed(3);
const avgTPM = isNaN(consumeTokens / timeDiff) ? '0' : (consumeTokens / timeDiff).toFixed(3);
return { avgRPM, avgTPM, timeDiff };
@@ -518,7 +522,7 @@ const Detail = (props) => {
{
title: t('当前余额'),
value: renderQuota(userState?.user?.quota),
- icon:
,
+ icon:
,
avatarColor: 'blue',
onClick: () => navigate('/console/topup'),
trendData: [],
@@ -527,7 +531,7 @@ const Detail = (props) => {
{
title: t('历史消耗'),
value: renderQuota(userState?.user?.used_quota),
- icon:
,
+ icon:
,
avatarColor: 'purple',
trendData: [],
trendColor: '#8b5cf6'
@@ -541,7 +545,7 @@ const Detail = (props) => {
{
title: t('请求次数'),
value: userState.user?.request_count,
- icon:
,
+ icon:
,
avatarColor: 'green',
trendData: [],
trendColor: '#10b981'
@@ -549,7 +553,7 @@ const Detail = (props) => {
{
title: t('统计次数'),
value: times,
- icon:
,
+ icon:
,
avatarColor: 'cyan',
trendData: trendData.times,
trendColor: '#06b6d4'
@@ -563,7 +567,7 @@ const Detail = (props) => {
{
title: t('统计额度'),
value: renderQuota(consumeQuota),
- icon:
,
+ icon:
,
avatarColor: 'yellow',
trendData: trendData.consumeQuota,
trendColor: '#f59e0b'
@@ -571,7 +575,7 @@ const Detail = (props) => {
{
title: t('统计Tokens'),
value: isNaN(consumeTokens) ? 0 : consumeTokens,
- icon:
,
+ icon:
,
avatarColor: 'pink',
trendData: trendData.tokens,
trendColor: '#ec4899'
@@ -585,7 +589,7 @@ const Detail = (props) => {
{
title: t('平均RPM'),
value: performanceMetrics.avgRPM,
- icon:
,
+ icon:
,
avatarColor: 'indigo',
trendData: trendData.rpm,
trendColor: '#6366f1'
@@ -593,7 +597,7 @@ const Detail = (props) => {
{
title: t('平均TPM'),
value: performanceMetrics.avgTPM,
- icon:
,
+ icon:
,
avatarColor: 'orange',
trendData: trendData.tpm,
trendColor: '#f97316'
@@ -614,7 +618,7 @@ const Detail = (props) => {
const handleSpeedTest = useCallback((apiUrl) => {
const encodedUrl = encodeURIComponent(apiUrl);
const speedTestUrl = `https://www.tcptest.cn/http/${encodedUrl}`;
- window.open(speedTestUrl, '_blank');
+ window.open(speedTestUrl, '_blank', 'noopener,noreferrer');
}, []);
const handleInputChange = useCallback((value, name) => {
@@ -627,6 +631,7 @@ const Detail = (props) => {
const loadQuotaData = useCallback(async () => {
setLoading(true);
+ const startTime = Date.now();
try {
let url = '';
let localStartTimestamp = Date.parse(start_timestamp) / 1000;
@@ -654,7 +659,11 @@ const Detail = (props) => {
showError(message);
}
} finally {
- setLoading(false);
+ const elapsed = Date.now() - startTime;
+ const remainingTime = Math.max(0, 500 - elapsed);
+ setTimeout(() => {
+ setLoading(false);
+ }, remainingTime);
}
}, [start_timestamp, end_timestamp, username, dataExportDefaultTime, isAdminUser]);
@@ -746,6 +755,13 @@ const Detail = (props) => {
return () => clearTimeout(timer);
}, [uptimeData, activeUptimeTab]);
+ useEffect(() => {
+ const timer = setTimeout(() => {
+ setGreetingVisible(true);
+ }, 100);
+ return () => clearTimeout(timer);
+ }, []);
+
const getUserData = async () => {
let res = await API.get(`/api/user/self`);
const { success, message, data } = res.data;
@@ -1104,16 +1120,23 @@ const Detail = (props) => {
}, []);
return (
-
+
-
{getGreeting}
+
+ {getGreeting}
+
-
}
onClick={showSearchModal}
className={`bg-green-500 hover:bg-green-600 ${ICON_BUTTON_CLASS}`}
/>
-
}
onClick={refresh}
loading={loading}
@@ -1129,7 +1152,7 @@ const Detail = (props) => {
onOk={handleSearchConfirm}
onCancel={handleCloseModal}
closeOnEsc={true}
- size={isMobile() ? 'full-width' : 'small'}
+ size={isMobile ? 'full-width' : 'small'}
centered
>
-
-
-
- {groupedStatsData.map((group, idx) => (
-
-
- {group.items.map((item, itemIdx) => (
-
-
-
- {item.icon}
-
-
-
{item.title}
-
{item.value}
+
+
+ {groupedStatsData.map((group, idx) => (
+
+
+ {group.items.map((item, itemIdx) => (
+
+
+
+ {item.icon}
+
+
+
{item.title}
+
+
+ }
+ >
+ {item.value}
+
- {item.trendData && item.trendData.length > 0 && (
-
-
-
- )}
- ))}
-
-
- ))}
-
+ {(loading || (item.trendData && item.trendData.length > 0)) && (
+
+
+
+ )}
+
+ ))}
+
+
+ ))}
+
-
-
+
+
+
+
+
+
+
+ {t('消耗分布')}
+
+ } itemKey="1" />
+
+
+ {t('消耗趋势')}
+
+ } itemKey="2" />
+
+
+ {t('调用次数分布')}
+
+ } itemKey="3" />
+
+
+ {t('调用次数排行')}
+
+ } itemKey="4" />
+
+
+ }
+ bodyStyle={{ padding: 0 }}
+ >
+
+ {activeChartTab === '1' && (
+
+ )}
+ {activeChartTab === '2' && (
+
+ )}
+ {activeChartTab === '3' && (
+
+ )}
+ {activeChartTab === '4' && (
+
+ )}
+
+
+
+ {hasApiInfoPanel && (
-
-
-
-
- {t('消耗分布')}
-
- } itemKey="1" />
-
-
- {t('消耗趋势')}
-
- } itemKey="2" />
-
-
- {t('调用次数分布')}
-
- } itemKey="3" />
-
-
- {t('调用次数排行')}
-
- } itemKey="4" />
-
+
+
+ {t('API信息')}
}
+ bodyStyle={{ padding: 0 }}
>
-
- {activeChartTab === '1' && (
-
- )}
- {activeChartTab === '2' && (
-
- )}
- {activeChartTab === '3' && (
-
- )}
- {activeChartTab === '4' && (
-
- )}
-
-
-
- {hasApiInfoPanel && (
-
-
- {t('API信息')}
-
- }
- >
-
-
- {apiInfoData.length > 0 ? (
- apiInfoData.map((api) => (
+
+
+ {apiInfoData.length > 0 ? (
+ apiInfoData.map((api) => (
+ <>
-
-
}
- size="small"
- color="white"
- shape='circle'
- onClick={() => handleSpeedTest(api.url)}
- className="cursor-pointer hover:opacity-80 text-xs"
- >
- {t('测速')}
-
- {api.route}
+
+
+ {api.route}
+
+
+ }
+ size="small"
+ color="white"
+ shape='circle'
+ onClick={() => handleSpeedTest(api.url)}
+ className="cursor-pointer hover:opacity-80 text-xs"
+ >
+ {t('测速')}
+
+ }
+ size="small"
+ color="white"
+ shape='circle'
+ onClick={() => window.open(api.url, '_blank', 'noopener,noreferrer')}
+ className="cursor-pointer hover:opacity-80 text-xs"
+ >
+ {t('跳转')}
+
+
{
- ))
- ) : (
-
- }
- darkModeImage={}
- title={t('暂无API信息')}
- description={t('请联系管理员在系统设置中配置API信息')}
- />
-
- )}
-
-
+
+ >
+ ))
+ ) : (
+
+ }
+ darkModeImage={}
+ title={t('暂无API信息')}
+ description={t('请联系管理员在系统设置中配置API信息')}
+ />
+
+ )}
-
- )}
-
+
+
+
+ )}
+
- {/* 系统公告和常见问答卡片 */}
- {hasInfoPanels && (
+ {/* 系统公告和常见问答卡片 */}
+ {
+ hasInfoPanels && (
{/* 公告卡片 */}
@@ -1381,7 +1437,7 @@ const Detail = (props) => {
{t('系统公告')}
-
+
{t('显示最新20条')}
@@ -1405,6 +1461,7 @@ const Detail = (props) => {
}
+ bodyStyle={{ padding: 0 }}
>
{
{uptimeEnabled && (
{t('服务可用性')}
- }
onClick={loadUptimeData}
loading={uptimeLoading}
size="small"
theme="borderless"
+ type='tertiary'
className="text-gray-500 hover:text-blue-500 hover:bg-blue-50 !rounded-full"
/>
@@ -1533,7 +1591,7 @@ const Detail = (props) => {
bodyStyle={{ padding: 0 }}
>
{/* 内容区域 */}
-
+
{uptimeData.length > 0 ? (
uptimeData.length === 1 ? (
@@ -1613,9 +1671,9 @@ const Detail = (props) => {
- {/* 固定在底部的图例 */}
+ {/* 图例 */}
{uptimeData.length > 0 && (
-
+
{uptimeLegendData.map((legend, index) => (
@@ -1633,9 +1691,9 @@ const Detail = (props) => {
)}
- )}
-
-
+ )
+ }
+
);
};
diff --git a/web/src/pages/Home/index.js b/web/src/pages/Home/index.js
index f2db769c1ce..0cf8dea1401 100644
--- a/web/src/pages/Home/index.js
+++ b/web/src/pages/Home/index.js
@@ -1,6 +1,7 @@
import React, { useContext, useEffect, useState } from 'react';
import { Button, Typography, Tag, Input, ScrollList, ScrollItem } from '@douyinfe/semi-ui';
-import { API, showError, isMobile, copy, showSuccess } from '../../helpers';
+import { API, showError, copy, showSuccess } from '../../helpers';
+import { useIsMobile } from '../../hooks/useIsMobile.js';
import { API_ENDPOINTS } from '../../constants/common.constant';
import { StatusContext } from '../../context/Status';
import { marked } from 'marked';
@@ -18,6 +19,7 @@ const Home = () => {
const [homePageContentLoaded, setHomePageContentLoaded] = useState(false);
const [homePageContent, setHomePageContent] = useState('');
const [noticeVisible, setNoticeVisible] = useState(false);
+ const isMobile = useIsMobile();
const isDemoSiteMode = statusState?.status?.demo_site_enabled || false;
const docsLink = statusState?.status?.docs_link || '';
const serverAddress = statusState?.status?.server_address || window.location.origin;
@@ -98,7 +100,7 @@ const Home = () => {
setNoticeVisible(false)}
- isMobile={isMobile()}
+ isMobile={isMobile}
/>
{homePageContentLoaded && homePageContent === '' ? (
@@ -133,7 +135,7 @@ const Home = () => {
readonly
value={serverAddress}
className="flex-1 !rounded-full"
- size={isMobile() ? 'default' : 'large'}
+ size={isMobile ? 'default' : 'large'}
suffix={
@@ -160,13 +162,13 @@ const Home = () => {
{/* 操作按钮 */}
-
}>
+
}>
{t('获取密钥')}
{isDemoSiteMode && statusState?.status?.version ? (
}
onClick={() => window.open('https://github.com/QuantumNous/new-api', '_blank')}
@@ -176,7 +178,7 @@ const Home = () => {
) : (
docsLink && (
}
onClick={() => window.open(docsLink, '_blank')}
diff --git a/web/src/pages/Log/index.js b/web/src/pages/Log/index.js
index beec5dd9bb5..74c570bb9aa 100644
--- a/web/src/pages/Log/index.js
+++ b/web/src/pages/Log/index.js
@@ -2,7 +2,7 @@ import React from 'react';
import LogsTable from '../../components/table/LogsTable';
const Token = () => (
-
+
);
diff --git a/web/src/pages/Midjourney/index.js b/web/src/pages/Midjourney/index.js
index 3b0b9e59964..71d4c3a8e70 100644
--- a/web/src/pages/Midjourney/index.js
+++ b/web/src/pages/Midjourney/index.js
@@ -2,7 +2,7 @@ import React from 'react';
import MjLogsTable from '../../components/table/MjLogsTable';
const Midjourney = () => (
-
+
);
diff --git a/web/src/pages/Playground/index.js b/web/src/pages/Playground/index.js
index e310b904b68..9a41bc1812e 100644
--- a/web/src/pages/Playground/index.js
+++ b/web/src/pages/Playground/index.js
@@ -5,7 +5,7 @@ import { Layout, Toast, Modal } from '@douyinfe/semi-ui';
// Context
import { UserContext } from '../../context/User/index.js';
-import { useStyle, styleActions } from '../../context/Style/index.js';
+import { useIsMobile } from '../../hooks/useIsMobile.js';
// hooks
import { usePlaygroundState } from '../../hooks/usePlaygroundState.js';
@@ -59,7 +59,8 @@ const generateAvatarDataUrl = (username) => {
const Playground = () => {
const { t } = useTranslation();
const [userState] = useContext(UserContext);
- const { state: styleState, dispatch: styleDispatch } = useStyle();
+ const isMobile = useIsMobile();
+ const styleState = { isMobile };
const [searchParams] = useSearchParams();
const state = usePlaygroundState();
@@ -321,19 +322,7 @@ const Playground = () => {
}
}, [searchParams, t]);
- // 处理窗口大小变化
- useEffect(() => {
- const handleResize = () => {
- const mobile = window.innerWidth < 768;
- if (styleState.isMobile !== mobile) {
- styleDispatch(styleActions.setMobile(mobile));
- }
- };
-
- handleResize();
- window.addEventListener('resize', handleResize);
- return () => window.removeEventListener('resize', handleResize);
- }, [styleState.isMobile, styleDispatch]);
+ // Playground 组件无需再监听窗口变化,isMobile 由 useIsMobile Hook 自动更新
// 构建预览payload
useEffect(() => {
@@ -365,26 +354,26 @@ const Playground = () => {
return (
- {(showSettings || !styleState.isMobile) && (
+ {(showSettings || !isMobile) && (
{
{/* 调试面板 - 桌面端 */}
- {showDebugPanel && !styleState.isMobile && (
+ {showDebugPanel && !isMobile && (
{
{/* 调试面板 - 移动端覆盖层 */}
- {showDebugPanel && styleState.isMobile && (
+ {showDebugPanel && isMobile && (
(
-
+
);
diff --git a/web/src/pages/Redemption/EditRedemption.js b/web/src/pages/Redemption/EditRedemption.js
index 4a1f8a07e44..44d17e62506 100644
--- a/web/src/pages/Redemption/EditRedemption.js
+++ b/web/src/pages/Redemption/EditRedemption.js
@@ -3,12 +3,12 @@ import { useTranslation } from 'react-i18next';
import {
API,
downloadTextAsFile,
- isMobile,
showError,
showSuccess,
renderQuota,
renderQuotaWithPrompt,
} from '../../helpers';
+import { useIsMobile } from '../../hooks/useIsMobile.js';
import {
Button,
Modal,
@@ -36,6 +36,7 @@ const EditRedemption = (props) => {
const { t } = useTranslation();
const isEdit = props.editingRedemption.id !== undefined;
const [loading, setLoading] = useState(isEdit);
+ const isMobile = useIsMobile();
const formApiRef = useRef(null);
const getInitValues = () => ({
@@ -155,7 +156,7 @@ const EditRedemption = (props) => {
}
bodyStyle={{ padding: '0' }}
visible={props.visiable}
- width={isMobile() ? '100%' : 600}
+ width={isMobile ? '100%' : 600}
footer={
diff --git a/web/src/pages/Redemption/index.js b/web/src/pages/Redemption/index.js
index 3cc289754c3..b55f8fdcbce 100644
--- a/web/src/pages/Redemption/index.js
+++ b/web/src/pages/Redemption/index.js
@@ -3,7 +3,7 @@ import RedemptionsTable from '../../components/table/RedemptionsTable';
const Redemption = () => {
return (
-
+
);
diff --git a/web/src/pages/Setting/Operation/SettingsGeneral.js b/web/src/pages/Setting/Operation/SettingsGeneral.js
index cc774ef84e8..3ca9c37796a 100644
--- a/web/src/pages/Setting/Operation/SettingsGeneral.js
+++ b/web/src/pages/Setting/Operation/SettingsGeneral.js
@@ -26,6 +26,7 @@ export default function GeneralSettings(props) {
'general_setting.docs_link': '',
QuotaPerUnit: '',
RetryTimes: '',
+ USDExchangeRate: '',
DisplayInCurrencyEnabled: false,
DisplayTokenStatEnabled: false,
DefaultCollapseSidebar: false,
@@ -129,6 +130,16 @@ export default function GeneralSettings(props) {
onClick={() => setShowQuotaWarning(true)}
/>
+
+
+
{t('更新服务器地址')}
diff --git a/web/src/pages/Setting/Payment/SettingsPaymentGatewayStripe.js b/web/src/pages/Setting/Payment/SettingsPaymentGatewayStripe.js
new file mode 100644
index 00000000000..4c4a1af6d64
--- /dev/null
+++ b/web/src/pages/Setting/Payment/SettingsPaymentGatewayStripe.js
@@ -0,0 +1,195 @@
+import React, { useEffect, useState, useRef } from 'react';
+import {
+ Banner,
+ Button,
+ Form,
+ Row,
+ Col,
+ Typography,
+ Spin,
+} from '@douyinfe/semi-ui';
+const { Text } = Typography;
+import {
+ API,
+ removeTrailingSlash,
+ showError,
+ showSuccess,
+} from '../../../helpers';
+import { useTranslation } from 'react-i18next';
+
+export default function SettingsPaymentGateway(props) {
+ const { t } = useTranslation();
+ const [loading, setLoading] = useState(false);
+ const [inputs, setInputs] = useState({
+ StripeApiSecret: '',
+ StripeWebhookSecret: '',
+ StripePriceId: '',
+ StripeUnitPrice: 8.0,
+ StripeMinTopUp: 1,
+ });
+ const [originInputs, setOriginInputs] = useState({});
+ const formApiRef = useRef(null);
+
+ useEffect(() => {
+ if (props.options && formApiRef.current) {
+ const currentInputs = {
+ StripeApiSecret: props.options.StripeApiSecret || '',
+ StripeWebhookSecret: props.options.StripeWebhookSecret || '',
+ StripePriceId: props.options.StripePriceId || '',
+ StripeUnitPrice: props.options.StripeUnitPrice !== undefined ? parseFloat(props.options.StripeUnitPrice) : 8.0,
+ StripeMinTopUp: props.options.StripeMinTopUp !== undefined ? parseFloat(props.options.StripeMinTopUp) : 1,
+ };
+ setInputs(currentInputs);
+ setOriginInputs({ ...currentInputs });
+ formApiRef.current.setValues(currentInputs);
+ }
+ }, [props.options]);
+
+ const handleFormChange = (values) => {
+ setInputs(values);
+ };
+
+ const submitStripeSetting = async () => {
+ if (props.options.ServerAddress === '') {
+ showError(t('请先填写服务器地址'));
+ return;
+ }
+
+ setLoading(true);
+ try {
+ const options = []
+
+ if (inputs.StripeApiSecret && inputs.StripeApiSecret !== '') {
+ options.push({ key: 'StripeApiSecret', value: inputs.StripeApiSecret });
+ }
+ if (inputs.StripeWebhookSecret && inputs.StripeWebhookSecret !== '') {
+ options.push({ key: 'StripeWebhookSecret', value: inputs.StripeWebhookSecret });
+ }
+ if (inputs.StripePriceId !== '') {
+ options.push({key: 'StripePriceId', value: inputs.StripePriceId,});
+ }
+ if (inputs.StripeUnitPrice !== undefined && inputs.StripeUnitPrice !== null) {
+ options.push({ key: 'StripeUnitPrice', value: inputs.StripeUnitPrice.toString() });
+ }
+ if (inputs.StripeMinTopUp !== undefined && inputs.StripeMinTopUp !== null) {
+ options.push({ key: 'StripeMinTopUp', value: inputs.StripeMinTopUp.toString() });
+ }
+
+ // 发送请求
+ const requestQueue = options.map(opt =>
+ API.put('/api/option/', {
+ key: opt.key,
+ value: opt.value,
+ })
+ );
+
+ const results = await Promise.all(requestQueue);
+
+ // 检查所有请求是否成功
+ const errorResults = results.filter(res => !res.data.success);
+ if (errorResults.length > 0) {
+ errorResults.forEach(res => {
+ showError(res.data.message);
+ });
+ } else {
+ showSuccess(t('更新成功'));
+ // 更新本地存储的原始值
+ setOriginInputs({ ...inputs });
+ props.refresh?.();
+ }
+ } catch (error) {
+ showError(t('更新失败'));
+ }
+ setLoading(false);
+ };
+
+ return (
+
+
+
+ Stripe 密钥、Webhook 等设置请
+
+ 点击此处
+
+ 进行设置,最好先在
+
+ 测试环境
+
+ 进行测试。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {t('更新 Stripe 设置')}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/web/src/pages/Setting/Ratio/UpstreamRatioSync.js b/web/src/pages/Setting/Ratio/UpstreamRatioSync.js
index ad9c537983f..647ca75802e 100644
--- a/web/src/pages/Setting/Ratio/UpstreamRatioSync.js
+++ b/web/src/pages/Setting/Ratio/UpstreamRatioSync.js
@@ -18,7 +18,8 @@ import {
AlertTriangle,
CheckCircle,
} from 'lucide-react';
-import { API, showError, showSuccess, showWarning, stringToColor, isMobile } from '../../../helpers';
+import { API, showError, showSuccess, showWarning, stringToColor } from '../../../helpers';
+import { useIsMobile } from '../../../hooks/useIsMobile.js';
import { DEFAULT_ENDPOINT } from '../../../constants';
import { useTranslation } from 'react-i18next';
import {
@@ -28,6 +29,7 @@ import {
import ChannelSelectorModal from '../../../components/settings/ChannelSelectorModal';
function ConflictConfirmModal({ t, visible, items, onOk, onCancel }) {
+ const isMobile = useIsMobile();
const columns = [
{ title: t('渠道'), dataIndex: 'channel' },
{ title: t('模型'), dataIndex: 'model' },
@@ -49,7 +51,7 @@ function ConflictConfirmModal({ t, visible, items, onOk, onCancel }) {
visible={visible}
onCancel={onCancel}
onOk={onOk}
- size={isMobile() ? 'full-width' : 'large'}
+ size={isMobile ? 'full-width' : 'large'}
>
@@ -61,6 +63,7 @@ export default function UpstreamRatioSync(props) {
const [modalVisible, setModalVisible] = useState(false);
const [loading, setLoading] = useState(false);
const [syncLoading, setSyncLoading] = useState(false);
+ const isMobile = useIsMobile();
// 渠道选择相关
const [allChannels, setAllChannels] = useState([]);
diff --git a/web/src/pages/Setting/index.js b/web/src/pages/Setting/index.js
index 1fae3516b98..43907826bc9 100644
--- a/web/src/pages/Setting/index.js
+++ b/web/src/pages/Setting/index.js
@@ -150,7 +150,7 @@ const Setting = () => {
}
}, [location.search]);
return (
-
+
{
title={
{t('数据库警告')}
-
+
SQLite
@@ -222,7 +222,7 @@ const Setup = () => {
title={
{t('数据库信息')}
-
+
MySQL
@@ -256,7 +256,7 @@ const Setup = () => {
title={
{t('数据库信息')}
-
+
PostgreSQL
@@ -425,7 +425,7 @@ const Setup = () => {
{t('对外运营模式')}
{t('适用于为多个用户提供服务的场景')}
-
+
{t('默认模式')}
@@ -443,7 +443,7 @@ const Setup = () => {
{t('自用模式')}
{t('适用于个人使用的场景,不需要设置模型价格')}
-
+
{t('无需计费')}
@@ -461,7 +461,7 @@ const Setup = () => {
{t('演示站点模式')}
{t('适用于展示系统功能的场景,提供基础功能演示')}
-
+
{t('演示体验')}
@@ -522,8 +522,8 @@ const Setup = () => {
{t('默认模式,适用于为多个用户提供服务的场景。')}
{t('此模式下,系统将计算每次调用的用量,您需要对每个模型都设置价格,如果没有设置价格,用户将无法使用该模型。')}
- {t('计费模式')}
- {t('多用户支持')}
+ {t('计费模式')}
+ {t('多用户支持')}
@@ -542,8 +542,8 @@ const Setup = () => {
{t('适用于个人使用的场景。')}
{t('不需要设置模型价格,系统将弱化用量计算,您可专注于使用模型。')}
- {t('无需计费')}
- {t('个人使用')}
+ {t('无需计费')}
+ {t('个人使用')}
@@ -562,8 +562,8 @@ const Setup = () => {
{t('适用于展示系统功能的场景。')}
{t('提供基础功能演示,方便用户了解系统特性。')}
- {t('功能演示')}
- {t('体验试用')}
+ {t('功能演示')}
+ {t('体验试用')}
diff --git a/web/src/pages/Task/index.js b/web/src/pages/Task/index.js
index 7cceb3f1c42..4e3a9af49a4 100644
--- a/web/src/pages/Task/index.js
+++ b/web/src/pages/Task/index.js
@@ -2,7 +2,7 @@ import React from 'react';
import TaskLogsTable from '../../components/table/TaskLogsTable.js';
const Task = () => (
-
+
);
diff --git a/web/src/pages/Token/EditToken.js b/web/src/pages/Token/EditToken.js
index fb8badf7667..4eb9bcf4584 100644
--- a/web/src/pages/Token/EditToken.js
+++ b/web/src/pages/Token/EditToken.js
@@ -1,13 +1,14 @@
import React, { useEffect, useState, useContext, useRef } from 'react';
import {
API,
- isMobile,
showError,
showSuccess,
timestamp2string,
renderGroupOption,
renderQuotaWithPrompt,
+ getModelCategories,
} from '../../helpers';
+import { useIsMobile } from '../../hooks/useIsMobile.js';
import {
Button,
SideSheet,
@@ -37,6 +38,7 @@ const EditToken = (props) => {
const { t } = useTranslation();
const [statusState, statusDispatch] = useContext(StatusContext);
const [loading, setLoading] = useState(false);
+ const isMobile = useIsMobile();
const formApiRef = useRef(null);
const [models, setModels] = useState([]);
const [groups, setGroups] = useState([]);
@@ -78,10 +80,25 @@ const EditToken = (props) => {
let res = await API.get(`/api/user/models`);
const { success, message, data } = res.data;
if (success) {
- let localModelOptions = data.map((model) => ({
- label: model,
- value: model,
- }));
+ const categories = getModelCategories(t);
+ let localModelOptions = data.map((model) => {
+ let icon = null;
+ for (const [key, category] of Object.entries(categories)) {
+ if (key !== 'all' && category.filter({ model_name: model })) {
+ icon = category.icon;
+ break;
+ }
+ }
+ return {
+ label: (
+
+ {icon}
+ {model}
+
+ ),
+ value: model,
+ };
+ });
setModels(localModelOptions);
} else {
showError(t(message));
@@ -261,7 +278,7 @@ const EditToken = (props) => {
}
bodyStyle={{ padding: '0' }}
visible={props.visiable}
- width={isMobile() ? '100%' : 600}
+ width={isMobile ? '100%' : 600}
footer={
@@ -345,7 +362,23 @@ const EditToken = (props) => {
label={t('过期时间')}
type='dateTime'
placeholder={t('请选择过期时间')}
- rules={[{ required: true, message: t('请选择过期时间') }]}
+ rules={[
+ { required: true, message: t('请选择过期时间') },
+ {
+ validator: (rule, value) => {
+ // 允许 -1 表示永不过期,也允许空值在必填校验时被拦截
+ if (value === -1 || !value) return Promise.resolve();
+ const time = Date.parse(value);
+ if (isNaN(time)) {
+ return Promise.reject(t('过期时间格式错误!'));
+ }
+ if (time <= Date.now()) {
+ return Promise.reject(t('过期时间不能早于当前时间!'));
+ }
+ return Promise.resolve();
+ },
+ },
+ ]}
showClear
style={{ width: '100%' }}
/>
@@ -453,18 +486,6 @@ const EditToken = (props) => {
-
-
-
{
placeholder={t('请选择该令牌支持的模型,留空支持所有模型')}
multiple
optionList={models}
- maxTagCount={3}
extraText={t('非必要,不建议启用模型限制')}
+ filter
+ searchPosition='dropdown'
+ showClear
+ style={{ width: '100%' }}
+ />
+
+
+
diff --git a/web/src/pages/Token/index.js b/web/src/pages/Token/index.js
index 4c03767de0c..33921eb68ce 100644
--- a/web/src/pages/Token/index.js
+++ b/web/src/pages/Token/index.js
@@ -3,7 +3,7 @@ import TokensTable from '../../components/table/TokensTable';
const Token = () => {
return (
-
+
);
diff --git a/web/src/pages/TopUp/index.js b/web/src/pages/TopUp/index.js
index dc986077650..a7ac6ba6b29 100644
--- a/web/src/pages/TopUp/index.js
+++ b/web/src/pages/TopUp/index.js
@@ -59,6 +59,13 @@ const TopUp = () => {
statusState?.status?.enable_online_topup || false,
);
const [priceRatio, setPriceRatio] = useState(statusState?.status?.price || 1);
+
+ const [stripeAmount, setStripeAmount] = useState(0.0);
+ const [stripeMinTopUp, setStripeMinTopUp] = useState(statusState?.status?.stripe_min_topup || 1);
+ const [stripeTopUpCount, setStripeTopUpCount] = useState(statusState?.status?.stripe_min_topup || 1);
+ const [enableStripeTopUp, setEnableStripeTopUp] = useState(statusState?.status?.enable_stripe_topup || false);
+ const [stripeOpen, setStripeOpen] = useState(false);
+
const [userQuota, setUserQuota] = useState(0);
const [isSubmitting, setIsSubmitting] = useState(false);
const [open, setOpen] = useState(false);
@@ -161,6 +168,7 @@ const TopUp = () => {
showError(t('管理员未开启在线充值!'));
return;
}
+ setPayWay(payment);
setPaymentLoading(true);
try {
await getAmount();
@@ -168,7 +176,6 @@ const TopUp = () => {
showError(t('充值数量不能小于') + minTopUp);
return;
}
- setPayWay(payment);
setOpen(true);
} catch (error) {
showError(t('获取金额失败'));
@@ -186,7 +193,6 @@ const TopUp = () => {
return;
}
setConfirmLoading(true);
- setOpen(false);
try {
const res = await API.post('/api/user/pay', {
amount: parseInt(topUpCount),
@@ -227,8 +233,67 @@ const TopUp = () => {
console.log(err);
showError(t('支付请求失败'));
} finally {
+ setOpen(false);
+ setConfirmLoading(false);
+ }
+ };
+
+ const stripePreTopUp = async () => {
+ if (!enableStripeTopUp) {
+ showError(t('管理员未开启在线充值!'));
+ return;
+ }
+ setPayWay('stripe');
+ setPaymentLoading(true);
+ try {
+ await getStripeAmount();
+ if (stripeTopUpCount < stripeMinTopUp) {
+ showError(t('充值数量不能小于') + stripeMinTopUp);
+ return;
+ }
+ setStripeOpen(true);
+ } catch (error) {
+ showError(t('获取金额失败'));
+ } finally {
+ setPaymentLoading(false);
+ }
+ };
+
+ const onlineStripeTopUp = async () => {
+ if (stripeAmount === 0) {
+ await getStripeAmount();
+ }
+ if (stripeTopUpCount < stripeMinTopUp) {
+ showError(t('充值数量不能小于') + stripeMinTopUp);
+ return;
+ }
+ setConfirmLoading(true);
+ try {
+ const res = await API.post('/api/user/stripe/pay', {
+ amount: parseInt(stripeTopUpCount),
+ payment_method: 'stripe',
+ });
+ if (res !== undefined) {
+ const { message, data } = res.data;
+ if (message === 'success') {
+ processStripeCallback(data);
+ } else {
+ showError(data);
+ }
+ } else {
+ showError(res);
+ }
+ } catch (err) {
+ console.log(err);
+ showError(t('支付请求失败'));
+ } finally {
+ setStripeOpen(false);
setConfirmLoading(false);
}
+ }
+
+ const processStripeCallback = (data) => {
+ window.open(data.pay_link, '_blank');
};
const getUserQuota = async () => {
@@ -327,6 +392,10 @@ const TopUp = () => {
setTopUpLink(statusState.status.top_up_link || '');
setEnableOnlineTopUp(statusState.status.enable_online_topup || false);
setPriceRatio(statusState.status.price || 1);
+
+ setStripeMinTopUp(statusState.status.stripe_min_topup || 1);
+ setStripeTopUpCount(statusState.status.stripe_min_topup || 1);
+ setEnableStripeTopUp(statusState.status.enable_stripe_topup || false);
}
}, [statusState?.status]);
@@ -334,6 +403,10 @@ const TopUp = () => {
return amount + ' ' + t('元');
};
+ const renderStripeAmount = () => {
+ return stripeAmount + ' ' + t('元');
+ };
+
const getAmount = async (value) => {
if (value === undefined) {
value = topUpCount;
@@ -361,10 +434,42 @@ const TopUp = () => {
setAmountLoading(false);
};
+ const getStripeAmount = async (value) => {
+ if (value === undefined) {
+ value = stripeTopUpCount
+ }
+ setAmountLoading(true);
+ try {
+ const res = await API.post('/api/user/stripe/amount', {
+ amount: parseFloat(value),
+ });
+ if (res !== undefined) {
+ const { message, data } = res.data;
+ // showInfo(message);
+ if (message === 'success') {
+ setStripeAmount(parseFloat(data));
+ } else {
+ setStripeAmount(0);
+ Toast.error({ content: '错误:' + data, id: 'getAmount' });
+ }
+ } else {
+ showError(res);
+ }
+ } catch (err) {
+ console.log(err);
+ } finally {
+ setAmountLoading(false);
+ }
+ }
+
const handleCancel = () => {
setOpen(false);
};
+ const handleStripeCancel = () => {
+ setStripeOpen(false);
+ };
+
const handleTransferCancel = () => {
setOpenTransfer(false);
};
@@ -374,6 +479,9 @@ const TopUp = () => {
setTopUpCount(preset.value);
setSelectedPreset(preset.value);
setAmount(preset.value * priceRatio);
+
+ setStripeTopUpCount(preset.value);
+ setStripeAmount(preset.value);
};
// 格式化大数字显示
@@ -496,6 +604,25 @@ const TopUp = () => {
+
+
+ {t('充值数量')}:{stripeTopUpCount}
+
+
+ {t('实付金额')}:{renderStripeAmount()}
+
+ {t('是否确认充值?')}
+
+
{/* 左侧充值区域 */}
@@ -798,7 +925,7 @@ const TopUp = () => {
>
)}
- {!enableOnlineTopUp && (
+ {!enableOnlineTopUp && !enableStripeTopUp && (
{
/>
)}
+ {enableStripeTopUp && (
+ <>
+ {/* 桌面端显示的自定义金额和支付按钮 */}
+
+
+
+ {t(!enableOnlineTopUp ? '或输入自定义金额' : 'Stripe')}
+
+
+
+
+
+ {t('充值数量')}
+ {amountLoading ? (
+
+ ) : (
+
+ {t('实付金额:') + renderStripeAmount()}
+
+ )}
+
+
{
+ if (value && value >= 1) {
+ setStripeTopUpCount(value);
+ setSelectedPreset(null);
+ await getStripeAmount(value);
+ }
+ }}
+ onBlur={(e) => {
+ const value = parseInt(e.target.value);
+ if (!value || value < 1) {
+ setStripeTopUpCount(1);
+ getStripeAmount(1);
+ }
+ }}
+ size='large'
+ className='w-full'
+ formatter={(value) => (value ? `${value}` : '')}
+ parser={(value) =>
+ value ? parseInt(value.replace(/[^\d]/g, '')) : 0
+ }
+ />
+
+
+
+
+ {t('选择支付方式')}
+
+
+ stripePreTopUp()}
+ size='large'
+ disabled={!enableStripeTopUp}
+ loading={paymentLoading && payWay === 'stripe'}
+ icon={}
+ style={{
+ height: '40px',
+ color: '#b161fe',
+ }}
+ className='transition-all hover:shadow-md w-full'
+ >
+ Stripe
+
+
+
+
+ >
+ )}
+
{t('兑换码充值')}
diff --git a/web/src/pages/User/AddUser.js b/web/src/pages/User/AddUser.js
index 1509ddb734f..fa4c97e6f19 100644
--- a/web/src/pages/User/AddUser.js
+++ b/web/src/pages/User/AddUser.js
@@ -1,5 +1,6 @@
import React, { useState, useRef } from 'react';
-import { API, isMobile, showError, showSuccess } from '../../helpers';
+import { API, showError, showSuccess } from '../../helpers';
+import { useIsMobile } from '../../hooks/useIsMobile.js';
import {
Button,
SideSheet,
@@ -26,6 +27,7 @@ const AddUser = (props) => {
const { t } = useTranslation();
const formApiRef = useRef(null);
const [loading, setLoading] = useState(false);
+ const isMobile = useIsMobile();
const getInitValues = () => ({
username: '',
@@ -67,7 +69,7 @@ const AddUser = (props) => {
}
bodyStyle={{ padding: '0' }}
visible={props.visible}
- width={isMobile() ? '100%' : 600}
+ width={isMobile ? '100%' : 600}
footer={
diff --git a/web/src/pages/User/EditUser.js b/web/src/pages/User/EditUser.js
index 4a8f46e9489..bfccf37b8e7 100644
--- a/web/src/pages/User/EditUser.js
+++ b/web/src/pages/User/EditUser.js
@@ -2,12 +2,12 @@ import React, { useEffect, useState, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import {
API,
- isMobile,
showError,
showSuccess,
renderQuota,
renderQuotaWithPrompt,
} from '../../helpers';
+import { useIsMobile } from '../../hooks/useIsMobile.js';
import {
Button,
Modal,
@@ -41,6 +41,7 @@ const EditUser = (props) => {
const [loading, setLoading] = useState(true);
const [addQuotaModalOpen, setIsModalOpen] = useState(false);
const [addQuotaLocal, setAddQuotaLocal] = useState('');
+ const isMobile = useIsMobile();
const [groupOptions, setGroupOptions] = useState([]);
const formApiRef = useRef(null);
@@ -137,7 +138,7 @@ const EditUser = (props) => {
}
bodyStyle={{ padding: 0 }}
visible={props.visible}
- width={isMobile() ? '100%' : 600}
+ width={isMobile ? '100%' : 600}
footer={
diff --git a/web/src/pages/User/index.js b/web/src/pages/User/index.js
index 12697d48fa9..dfe7de81327 100644
--- a/web/src/pages/User/index.js
+++ b/web/src/pages/User/index.js
@@ -3,7 +3,7 @@ import UsersTable from '../../components/table/UsersTable';
const User = () => {
return (
-