mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-04-01 04:20:00 +00:00
Compare commits
111 Commits
refactor_e
...
v0.8.8.0-a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d0037a40c | ||
|
|
f87054895e | ||
|
|
5b869376ab | ||
|
|
c674c3561a | ||
|
|
7aa2972c3f | ||
|
|
986558fea7 | ||
|
|
a3059597fb | ||
|
|
d19a6914f9 | ||
|
|
4313ede132 | ||
|
|
952b679ca3 | ||
|
|
fa02b5150c | ||
|
|
63a1904242 | ||
|
|
1e3450fdcb | ||
|
|
5541026b86 | ||
|
|
c36c920b34 | ||
|
|
514fea65c4 | ||
|
|
e269b3bfdd | ||
|
|
0862a9bfa7 | ||
|
|
4e2a3d61dc | ||
|
|
b485f2e42e | ||
|
|
16e32c3f67 | ||
|
|
15f65bb558 | ||
|
|
b161d6831f | ||
|
|
969953039f | ||
|
|
f1506ed5da | ||
|
|
9a239d9e13 | ||
|
|
a5da09dfb9 | ||
|
|
6f81f2d143 | ||
|
|
0b877ca8a3 | ||
|
|
2911b9cd04 | ||
|
|
6b3f1ab0e4 | ||
|
|
2c15655b08 | ||
|
|
afa9c650fe | ||
|
|
28d8d82ded | ||
|
|
a100baf57f | ||
|
|
d892bfc278 | ||
|
|
4369b18fbf | ||
|
|
fb9b5d31e8 | ||
|
|
3bf0748389 | ||
|
|
cf46b89814 | ||
|
|
3360b34af9 | ||
|
|
4558eb41fc | ||
|
|
bbc5584f80 | ||
|
|
8604c9f9d5 | ||
|
|
747e02ee0d | ||
|
|
8b0334309b | ||
|
|
48afa821e4 | ||
|
|
42a8d3e3dc | ||
|
|
a44fc51007 | ||
|
|
961bc874d2 | ||
|
|
b2b018ab93 | ||
|
|
77da33de4f | ||
|
|
06ad5e3f8c | ||
|
|
9326bf96fc | ||
|
|
bed73102b4 | ||
|
|
eb59f9c75d | ||
|
|
f3bd2ed472 | ||
|
|
456475d593 | ||
|
|
a36ce199ba | ||
|
|
b7c3ad0867 | ||
|
|
ea3545cc7e | ||
|
|
232ba46b16 | ||
|
|
5f011502d1 | ||
|
|
93b6f1066b | ||
|
|
52fe92ed7f | ||
|
|
0d005df463 | ||
|
|
e3ef3ace29 | ||
|
|
a203e98689 | ||
|
|
27f99a0f38 | ||
|
|
d1e48d02bd | ||
|
|
4f06a1df50 | ||
|
|
2d7ae1180f | ||
|
|
75b486b467 | ||
|
|
5b5f10fe93 | ||
|
|
5f654e76e2 | ||
|
|
aa8d112c58 | ||
|
|
e82dc0e841 | ||
|
|
dd741fc38a | ||
|
|
120e4ee92f | ||
|
|
9d2a56bff4 | ||
|
|
31d82a3169 | ||
|
|
d22ee5d451 | ||
|
|
203edaed50 | ||
|
|
93b5638a9c | ||
|
|
52a5e58f0c | ||
|
|
20607b0b5c | ||
|
|
6bebfe9e54 | ||
|
|
50b76f4466 | ||
|
|
23e4e25e9a | ||
|
|
5b83d478d6 | ||
|
|
dca38d01d6 | ||
|
|
0a434d3b3a | ||
|
|
7c4b83a430 | ||
|
|
b7f24b428b | ||
|
|
22a0ed0ee2 | ||
|
|
cf711d55a5 | ||
|
|
26ea562fdb | ||
|
|
efce0c6c57 | ||
|
|
a3768dae97 | ||
|
|
85efea3fb8 | ||
|
|
c820fda26d | ||
|
|
4740293640 | ||
|
|
8be8813cd8 | ||
|
|
8cc747ef22 | ||
|
|
d6ed2ab3e0 | ||
|
|
e8ae980104 | ||
|
|
cd8c23c0ab | ||
|
|
3568042cd9 | ||
|
|
7443129e18 | ||
|
|
4196a3db5a | ||
|
|
b887db474e |
@@ -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 .
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -193,3 +193,9 @@ const (
|
||||
ChannelStatusManuallyDisabled = 2 // also don't use 0
|
||||
ChannelStatusAutoDisabled = 3
|
||||
)
|
||||
|
||||
const (
|
||||
TopUpStatusPending = "pending"
|
||||
TopUpStatusSuccess = "success"
|
||||
TopUpStatusExpired = "expired"
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"github.com/gin-gonic/gin"
|
||||
"io"
|
||||
"net/http"
|
||||
"one-api/constant"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
34
common/hash.go
Normal file
34
common/hash.go
Normal file
@@ -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)))
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -30,5 +30,6 @@ const (
|
||||
APITypeXinference
|
||||
APITypeXai
|
||||
APITypeCoze
|
||||
APITypeJimeng
|
||||
APITypeDummy // this one is only for count, do not add any channel after this
|
||||
)
|
||||
|
||||
@@ -19,7 +19,7 @@ const (
|
||||
/* channel related keys */
|
||||
ContextKeyChannelId ContextKey = "channel_id"
|
||||
ContextKeyChannelName ContextKey = "channel_name"
|
||||
ContextKeyChannelCreateTime ContextKey = "channel_create_name"
|
||||
ContextKeyChannelCreateTime ContextKey = "channel_create_time"
|
||||
ContextKeyChannelBaseUrl ContextKey = "base_url"
|
||||
ContextKeyChannelType ContextKey = "channel_type"
|
||||
ContextKeyChannelSetting ContextKey = "channel_setting"
|
||||
@@ -29,6 +29,8 @@ const (
|
||||
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"
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -17,6 +17,7 @@ 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"
|
||||
@@ -30,22 +31,43 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func testChannel(channel *model.Channel, testModel string) (err error, newAPIError *types.NewAPIError) {
|
||||
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)
|
||||
@@ -82,31 +104,49 @@ func testChannel(channel *model.Channel, testModel string) (err error, newAPIErr
|
||||
|
||||
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, types.NewError(err, types.ErrorCodeChannelModelMappedError)
|
||||
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), types.NewError(fmt.Errorf("invalid api type: %d, adaptor is nil", apiType), types.ErrorCodeInvalidApiType)
|
||||
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)
|
||||
@@ -117,45 +157,91 @@ func testChannel(channel *model.Channel, testModel string) (err error, newAPIErr
|
||||
|
||||
priceData, err := helper.ModelPriceHelper(c, info, 0, int(request.MaxTokens))
|
||||
if err != nil {
|
||||
return err, types.NewError(err, types.ErrorCodeModelPriceError)
|
||||
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, types.NewError(err, types.ErrorCodeConvertRequestFailed)
|
||||
return testResult{
|
||||
context: c,
|
||||
localErr: err,
|
||||
newAPIError: types.NewError(err, types.ErrorCodeConvertRequestFailed),
|
||||
}
|
||||
}
|
||||
jsonData, err := json.Marshal(convertedRequest)
|
||||
if err != nil {
|
||||
return err, types.NewError(err, types.ErrorCodeJsonMarshalFailed)
|
||||
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, types.NewError(err, types.ErrorCodeDoRequestFailed)
|
||||
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 err, types.NewError(err, types.ErrorCodeBadResponse)
|
||||
return testResult{
|
||||
context: c,
|
||||
localErr: err,
|
||||
newAPIError: types.NewError(err, types.ErrorCodeBadResponse),
|
||||
}
|
||||
}
|
||||
}
|
||||
usageA, respErr := adaptor.DoResponse(c, httpResp, info)
|
||||
if respErr != nil {
|
||||
return respErr, respErr
|
||||
return testResult{
|
||||
context: c,
|
||||
localErr: respErr,
|
||||
newAPIError: respErr,
|
||||
}
|
||||
}
|
||||
if usageA == nil {
|
||||
return errors.New("usage is nil"), types.NewError(errors.New("usage is nil"), types.ErrorCodeBadResponseBody)
|
||||
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, types.NewError(err, types.ErrorCodeReadResponseBodyFailed)
|
||||
return testResult{
|
||||
context: c,
|
||||
localErr: err,
|
||||
newAPIError: types.NewError(err, types.ErrorCodeReadResponseBodyFailed),
|
||||
}
|
||||
}
|
||||
info.PromptTokens = usage.PromptTokens
|
||||
|
||||
@@ -188,7 +274,11 @@ func testChannel(channel *model.Channel, testModel string) (err error, newAPIErr
|
||||
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 {
|
||||
@@ -203,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 模型
|
||||
@@ -231,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 {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
//defer func() {
|
||||
// if channel.ChannelInfo.IsMultiKey {
|
||||
// go func() { _ = channel.SaveChannelInfo() }()
|
||||
// }
|
||||
//}()
|
||||
testModel := c.Query("model")
|
||||
tik := time.Now()
|
||||
_, newAPIError := testChannel(channel, testModel)
|
||||
result := testChannel(channel, testModel)
|
||||
if result.localErr != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": result.localErr.Error(),
|
||||
"time": 0.0,
|
||||
})
|
||||
return
|
||||
}
|
||||
tok := time.Now()
|
||||
milliseconds := tok.Sub(tik).Milliseconds()
|
||||
go channel.UpdateResponseTime(milliseconds)
|
||||
consumedTime := float64(milliseconds) / 1000.0
|
||||
if newAPIError != nil {
|
||||
if result.newAPIError != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": newAPIError.Error(),
|
||||
"message": result.newAPIError.Error(),
|
||||
"time": consumedTime,
|
||||
})
|
||||
return
|
||||
@@ -280,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 {
|
||||
@@ -299,30 +396,34 @@ func testAllChannels(notify bool) error {
|
||||
for _, channel := range channels {
|
||||
isChannelEnabled := channel.Status == common.ChannelStatusEnabled
|
||||
tik := time.Now()
|
||||
err, newAPIError := testChannel(channel, "")
|
||||
result := testChannel(channel, "")
|
||||
tok := time.Now()
|
||||
milliseconds := tok.Sub(tik).Milliseconds()
|
||||
|
||||
shouldBanChannel := false
|
||||
|
||||
newAPIError := result.newAPIError
|
||||
// request error disables the channel
|
||||
if err != nil {
|
||||
shouldBanChannel = service.ShouldDisableChannel(channel.Type, newAPIError)
|
||||
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, newAPIError, 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)
|
||||
@@ -339,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{
|
||||
|
||||
@@ -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,6 +350,46 @@ 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"`
|
||||
@@ -422,6 +432,12 @@ func AddChannel(c *gin.Context) {
|
||||
addChannelRequest := AddChannelRequest{}
|
||||
err := c.ShouldBindJSON(&addChannelRequest)
|
||||
if err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
// 使用统一的校验函数
|
||||
if err := validateChannel(addChannelRequest.Channel, true); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
@@ -429,59 +445,6 @@ func AddChannel(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
err = addChannelRequest.Channel.ValidateSettings()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "channel setting 格式错误:" + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if addChannelRequest.Channel == nil || addChannelRequest.Channel.Key == "" {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "channel cannot be empty",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Validate the length of the model name
|
||||
for _, m := range addChannelRequest.Channel.GetModels() {
|
||||
if len(m) > 255 {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": fmt.Sprintf("模型名称过长: %s", m),
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
if addChannelRequest.Channel.Type == constant.ChannelTypeVertexAi {
|
||||
if addChannelRequest.Channel.Other == "" {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "部署地区不能为空",
|
||||
})
|
||||
return
|
||||
} else {
|
||||
regionMap, err := common.StrToMap(addChannelRequest.Channel.Other)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "部署地区必须是标准的Json格式,例如{\"default\": \"us-central1\", \"region2\": \"us-east1\"}",
|
||||
})
|
||||
return
|
||||
}
|
||||
if regionMap["default"] == nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "部署地区必须包含default字段",
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addChannelRequest.Channel.CreatedTime = common.GetTimestamp()
|
||||
keys := make([]string, 0)
|
||||
switch addChannelRequest.Mode {
|
||||
@@ -497,6 +460,7 @@ func AddChannel(c *gin.Context) {
|
||||
})
|
||||
return
|
||||
}
|
||||
addChannelRequest.Channel.ChannelInfo.MultiKeySize = len(array)
|
||||
addChannelRequest.Channel.Key = strings.Join(array, "\n")
|
||||
} else {
|
||||
cleanKeys := make([]string, 0)
|
||||
@@ -507,6 +471,7 @@ func AddChannel(c *gin.Context) {
|
||||
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}
|
||||
@@ -545,10 +510,7 @@ func AddChannel(c *gin.Context) {
|
||||
}
|
||||
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{
|
||||
@@ -563,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": "",
|
||||
@@ -579,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": "",
|
||||
@@ -615,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": "",
|
||||
@@ -640,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": "",
|
||||
@@ -672,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": "",
|
||||
@@ -702,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": "",
|
||||
@@ -716,9 +666,29 @@ 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
|
||||
}
|
||||
// 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,
|
||||
@@ -726,47 +696,20 @@ func UpdateChannel(c *gin.Context) {
|
||||
})
|
||||
return
|
||||
}
|
||||
err = channel.ValidateSettings()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "channel setting 格式错误:" + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
if channel.Type == constant.ChannelTypeVertexAi {
|
||||
if channel.Other == "" {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "部署地区不能为空",
|
||||
})
|
||||
return
|
||||
} else {
|
||||
regionMap, err := common.StrToMap(channel.Other)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "部署地区必须是标准的Json格式,例如{\"default\": \"us-central1\", \"region2\": \"us-east1\"}",
|
||||
})
|
||||
return
|
||||
}
|
||||
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,
|
||||
@@ -869,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": "",
|
||||
@@ -923,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}})
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
"<p>验证码 %d 分钟内有效,如果不是本人操作,请忽略。</p>", 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) {
|
||||
"<p>重置链接 %d 分钟内有效,如果不是本人操作,请忽略。</p>", 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)
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -58,20 +58,6 @@ func Playground(c *gin.Context) {
|
||||
}
|
||||
|
||||
userId := c.GetInt("id")
|
||||
//c.Set("token_name", "playground-"+group)
|
||||
tempToken := &model.Token{
|
||||
UserId: userId,
|
||||
Name: fmt.Sprintf("playground-%s", group),
|
||||
Group: group,
|
||||
}
|
||||
_ = middleware.SetupContextForToken(c, tempToken)
|
||||
_, err = getChannel(c, group, playgroundRequest.Model, 0)
|
||||
if err != nil {
|
||||
newAPIError = types.NewError(err, types.ErrorCodeGetChannelFailed)
|
||||
return
|
||||
}
|
||||
//middleware.SetupContextForSelectedChannel(c, channel, playgroundRequest.Model)
|
||||
common.SetContextKey(c, constant.ContextKeyRequestStartTime, time.Now())
|
||||
|
||||
// Write user context to ensure acceptUnsetRatio is available
|
||||
userCache, err := model.GetUserCache(userId)
|
||||
@@ -80,5 +66,19 @@ func Playground(c *gin.Context) {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ func Relay(c *gin.Context) {
|
||||
channel, err := getChannel(c, group, originalModel, i)
|
||||
if err != nil {
|
||||
common.LogError(c, err.Error())
|
||||
newAPIError = types.NewError(err, types.ErrorCodeGetChannelFailed)
|
||||
newAPIError = err
|
||||
break
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ func Relay(c *gin.Context) {
|
||||
return // 成功处理请求,直接返回
|
||||
}
|
||||
|
||||
go processChannelError(c, channel.Id, channel.Type, channel.Name, channel.GetAutoBan(), newAPIError)
|
||||
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, newAPIError, common.RetryTimes-i) {
|
||||
break
|
||||
@@ -103,10 +103,10 @@ func Relay(c *gin.Context) {
|
||||
}
|
||||
|
||||
if newAPIError != nil {
|
||||
if newAPIError.StatusCode == http.StatusTooManyRequests {
|
||||
common.LogError(c, fmt.Sprintf("origin 429 error: %s", newAPIError.Error()))
|
||||
newAPIError.SetMessage("当前分组上游负载已饱和,请稍后再试")
|
||||
}
|
||||
//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(),
|
||||
@@ -143,7 +143,7 @@ func WssRelay(c *gin.Context) {
|
||||
channel, err := getChannel(c, group, originalModel, i)
|
||||
if err != nil {
|
||||
common.LogError(c, err.Error())
|
||||
newAPIError = types.NewError(err, types.ErrorCodeGetChannelFailed)
|
||||
newAPIError = err
|
||||
break
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ func WssRelay(c *gin.Context) {
|
||||
return // 成功处理请求,直接返回
|
||||
}
|
||||
|
||||
go processChannelError(c, channel.Id, channel.Type, channel.Name, channel.GetAutoBan(), newAPIError)
|
||||
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, newAPIError, common.RetryTimes-i) {
|
||||
break
|
||||
@@ -166,9 +166,9 @@ func WssRelay(c *gin.Context) {
|
||||
}
|
||||
|
||||
if newAPIError != nil {
|
||||
if newAPIError.StatusCode == http.StatusTooManyRequests {
|
||||
newAPIError.SetMessage("当前分组上游负载已饱和,请稍后再试")
|
||||
}
|
||||
//if newAPIError.StatusCode == http.StatusTooManyRequests {
|
||||
// newAPIError.SetMessage("当前分组上游负载已饱和,请稍后再试")
|
||||
//}
|
||||
newAPIError.SetMessage(common.MessageWithRequestId(newAPIError.Error(), requestId))
|
||||
helper.WssError(c, ws, newAPIError.ToOpenAIError())
|
||||
}
|
||||
@@ -185,7 +185,7 @@ func RelayClaude(c *gin.Context) {
|
||||
channel, err := getChannel(c, group, originalModel, i)
|
||||
if err != nil {
|
||||
common.LogError(c, err.Error())
|
||||
newAPIError = types.NewError(err, types.ErrorCodeGetChannelFailed)
|
||||
newAPIError = err
|
||||
break
|
||||
}
|
||||
|
||||
@@ -195,7 +195,7 @@ func RelayClaude(c *gin.Context) {
|
||||
return // 成功处理请求,直接返回
|
||||
}
|
||||
|
||||
go processChannelError(c, channel.Id, channel.Type, channel.Name, channel.GetAutoBan(), newAPIError)
|
||||
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, newAPIError, common.RetryTimes-i) {
|
||||
break
|
||||
@@ -243,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
|
||||
@@ -260,11 +260,14 @@ func getChannel(c *gin.Context, group, originalModel string, retryCount int) (*m
|
||||
channel, selectGroup, err := model.CacheGetRandomSatisfiedChannel(c, group, originalModel, retryCount)
|
||||
if err != nil {
|
||||
if group == "auto" {
|
||||
return nil, errors.New(fmt.Sprintf("获取自动分组下模型 %s 的可用渠道失败: %s", originalModel, err.Error()))
|
||||
return nil, types.NewError(errors.New(fmt.Sprintf("获取自动分组下模型 %s 的可用渠道失败: %s", originalModel, err.Error())), types.ErrorCodeGetChannelFailed)
|
||||
}
|
||||
return nil, errors.New(fmt.Sprintf("获取分组 %s 下模型 %s 的可用渠道失败: %s", selectGroup, originalModel, err.Error()))
|
||||
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
|
||||
}
|
||||
|
||||
@@ -314,12 +317,12 @@ func shouldRetry(c *gin.Context, openaiErr *types.NewAPIError, retryTimes int) b
|
||||
return true
|
||||
}
|
||||
|
||||
func processChannelError(c *gin.Context, channelId int, channelType int, channelName string, autoBan bool, err *types.NewAPIError) {
|
||||
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()))
|
||||
if service.ShouldDisableChannel(channelType, err) && autoBan {
|
||||
service.DisableChannel(channelId, channelName, err.Error())
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -392,10 +395,10 @@ func RelayTask(c *gin.Context) {
|
||||
retryTimes = 0
|
||||
}
|
||||
for i := 0; shouldRetryTaskRelay(c, channelId, taskErr, retryTimes) && i < retryTimes; i++ {
|
||||
channel, err := getChannel(c, group, originalModel, i)
|
||||
if err != nil {
|
||||
common.LogError(c, fmt.Sprintf("CacheGetRandomSatisfiedChannel failed: %s", err.Error()))
|
||||
taskErr = service.TaskErrorWrapperLocal(err, "get_channel_failed", http.StatusInternalServerError)
|
||||
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
|
||||
@@ -405,7 +408,7 @@ func RelayTask(c *gin.Context) {
|
||||
common.LogInfo(c, fmt.Sprintf("using channel #%d to retry (remain times %d)", channel.Id, i))
|
||||
//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)
|
||||
}
|
||||
|
||||
116
controller/swag_video.go
Normal file
116
controller/swag_video.go
Normal file
@@ -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"`
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
|
||||
275
controller/topup_stripe.go
Normal file
275
controller/topup_stripe.go
Normal file
@@ -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)
|
||||
}
|
||||
@@ -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{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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{
|
||||
|
||||
195
docs/api/web_api.md
Normal file
195
docs/api/web_api.md
Normal file
@@ -0,0 +1,195 @@
|
||||
# One API – Web 界面后端接口文档
|
||||
|
||||
> 本文档汇总了 **One API** 后端提供给前端 Web 界面的全部 REST 接口(不含 *Relay* 相关接口)。
|
||||
>
|
||||
> 接口前缀统一为 `https://<your-domain>`,以下仅列出 **路径**、**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
|
||||
@@ -159,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"`
|
||||
@@ -177,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"`
|
||||
@@ -251,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"`
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
@@ -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"`
|
||||
//}
|
||||
|
||||
@@ -48,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"`
|
||||
@@ -182,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 {
|
||||
@@ -200,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 *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 []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 {
|
||||
|
||||
4
go.mod
4
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
|
||||
|
||||
6
go.sum
6
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=
|
||||
|
||||
4
main.go
4
main.go
@@ -168,11 +168,11 @@ func InitResources() error {
|
||||
common.SysLog("No .env file found, using default environment variables. If needed, please create a .env file and set the relevant variables.")
|
||||
}
|
||||
|
||||
common.SetupLogger()
|
||||
|
||||
// 加载环境变量
|
||||
common.InitEnv()
|
||||
|
||||
common.SetupLogger()
|
||||
|
||||
// Initialize model settings
|
||||
ratio_setting.InitRatioSettings()
|
||||
|
||||
|
||||
@@ -123,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()
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"one-api/service"
|
||||
"one-api/setting"
|
||||
"one-api/setting/ratio_setting"
|
||||
"one-api/types"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -249,10 +250,10 @@ func getModelRequest(c *gin.Context) (*ModelRequest, bool, error) {
|
||||
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)
|
||||
}
|
||||
common.SetContextKey(c, constant.ContextKeyChannelId, channel.Id)
|
||||
common.SetContextKey(c, constant.ContextKeyChannelName, channel.Name)
|
||||
@@ -266,11 +267,17 @@ func SetupContextForSelectedChannel(c *gin.Context, channel *model.Channel, mode
|
||||
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", channel.Key))
|
||||
// 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统一
|
||||
@@ -292,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 路径中提取模型名
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
250
model/channel.go
250
model/channel.go
@@ -3,11 +3,13 @@ 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"
|
||||
|
||||
@@ -48,6 +50,7 @@ type Channel struct {
|
||||
|
||||
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"`
|
||||
@@ -68,22 +71,34 @@ func (channel *Channel) getKeys() []string {
|
||||
if channel.Key == "" {
|
||||
return []string{}
|
||||
}
|
||||
// use \n to split keys
|
||||
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, error) {
|
||||
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, nil
|
||||
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 "", fmt.Errorf("no valid keys in channel")
|
||||
return "", 0, types.NewError(errors.New("no keys available"), types.ErrorCodeChannelNoAvailableKey)
|
||||
}
|
||||
|
||||
statusList := channel.ChannelInfo.MultiKeyStatusList
|
||||
@@ -107,16 +122,37 @@ func (channel *Channel) GetNextEnabledKey() (string, error) {
|
||||
}
|
||||
// If no specific status list or none enabled, fall back to first key
|
||||
if len(enabledIdx) == 0 {
|
||||
return keys[0], nil
|
||||
return keys[0], 0, nil
|
||||
}
|
||||
|
||||
switch channel.ChannelInfo.MultiKeyMode {
|
||||
case constant.MultiKeyModeRandom:
|
||||
// Randomly pick one enabled key
|
||||
return keys[enabledIdx[rand.Intn(len(enabledIdx))]], nil
|
||||
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 := channel.ChannelInfo.MultiKeyPollingIndex
|
||||
start := channelInfo.MultiKeyPollingIndex
|
||||
if start < 0 || start >= len(keys) {
|
||||
start = 0
|
||||
}
|
||||
@@ -125,17 +161,21 @@ func (channel *Channel) GetNextEnabledKey() (string, error) {
|
||||
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], nil
|
||||
return keys[idx], idx, nil
|
||||
}
|
||||
}
|
||||
// Fallback – should not happen, but return first enabled key
|
||||
return keys[enabledIdx[0]], nil
|
||||
return keys[enabledIdx[0]], enabledIdx[0], nil
|
||||
default:
|
||||
// Unknown mode, default to first enabled key (or original key string)
|
||||
return keys[enabledIdx[0]], nil
|
||||
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 {
|
||||
if channel.Models == "" {
|
||||
return []string{}
|
||||
@@ -271,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
|
||||
}
|
||||
return &channel, err
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if channel == nil {
|
||||
return nil, errors.New("channel not found")
|
||||
}
|
||||
return channel, nil
|
||||
}
|
||||
|
||||
func BatchInsertChannels(channels []Channel) error {
|
||||
@@ -362,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 {
|
||||
@@ -404,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())
|
||||
@@ -628,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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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"`
|
||||
|
||||
@@ -260,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{}
|
||||
@@ -279,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)
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -40,7 +40,7 @@ func embeddingRequestOpenAI2Ali(request dto.EmbeddingRequest) *AliEmbeddingReque
|
||||
}
|
||||
|
||||
func aliEmbeddingHandler(c *gin.Context, resp *http.Response) (*types.NewAPIError, *dto.Usage) {
|
||||
var fullTextResponse dto.OpenAIEmbeddingResponse
|
||||
var fullTextResponse dto.FlexibleEmbeddingResponse
|
||||
err := json.NewDecoder(resp.Body).Decode(&fullTextResponse)
|
||||
if err != nil {
|
||||
return types.NewError(err, types.ErrorCodeBadResponseBody), nil
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -18,6 +18,12 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const (
|
||||
WebSearchMaxUsesLow = 1
|
||||
WebSearchMaxUsesMedium = 5
|
||||
WebSearchMaxUsesHigh = 10
|
||||
)
|
||||
|
||||
func stopReasonClaude2OpenAI(reason string) string {
|
||||
switch reason {
|
||||
case "stop_sequence":
|
||||
@@ -65,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 {
|
||||
@@ -85,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,
|
||||
@@ -100,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))
|
||||
}
|
||||
@@ -124,6 +190,27 @@ 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.Unmarshal(textRequest.Reasoning, &reasoning); err != nil {
|
||||
@@ -645,6 +732,10 @@ func HandleClaudeResponseData(c *gin.Context, info *relaycommon.RelayInfo, claud
|
||||
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
|
||||
}
|
||||
@@ -672,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
|
||||
}
|
||||
|
||||
@@ -74,12 +74,12 @@ func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.Rela
|
||||
|
||||
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, info, resp)
|
||||
usage, err = cohereStreamHandler(c, info, resp) // TODO: fix this
|
||||
} else {
|
||||
err, usage = cohereHandler(c, info, resp)
|
||||
usage, err = cohereHandler(c, info, resp)
|
||||
}
|
||||
}
|
||||
return
|
||||
|
||||
@@ -78,7 +78,7 @@ func stopReasonCohere2OpenAI(reason string) string {
|
||||
}
|
||||
}
|
||||
|
||||
func cohereStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Response) (*types.NewAPIError, *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{}
|
||||
@@ -166,20 +166,20 @@ func cohereStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http
|
||||
if usage.PromptTokens == 0 {
|
||||
usage = service.ResponseText2Usage(responseText, info.UpstreamModelName, info.PromptTokens)
|
||||
}
|
||||
return nil, usage
|
||||
return usage, nil
|
||||
}
|
||||
|
||||
func cohereHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Response) (*types.NewAPIError, *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 types.NewError(err, types.ErrorCodeBadResponseBody), nil
|
||||
return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
|
||||
}
|
||||
common.CloseResponseBodyGracefully(resp)
|
||||
var cohereResp CohereResponseResult
|
||||
err = json.Unmarshal(responseBody, &cohereResp)
|
||||
if err != nil {
|
||||
return types.NewError(err, types.ErrorCodeBadResponseBody), nil
|
||||
return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
|
||||
}
|
||||
usage := dto.Usage{}
|
||||
usage.PromptTokens = cohereResp.Meta.BilledUnits.InputTokens
|
||||
@@ -203,24 +203,24 @@ func cohereHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Respo
|
||||
|
||||
jsonResponse, err := json.Marshal(openaiResp)
|
||||
if err != nil {
|
||||
return types.NewError(err, types.ErrorCodeBadResponseBody), 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) (*types.NewAPIError, *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 types.NewError(err, types.ErrorCodeBadResponseBody), nil
|
||||
return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
|
||||
}
|
||||
common.CloseResponseBodyGracefully(resp)
|
||||
var cohereResp CohereRerankResponseResult
|
||||
err = json.Unmarshal(responseBody, &cohereResp)
|
||||
if err != nil {
|
||||
return types.NewError(err, types.ErrorCodeBadResponseBody), nil
|
||||
return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
|
||||
}
|
||||
usage := dto.Usage{}
|
||||
if cohereResp.Meta.BilledUnits.InputTokens == 0 {
|
||||
@@ -239,10 +239,10 @@ func cohereRerankHandler(c *gin.Context, resp *http.Response, info *relaycommon.
|
||||
|
||||
jsonResponse, err := json.Marshal(rerankResp)
|
||||
if err != nil {
|
||||
return types.NewError(err, types.ErrorCodeBadResponseBody), 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
|
||||
}
|
||||
|
||||
@@ -98,9 +98,9 @@ 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 *types.NewAPIError) {
|
||||
if info.IsStream {
|
||||
err, usage = cozeChatStreamHandler(c, info, resp)
|
||||
usage, err = cozeChatStreamHandler(c, info, resp)
|
||||
} else {
|
||||
err, usage = cozeChatHandler(c, info, resp)
|
||||
usage, err = cozeChatHandler(c, info, resp)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -44,10 +44,10 @@ func convertCozeChatRequest(c *gin.Context, request dto.GeneralOpenAIRequest) *C
|
||||
return cozeRequest
|
||||
}
|
||||
|
||||
func cozeChatHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Response) (*types.NewAPIError, *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 types.NewError(err, types.ErrorCodeBadResponseBody), nil
|
||||
return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
|
||||
}
|
||||
common.CloseResponseBodyGracefully(resp)
|
||||
// convert coze response to openai response
|
||||
@@ -56,10 +56,10 @@ func cozeChatHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Res
|
||||
response.Model = info.UpstreamModelName
|
||||
err = json.Unmarshal(responseBody, &cozeResponse)
|
||||
if err != nil {
|
||||
return types.NewError(err, types.ErrorCodeBadResponseBody), nil
|
||||
return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
|
||||
}
|
||||
if cozeResponse.Code != 0 {
|
||||
return types.NewError(errors.New(cozeResponse.Msg), types.ErrorCodeBadResponseBody), nil
|
||||
return nil, types.NewError(errors.New(cozeResponse.Msg), types.ErrorCodeBadResponseBody)
|
||||
}
|
||||
// 从上下文获取 usage
|
||||
var usage dto.Usage
|
||||
@@ -86,16 +86,16 @@ func cozeChatHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Res
|
||||
}
|
||||
jsonResponse, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
return types.NewError(err, types.ErrorCodeBadResponseBody), 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, info *relaycommon.RelayInfo, resp *http.Response) (*types.NewAPIError, *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)
|
||||
@@ -136,7 +136,7 @@ func cozeChatStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *ht
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return types.NewError(err, types.ErrorCodeBadResponseBody), nil
|
||||
return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
|
||||
}
|
||||
helper.Done(c)
|
||||
|
||||
@@ -144,7 +144,7 @@ func cozeChatStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *ht
|
||||
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) {
|
||||
|
||||
136
relay/channel/jimeng/adaptor.go
Normal file
136
relay/channel/jimeng/adaptor.go
Normal file
@@ -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
|
||||
}
|
||||
9
relay/channel/jimeng/constants.go
Normal file
9
relay/channel/jimeng/constants.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package jimeng
|
||||
|
||||
const (
|
||||
ChannelName = "jimeng"
|
||||
)
|
||||
|
||||
var ModelList = []string{
|
||||
"jimeng_high_aes_general_v21_L",
|
||||
}
|
||||
89
relay/channel/jimeng/image.go
Normal file
89
relay/channel/jimeng/image.go
Normal file
@@ -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
|
||||
}
|
||||
176
relay/channel/jimeng/sign.go
Normal file
176
relay/channel/jimeng/sign.go
Normal file
@@ -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)
|
||||
}
|
||||
@@ -96,7 +96,7 @@ func ollamaEmbeddingHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *h
|
||||
return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
|
||||
}
|
||||
if ollamaEmbeddingResponse.Error != "" {
|
||||
return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
|
||||
return nil, types.NewError(fmt.Errorf("ollama error: %s", ollamaEmbeddingResponse.Error), types.ErrorCodeBadResponseBody)
|
||||
}
|
||||
flattenedEmbeddings := flattenEmbeddings(ollamaEmbeddingResponse.Embedding)
|
||||
data := make([]dto.OpenAIEmbeddingResponseItem, 0, 1)
|
||||
|
||||
@@ -35,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
|
||||
|
||||
@@ -42,7 +42,7 @@ func OaiResponsesHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http
|
||||
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 &usage, nil
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"one-api/common"
|
||||
"one-api/constant"
|
||||
"one-api/dto"
|
||||
"one-api/relay/channel"
|
||||
relaycommon "one-api/relay/common"
|
||||
@@ -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
|
||||
|
||||
@@ -57,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,21 +82,26 @@ func xAIHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Response
|
||||
defer common.CloseResponseBodyGracefully(resp)
|
||||
|
||||
responseBody, err := io.ReadAll(resp.Body)
|
||||
var response *dto.SimpleResponse
|
||||
err = common.Unmarshal(responseBody, &response)
|
||||
if err != nil {
|
||||
return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
|
||||
}
|
||||
response.Usage.CompletionTokens = response.Usage.TotalTokens - response.Usage.PromptTokens
|
||||
response.Usage.CompletionTokenDetails.TextTokens = response.Usage.CompletionTokens - response.Usage.CompletionTokenDetails.ReasoningTokens
|
||||
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
|
||||
}
|
||||
|
||||
// new body
|
||||
encodeJson, err := common.Marshal(response)
|
||||
encodeJson, err := common.Marshal(xaiResponse)
|
||||
if err != nil {
|
||||
return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
|
||||
}
|
||||
|
||||
common.IOCopyBytesGracefully(c, resp, encodeJson)
|
||||
|
||||
return &response.Usage, nil
|
||||
return xaiResponse.Usage, nil
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@ func ClaudeHelper(c *gin.Context) (newAPIError *types.NewAPIError) {
|
||||
var httpResp *http.Response
|
||||
resp, err := adaptor.DoRequest(c, relayInfo, requestBody)
|
||||
if err != nil {
|
||||
return types.NewError(err, types.ErrorCodeDoRequestFailed)
|
||||
return types.NewOpenAIError(err, types.ErrorCodeDoRequestFailed, http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
if resp != nil {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"),
|
||||
|
||||
@@ -91,7 +91,7 @@ func EmbeddingHelper(c *gin.Context) (newAPIError *types.NewAPIError) {
|
||||
statusCodeMappingStr := c.GetString("status_code_mapping")
|
||||
resp, err := adaptor.DoRequest(c, relayInfo, requestBody)
|
||||
if err != nil {
|
||||
return types.NewError(err, types.ErrorCodeDoRequestFailed)
|
||||
return types.NewOpenAIError(err, types.ErrorCodeDoRequestFailed, http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
var httpResp *http.Response
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -145,22 +145,25 @@ func ImageHelper(c *gin.Context) (newAPIError *types.NewAPIError) {
|
||||
|
||||
} 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,7 +209,7 @@ func ImageHelper(c *gin.Context) (newAPIError *types.NewAPIError) {
|
||||
|
||||
resp, err := adaptor.DoRequest(c, relayInfo, requestBody)
|
||||
if err != nil {
|
||||
return types.NewError(err, types.ErrorCodeDoRequestFailed)
|
||||
return types.NewOpenAIError(err, types.ErrorCodeDoRequestFailed, http.StatusInternalServerError)
|
||||
}
|
||||
var httpResp *http.Response
|
||||
if resp != nil {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -210,7 +210,7 @@ func TextHelper(c *gin.Context) (newAPIError *types.NewAPIError) {
|
||||
resp, err := adaptor.DoRequest(c, relayInfo, requestBody)
|
||||
|
||||
if err != nil {
|
||||
return types.NewError(err, types.ErrorCodeDoRequestFailed)
|
||||
return types.NewOpenAIError(err, types.ErrorCodeDoRequestFailed, http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
statusCodeMappingStr := c.GetString("status_code_mapping")
|
||||
@@ -379,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 * 分组倍率)
|
||||
@@ -401,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
|
||||
@@ -524,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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -84,7 +84,7 @@ func RerankHelper(c *gin.Context, relayMode int) (newAPIError *types.NewAPIError
|
||||
}
|
||||
resp, err := adaptor.DoRequest(c, relayInfo, requestBody)
|
||||
if err != nil {
|
||||
return types.NewError(err, types.ErrorCodeDoRequestFailed)
|
||||
return types.NewOpenAIError(err, types.ErrorCodeDoRequestFailed, http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
statusCodeMappingStr := c.GetString("status_code_mapping")
|
||||
|
||||
@@ -137,7 +137,7 @@ func ResponsesHelper(c *gin.Context) (newAPIError *types.NewAPIError) {
|
||||
var httpResp *http.Response
|
||||
resp, err := adaptor.DoRequest(c, relayInfo, requestBody)
|
||||
if err != nil {
|
||||
return types.NewError(err, types.ErrorCodeDoRequestFailed)
|
||||
return types.NewOpenAIError(err, types.ErrorCodeDoRequestFailed, http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
statusCodeMappingStr := c.GetString("status_code_mapping")
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -17,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)
|
||||
@@ -87,13 +87,10 @@ func ShouldDisableChannel(channelType int, err *types.NewAPIError) bool {
|
||||
return search
|
||||
}
|
||||
|
||||
func ShouldEnableChannel(err error, newAPIError *types.NewAPIError, status int) bool {
|
||||
func ShouldEnableChannel(newAPIError *types.NewAPIError, status int) bool {
|
||||
if !common.AutomaticEnableChannelEnabled {
|
||||
return false
|
||||
}
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if newAPIError != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -82,6 +82,7 @@ func ClaudeErrorWrapperLocal(err error, code string, statusCode int) *dto.Claude
|
||||
func RelayErrorHandler(resp *http.Response, showBodyWhenFail bool) (newApiErr *types.NewAPIError) {
|
||||
newApiErr = &types.NewAPIError{
|
||||
StatusCode: resp.StatusCode,
|
||||
ErrorType: types.ErrorTypeOpenAIError,
|
||||
}
|
||||
|
||||
responseBody, err := io.ReadAll(resp.Body)
|
||||
@@ -105,6 +106,7 @@ func RelayErrorHandler(resp *http.Response, showBodyWhenFail bool) (newApiErr *t
|
||||
newApiErr = types.WithOpenAIError(errResponse.Error, resp.StatusCode)
|
||||
} else {
|
||||
newApiErr = types.NewErrorWithStatusCode(errors.New(errResponse.ToMessage()), types.ErrorCodeBadResponseStatusCode, resp.StatusCode)
|
||||
newApiErr.ErrorType = types.ErrorTypeOpenAIError
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 收费
|
||||
|
||||
@@ -8,6 +8,7 @@ var EpayId = ""
|
||||
var EpayKey = ""
|
||||
var Price = 7.3
|
||||
var MinTopUp = 1
|
||||
var USDExchangeRate = 7.3
|
||||
|
||||
var PayMethods = []map[string]string{
|
||||
{
|
||||
|
||||
7
setting/payment_stripe.go
Normal file
7
setting/payment_stripe.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package setting
|
||||
|
||||
var StripeApiSecret = ""
|
||||
var StripeWebhookSecret = ""
|
||||
var StripePriceId = ""
|
||||
var StripeUnitPrice = 8.0
|
||||
var StripeMinTopUp = 1
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
21
types/channel_error.go
Normal file
21
types/channel_error.go
Normal file
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,7 @@ const (
|
||||
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"
|
||||
@@ -87,6 +88,13 @@ func (e *NewAPIError) GetErrorCode() 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()
|
||||
}
|
||||
|
||||
@@ -144,6 +152,14 @@ func NewError(err error, errorCode ErrorCode) *NewAPIError {
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
|
||||
2010
web/bun.lock
Normal file
2010
web/bun.lock
Normal file
File diff suppressed because it is too large
Load Diff
BIN
web/bun.lockb
BIN
web/bun.lockb
Binary file not shown.
@@ -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 <Loading prompt={prompt} />;
|
||||
return <Loading />;
|
||||
};
|
||||
|
||||
export default OAuth2Callback;
|
||||
|
||||
@@ -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 (
|
||||
<div className="fixed inset-0 w-screen h-screen flex items-center justify-center bg-white/80 z-[1000]">
|
||||
<div className="flex flex-col items-center">
|
||||
<Spin
|
||||
size={size}
|
||||
spinning={true}
|
||||
tip={null}
|
||||
/>
|
||||
<span className="whitespace-nowrap mt-2 text-center" style={{ color: 'var(--semi-color-primary)' }}>
|
||||
{name ? t('{{name}}', { name }) : t('加载中...')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="fixed inset-0 w-screen h-screen flex items-center justify-center">
|
||||
<Spin
|
||||
size={size}
|
||||
spinning={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -22,7 +22,7 @@ const FooterBar = () => {
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
const customFooter = useMemo(() => (
|
||||
<footer className="relative bg-semi-color-bg-2 h-auto py-16 px-6 md:px-24 w-full flex flex-col items-center justify-between overflow-hidden">
|
||||
<footer className="relative h-auto py-16 px-6 md:px-24 w-full flex flex-col items-center justify-between overflow-hidden">
|
||||
<div className="absolute hidden md:block top-[204px] left-[-100px] w-[151px] h-[151px] rounded-full bg-[#FFD166]"></div>
|
||||
<div className="absolute md:hidden bottom-[20px] left-[-50px] w-[80px] h-[80px] rounded-full bg-[#FFD166] opacity-60"></div>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import React, { useContext, useEffect, useState, useRef } from 'react';
|
||||
import { Link, useNavigate, useLocation } from 'react-router-dom';
|
||||
import { UserContext } from '../../context/User/index.js';
|
||||
import { useSetTheme, useTheme } from '../../context/Theme/index.js';
|
||||
@@ -31,13 +31,15 @@ import {
|
||||
Badge,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { StatusContext } from '../../context/Status/index.js';
|
||||
import { useStyle, styleActions } from '../../context/Style/index.js';
|
||||
import { useIsMobile } from '../../hooks/useIsMobile.js';
|
||||
import { useSidebarCollapsed } from '../../hooks/useSidebarCollapsed.js';
|
||||
|
||||
const HeaderBar = () => {
|
||||
const HeaderBar = ({ onMobileMenuToggle, drawerOpen }) => {
|
||||
const { t, i18n } = useTranslation();
|
||||
const [userState, userDispatch] = useContext(UserContext);
|
||||
const [statusState, statusDispatch] = useContext(StatusContext);
|
||||
const { state: styleState, dispatch: styleDispatch } = useStyle();
|
||||
const isMobile = useIsMobile();
|
||||
const [collapsed, toggleCollapsed] = useSidebarCollapsed();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
let navigate = useNavigate();
|
||||
const [currentLang, setCurrentLang] = useState(i18n.language);
|
||||
@@ -45,6 +47,7 @@ const HeaderBar = () => {
|
||||
const location = useLocation();
|
||||
const [noticeVisible, setNoticeVisible] = useState(false);
|
||||
const [unreadCount, setUnreadCount] = useState(0);
|
||||
const loadingStartRef = useRef(Date.now());
|
||||
|
||||
const systemName = getSystemName();
|
||||
const logo = getLogo();
|
||||
@@ -194,11 +197,15 @@ const HeaderBar = () => {
|
||||
}, [i18n]);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
}, 500);
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
if (statusState?.status !== undefined) {
|
||||
const elapsed = Date.now() - loadingStartRef.current;
|
||||
const remaining = Math.max(0, 500 - elapsed);
|
||||
const timer = setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
}, remaining);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [statusState?.status]);
|
||||
|
||||
const handleLanguageChange = (lang) => {
|
||||
i18n.changeLanguage(lang);
|
||||
@@ -207,7 +214,7 @@ const HeaderBar = () => {
|
||||
|
||||
const handleNavLinkClick = (itemKey) => {
|
||||
if (itemKey === 'home') {
|
||||
styleDispatch(styleActions.setSider(false));
|
||||
// styleDispatch(styleActions.setSider(false)); // This line is removed
|
||||
}
|
||||
setMobileMenuOpen(false);
|
||||
};
|
||||
@@ -221,7 +228,16 @@ const HeaderBar = () => {
|
||||
.fill(null)
|
||||
.map((_, index) => (
|
||||
<div key={index} className={skeletonLinkClasses}>
|
||||
<Skeleton.Title style={{ width: isMobileView ? 100 : 60, height: 16 }} />
|
||||
<Skeleton
|
||||
loading={true}
|
||||
active
|
||||
placeholder={
|
||||
<Skeleton.Title
|
||||
active
|
||||
style={{ width: isMobileView ? 100 : 60, height: 16 }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
}
|
||||
@@ -272,9 +288,22 @@ const HeaderBar = () => {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center p-1 rounded-full bg-semi-color-fill-0 dark:bg-semi-color-fill-1">
|
||||
<Skeleton.Avatar size="extra-small" className="shadow-sm" />
|
||||
<Skeleton
|
||||
loading={true}
|
||||
active
|
||||
placeholder={<Skeleton.Avatar active size="extra-small" className="shadow-sm" />}
|
||||
/>
|
||||
<div className="ml-1.5 mr-1">
|
||||
<Skeleton.Title style={{ width: styleState.isMobile ? 15 : 50, height: 12 }} />
|
||||
<Skeleton
|
||||
loading={true}
|
||||
active
|
||||
placeholder={
|
||||
<Skeleton.Title
|
||||
active
|
||||
style={{ width: isMobile ? 15 : 50, height: 12 }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -366,7 +395,7 @@ const HeaderBar = () => {
|
||||
const registerButtonTextSpanClass = "!text-xs !text-white !p-1.5";
|
||||
|
||||
if (showRegisterButton) {
|
||||
if (styleState.isMobile) {
|
||||
if (isMobile) {
|
||||
loginButtonClasses += " !rounded-full";
|
||||
} else {
|
||||
loginButtonClasses += " !rounded-l-full !rounded-r-none";
|
||||
@@ -414,7 +443,7 @@ const HeaderBar = () => {
|
||||
<NoticeModal
|
||||
visible={noticeVisible}
|
||||
onClose={handleNoticeClose}
|
||||
isMobile={styleState.isMobile}
|
||||
isMobile={isMobile}
|
||||
defaultTab={unreadCount > 0 ? 'system' : 'inApp'}
|
||||
unreadKeys={getUnreadKeys()}
|
||||
/>
|
||||
@@ -425,18 +454,18 @@ const HeaderBar = () => {
|
||||
<Button
|
||||
icon={
|
||||
isConsoleRoute
|
||||
? (styleState.showSider ? <IconClose className="text-lg" /> : <IconMenu className="text-lg" />)
|
||||
? ((isMobile ? drawerOpen : collapsed) ? <IconClose className="text-lg" /> : <IconMenu className="text-lg" />)
|
||||
: (mobileMenuOpen ? <IconClose className="text-lg" /> : <IconMenu className="text-lg" />)
|
||||
}
|
||||
aria-label={
|
||||
isConsoleRoute
|
||||
? (styleState.showSider ? t('关闭侧边栏') : t('打开侧边栏'))
|
||||
? ((isMobile ? drawerOpen : collapsed) ? t('关闭侧边栏') : t('打开侧边栏'))
|
||||
: (mobileMenuOpen ? t('关闭菜单') : t('打开菜单'))
|
||||
}
|
||||
onClick={() => {
|
||||
if (isConsoleRoute) {
|
||||
// 控制侧边栏的显示/隐藏,无论是否移动设备
|
||||
styleDispatch(styleActions.toggleSider());
|
||||
isMobile ? onMobileMenuToggle() : toggleCollapsed();
|
||||
} else {
|
||||
// 控制HeaderBar自己的移动菜单
|
||||
setMobileMenuOpen(!mobileMenuOpen);
|
||||
@@ -448,22 +477,35 @@ const HeaderBar = () => {
|
||||
/>
|
||||
</div>
|
||||
<Link to="/" onClick={() => handleNavLinkClick('home')} className="flex items-center gap-2 group ml-2">
|
||||
{isLoading ? (
|
||||
<Skeleton.Image className="h-7 md:h-8 !rounded-full" style={{ width: 32, height: 32 }} />
|
||||
) : (
|
||||
<Skeleton
|
||||
loading={isLoading}
|
||||
active
|
||||
placeholder={
|
||||
<Skeleton.Image
|
||||
active
|
||||
className="h-7 md:h-8 !rounded-full"
|
||||
style={{ width: 32, height: 32 }}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<img src={logo} alt="logo" className="h-7 md:h-8 transition-transform duration-300 ease-in-out group-hover:scale-105 rounded-full" />
|
||||
)}
|
||||
</Skeleton>
|
||||
<div className="hidden md:flex items-center gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
{isLoading ? (
|
||||
<Skeleton.Title style={{ width: 120, height: 24 }} />
|
||||
) : (
|
||||
<Typography.Title heading={4} className="!text-lg !font-semibold !mb-0
|
||||
bg-gradient-to-r from-blue-500 to-purple-500 dark:from-blue-400 dark:to-purple-400
|
||||
bg-clip-text text-transparent">
|
||||
<Skeleton
|
||||
loading={isLoading}
|
||||
active
|
||||
placeholder={
|
||||
<Skeleton.Title
|
||||
active
|
||||
style={{ width: 120, height: 24 }}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Typography.Title heading={4} className="!text-lg !font-semibold !mb-0">
|
||||
{systemName}
|
||||
</Typography.Title>
|
||||
)}
|
||||
</Skeleton>
|
||||
{(isSelfUseMode || isDemoSiteMode) && !isLoading && (
|
||||
<Tag
|
||||
color={isSelfUseMode ? 'purple' : 'blue'}
|
||||
|
||||
@@ -4,8 +4,9 @@ import SiderBar from './SiderBar.js';
|
||||
import App from '../../App.js';
|
||||
import FooterBar from './Footer.js';
|
||||
import { ToastContainer } from 'react-toastify';
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
import { useStyle } from '../../context/Style/index.js';
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { useIsMobile } from '../../hooks/useIsMobile.js';
|
||||
import { useSidebarCollapsed } from '../../hooks/useSidebarCollapsed.js';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { API, getLogo, getSystemName, showError, setStatusData } from '../../helpers/index.js';
|
||||
import { UserContext } from '../../context/User/index.js';
|
||||
@@ -14,9 +15,11 @@ import { useLocation } from 'react-router-dom';
|
||||
const { Sider, Content, Header } = Layout;
|
||||
|
||||
const PageLayout = () => {
|
||||
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',
|
||||
}}
|
||||
>
|
||||
<Header
|
||||
@@ -90,16 +101,16 @@ const PageLayout = () => {
|
||||
zIndex: 100,
|
||||
}}
|
||||
>
|
||||
<HeaderBar />
|
||||
<HeaderBar onMobileMenuToggle={() => setDrawerOpen(prev => !prev)} drawerOpen={drawerOpen} />
|
||||
</Header>
|
||||
<Layout
|
||||
style={{
|
||||
overflow: styleState.isMobile ? 'visible' : 'auto',
|
||||
overflow: isMobile ? 'visible' : 'auto',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
{styleState.showSider && (
|
||||
{showSider && (
|
||||
<Sider
|
||||
style={{
|
||||
position: 'fixed',
|
||||
@@ -109,21 +120,15 @@ const PageLayout = () => {
|
||||
border: 'none',
|
||||
paddingRight: '0',
|
||||
height: 'calc(100vh - 64px)',
|
||||
width: 'var(--sidebar-current-width)',
|
||||
}}
|
||||
>
|
||||
<SiderBar />
|
||||
<SiderBar onNavigate={() => { if (isMobile) setDrawerOpen(false); }} />
|
||||
</Sider>
|
||||
)}
|
||||
<Layout
|
||||
style={{
|
||||
marginLeft: styleState.isMobile
|
||||
? '0'
|
||||
: styleState.showSider
|
||||
? styleState.siderCollapsed
|
||||
? '60px'
|
||||
: '180px'
|
||||
: '0',
|
||||
transition: 'margin-left 0.3s ease',
|
||||
marginLeft: isMobile ? '0' : showSider ? 'var(--sidebar-current-width)' : '0',
|
||||
flex: '1 1 auto',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
@@ -132,9 +137,9 @@ const PageLayout = () => {
|
||||
<Content
|
||||
style={{
|
||||
flex: '1 0 auto',
|
||||
overflowY: styleState.isMobile ? 'visible' : 'hidden',
|
||||
overflowY: isMobile ? 'visible' : 'hidden',
|
||||
WebkitOverflowScrolling: 'touch',
|
||||
padding: shouldInnerPadding ? (styleState.isMobile ? '5px' : '24px') : '0',
|
||||
padding: shouldInnerPadding ? (isMobile ? '5px' : '24px') : '0',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user