mirror of
https://github.com/songquanpeng/one-api.git
synced 2025-10-27 03:43:43 +08:00
Compare commits
11 Commits
v0.4.7
...
v0.4.8-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0941e294bf | ||
|
|
431d505f79 | ||
|
|
f0dc7f3f06 | ||
|
|
99fed1f850 | ||
|
|
4dc5388a80 | ||
|
|
f81f4c60b2 | ||
|
|
c613d8b6b2 | ||
|
|
7adac1c09c | ||
|
|
6f05128368 | ||
|
|
9b178a28a3 | ||
|
|
4a6a7f4635 |
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
# One API
|
# One API
|
||||||
|
|
||||||
_✨ The all-in-one OpenAI interface, integrates various API access methods, ready to use ✨_
|
_✨ An OpenAI key management & redistribution system, easy to deploy & use ✨_
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -12,14 +12,16 @@ total_time=0
|
|||||||
times=()
|
times=()
|
||||||
|
|
||||||
for ((i=1; i<=count; i++)); do
|
for ((i=1; i<=count; i++)); do
|
||||||
result=$(curl -o /dev/null -s -w %{time_total}\\n \
|
result=$(curl -o /dev/null -s -w "%{http_code} %{time_total}\\n" \
|
||||||
https://"$domain"/v1/chat/completions \
|
https://"$domain"/v1/chat/completions \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-H "Authorization: Bearer $key" \
|
-H "Authorization: Bearer $key" \
|
||||||
-d '{"messages": [{"content": "echo hi", "role": "user"}], "model": "gpt-3.5-turbo", "stream": false, "max_tokens": 1}')
|
-d '{"messages": [{"content": "echo hi", "role": "user"}], "model": "gpt-3.5-turbo", "stream": false, "max_tokens": 1}')
|
||||||
echo "$result"
|
http_code=$(echo "$result" | awk '{print $1}')
|
||||||
total_time=$(bc <<< "$total_time + $result")
|
time=$(echo "$result" | awk '{print $2}')
|
||||||
times+=("$result")
|
echo "HTTP status code: $http_code, Time taken: $time"
|
||||||
|
total_time=$(bc <<< "$total_time + $time")
|
||||||
|
times+=("$time")
|
||||||
done
|
done
|
||||||
|
|
||||||
average_time=$(echo "scale=4; $total_time / $count" | bc)
|
average_time=$(echo "scale=4; $total_time / $count" | bc)
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ var ModelRatio = map[string]float64{
|
|||||||
"curie": 10,
|
"curie": 10,
|
||||||
"babbage": 10,
|
"babbage": 10,
|
||||||
"ada": 10,
|
"ada": 10,
|
||||||
"text-embedding-ada-002": 0.2,
|
"text-embedding-ada-002": 0.05,
|
||||||
"text-search-ada-doc-001": 10,
|
"text-search-ada-doc-001": 10,
|
||||||
"text-moderation-stable": 0.1,
|
"text-moderation-stable": 0.1,
|
||||||
"text-moderation-latest": 0.1,
|
"text-moderation-latest": 0.1,
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ func GetSubscription(c *gin.Context) {
|
|||||||
amount /= common.QuotaPerUnit
|
amount /= common.QuotaPerUnit
|
||||||
}
|
}
|
||||||
if token != nil && token.UnlimitedQuota {
|
if token != nil && token.UnlimitedQuota {
|
||||||
amount = 99999999.9999
|
amount = 100000000
|
||||||
}
|
}
|
||||||
subscription := OpenAISubscriptionResponse{
|
subscription := OpenAISubscriptionResponse{
|
||||||
Object: "billing_subscription",
|
Object: "billing_subscription",
|
||||||
|
|||||||
@@ -224,6 +224,24 @@ func init() {
|
|||||||
Root: "text-moderation-stable",
|
Root: "text-moderation-stable",
|
||||||
Parent: nil,
|
Parent: nil,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Id: "text-davinci-edit-001",
|
||||||
|
Object: "model",
|
||||||
|
Created: 1677649963,
|
||||||
|
OwnedBy: "openai",
|
||||||
|
Permission: permission,
|
||||||
|
Root: "text-davinci-edit-001",
|
||||||
|
Parent: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Id: "code-davinci-edit-001",
|
||||||
|
Object: "model",
|
||||||
|
Created: 1677649963,
|
||||||
|
OwnedBy: "openai",
|
||||||
|
Permission: permission,
|
||||||
|
Root: "code-davinci-edit-001",
|
||||||
|
Parent: nil,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
openAIModelsMap = make(map[string]OpenAIModels)
|
openAIModelsMap = make(map[string]OpenAIModels)
|
||||||
for _, model := range openAIModels {
|
for _, model := range openAIModels {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
|
|||||||
return errorWrapper(err, "bind_request_body_failed", http.StatusBadRequest)
|
return errorWrapper(err, "bind_request_body_failed", http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if relayMode == RelayModeModeration && textRequest.Model == "" {
|
if relayMode == RelayModeModerations && textRequest.Model == "" {
|
||||||
textRequest.Model = "text-moderation-latest"
|
textRequest.Model = "text-moderation-latest"
|
||||||
}
|
}
|
||||||
// request validation
|
// request validation
|
||||||
@@ -37,16 +37,34 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
|
|||||||
switch relayMode {
|
switch relayMode {
|
||||||
case RelayModeCompletions:
|
case RelayModeCompletions:
|
||||||
if textRequest.Prompt == "" {
|
if textRequest.Prompt == "" {
|
||||||
return errorWrapper(errors.New("prompt is required"), "required_field_missing", http.StatusBadRequest)
|
return errorWrapper(errors.New("field prompt is required"), "required_field_missing", http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
case RelayModeChatCompletions:
|
case RelayModeChatCompletions:
|
||||||
if len(textRequest.Messages) == 0 {
|
if textRequest.Messages == nil || len(textRequest.Messages) == 0 {
|
||||||
return errorWrapper(errors.New("messages is required"), "required_field_missing", http.StatusBadRequest)
|
return errorWrapper(errors.New("field messages is required"), "required_field_missing", http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
case RelayModeEmbeddings:
|
case RelayModeEmbeddings:
|
||||||
case RelayModeModeration:
|
case RelayModeModerations:
|
||||||
if textRequest.Input == "" {
|
if textRequest.Input == "" {
|
||||||
return errorWrapper(errors.New("input is required"), "required_field_missing", http.StatusBadRequest)
|
return errorWrapper(errors.New("field input is required"), "required_field_missing", http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
case RelayModeEdits:
|
||||||
|
if textRequest.Instruction == "" {
|
||||||
|
return errorWrapper(errors.New("field instruction is required"), "required_field_missing", http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// map model name
|
||||||
|
modelMapping := c.GetString("model_mapping")
|
||||||
|
isModelMapped := false
|
||||||
|
if modelMapping != "" {
|
||||||
|
modelMap := make(map[string]string)
|
||||||
|
err := json.Unmarshal([]byte(modelMapping), &modelMap)
|
||||||
|
if err != nil {
|
||||||
|
return errorWrapper(err, "unmarshal_model_mapping_failed", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
if modelMap[textRequest.Model] != "" {
|
||||||
|
textRequest.Model = modelMap[textRequest.Model]
|
||||||
|
isModelMapped = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
baseURL := common.ChannelBaseURLs[channelType]
|
baseURL := common.ChannelBaseURLs[channelType]
|
||||||
@@ -84,7 +102,7 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
|
|||||||
promptTokens = countTokenMessages(textRequest.Messages, textRequest.Model)
|
promptTokens = countTokenMessages(textRequest.Messages, textRequest.Model)
|
||||||
case RelayModeCompletions:
|
case RelayModeCompletions:
|
||||||
promptTokens = countTokenInput(textRequest.Prompt, textRequest.Model)
|
promptTokens = countTokenInput(textRequest.Prompt, textRequest.Model)
|
||||||
case RelayModeModeration:
|
case RelayModeModerations:
|
||||||
promptTokens = countTokenInput(textRequest.Input, textRequest.Model)
|
promptTokens = countTokenInput(textRequest.Input, textRequest.Model)
|
||||||
}
|
}
|
||||||
preConsumedTokens := common.PreConsumedQuota
|
preConsumedTokens := common.PreConsumedQuota
|
||||||
@@ -110,7 +128,17 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
|
|||||||
return errorWrapper(err, "pre_consume_token_quota_failed", http.StatusForbidden)
|
return errorWrapper(err, "pre_consume_token_quota_failed", http.StatusForbidden)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
req, err := http.NewRequest(c.Request.Method, fullRequestURL, c.Request.Body)
|
var requestBody io.Reader
|
||||||
|
if isModelMapped {
|
||||||
|
jsonStr, err := json.Marshal(textRequest)
|
||||||
|
if err != nil {
|
||||||
|
return errorWrapper(err, "marshal_text_request_failed", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
requestBody = bytes.NewBuffer(jsonStr)
|
||||||
|
} else {
|
||||||
|
requestBody = c.Request.Body
|
||||||
|
}
|
||||||
|
req, err := http.NewRequest(c.Request.Method, fullRequestURL, requestBody)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorWrapper(err, "new_request_failed", http.StatusInternalServerError)
|
return errorWrapper(err, "new_request_failed", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
@@ -144,7 +172,10 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
|
|||||||
defer func() {
|
defer func() {
|
||||||
if consumeQuota {
|
if consumeQuota {
|
||||||
quota := 0
|
quota := 0
|
||||||
completionRatio := 1.333333 // default for gpt-3
|
completionRatio := 1.0
|
||||||
|
if strings.HasPrefix(textRequest.Model, "gpt-3.5") {
|
||||||
|
completionRatio = 1.333333
|
||||||
|
}
|
||||||
if strings.HasPrefix(textRequest.Model, "gpt-4") {
|
if strings.HasPrefix(textRequest.Model, "gpt-4") {
|
||||||
completionRatio = 2
|
completionRatio = 2
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,22 +19,24 @@ const (
|
|||||||
RelayModeChatCompletions
|
RelayModeChatCompletions
|
||||||
RelayModeCompletions
|
RelayModeCompletions
|
||||||
RelayModeEmbeddings
|
RelayModeEmbeddings
|
||||||
RelayModeModeration
|
RelayModeModerations
|
||||||
RelayModeImagesGenerations
|
RelayModeImagesGenerations
|
||||||
|
RelayModeEdits
|
||||||
)
|
)
|
||||||
|
|
||||||
// https://platform.openai.com/docs/api-reference/chat
|
// https://platform.openai.com/docs/api-reference/chat
|
||||||
|
|
||||||
type GeneralOpenAIRequest struct {
|
type GeneralOpenAIRequest struct {
|
||||||
Model string `json:"model"`
|
Model string `json:"model,omitempty"`
|
||||||
Messages []Message `json:"messages"`
|
Messages []Message `json:"messages,omitempty"`
|
||||||
Prompt any `json:"prompt"`
|
Prompt any `json:"prompt,omitempty"`
|
||||||
Stream bool `json:"stream"`
|
Stream bool `json:"stream,omitempty"`
|
||||||
MaxTokens int `json:"max_tokens"`
|
MaxTokens int `json:"max_tokens,omitempty"`
|
||||||
Temperature float64 `json:"temperature"`
|
Temperature float64 `json:"temperature,omitempty"`
|
||||||
TopP float64 `json:"top_p"`
|
TopP float64 `json:"top_p,omitempty"`
|
||||||
N int `json:"n"`
|
N int `json:"n,omitempty"`
|
||||||
Input any `json:"input"`
|
Input any `json:"input,omitempty"`
|
||||||
|
Instruction string `json:"instruction,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChatRequest struct {
|
type ChatRequest struct {
|
||||||
@@ -99,9 +101,11 @@ func Relay(c *gin.Context) {
|
|||||||
} else if strings.HasPrefix(c.Request.URL.Path, "/v1/embeddings") {
|
} else if strings.HasPrefix(c.Request.URL.Path, "/v1/embeddings") {
|
||||||
relayMode = RelayModeEmbeddings
|
relayMode = RelayModeEmbeddings
|
||||||
} else if strings.HasPrefix(c.Request.URL.Path, "/v1/moderations") {
|
} else if strings.HasPrefix(c.Request.URL.Path, "/v1/moderations") {
|
||||||
relayMode = RelayModeModeration
|
relayMode = RelayModeModerations
|
||||||
} else if strings.HasPrefix(c.Request.URL.Path, "/v1/images/generations") {
|
} else if strings.HasPrefix(c.Request.URL.Path, "/v1/images/generations") {
|
||||||
relayMode = RelayModeImagesGenerations
|
relayMode = RelayModeImagesGenerations
|
||||||
|
} else if strings.HasPrefix(c.Request.URL.Path, "/v1/edits") {
|
||||||
|
relayMode = RelayModeEdits
|
||||||
}
|
}
|
||||||
var err *OpenAIErrorWithStatusCode
|
var err *OpenAIErrorWithStatusCode
|
||||||
switch relayMode {
|
switch relayMode {
|
||||||
|
|||||||
11
main.go
11
main.go
@@ -4,7 +4,6 @@ import (
|
|||||||
"embed"
|
"embed"
|
||||||
"github.com/gin-contrib/sessions"
|
"github.com/gin-contrib/sessions"
|
||||||
"github.com/gin-contrib/sessions/cookie"
|
"github.com/gin-contrib/sessions/cookie"
|
||||||
"github.com/gin-contrib/sessions/redis"
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"one-api/common"
|
"one-api/common"
|
||||||
"one-api/controller"
|
"one-api/controller"
|
||||||
@@ -82,14 +81,8 @@ func main() {
|
|||||||
server.Use(middleware.CORS())
|
server.Use(middleware.CORS())
|
||||||
|
|
||||||
// Initialize session store
|
// Initialize session store
|
||||||
if common.RedisEnabled {
|
store := cookie.NewStore([]byte(common.SessionSecret))
|
||||||
opt := common.ParseRedisOption()
|
server.Use(sessions.Sessions("session", store))
|
||||||
store, _ := redis.NewStore(opt.MinIdleConns, opt.Network, opt.Addr, opt.Password, []byte(common.SessionSecret))
|
|
||||||
server.Use(sessions.Sessions("session", store))
|
|
||||||
} else {
|
|
||||||
store := cookie.NewStore([]byte(common.SessionSecret))
|
|
||||||
server.Use(sessions.Sessions("session", store))
|
|
||||||
}
|
|
||||||
|
|
||||||
router.SetRouter(server, buildFS, indexPage)
|
router.SetRouter(server, buildFS, indexPage)
|
||||||
var port = os.Getenv("PORT")
|
var port = os.Getenv("PORT")
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ func Distribute() func(c *gin.Context) {
|
|||||||
c.Set("channel", channel.Type)
|
c.Set("channel", channel.Type)
|
||||||
c.Set("channel_id", channel.Id)
|
c.Set("channel_id", channel.Id)
|
||||||
c.Set("channel_name", channel.Name)
|
c.Set("channel_name", channel.Name)
|
||||||
|
c.Set("model_mapping", channel.ModelMapping)
|
||||||
c.Request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", channel.Key))
|
c.Request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", channel.Key))
|
||||||
c.Set("base_url", channel.BaseURL)
|
c.Set("base_url", channel.BaseURL)
|
||||||
if channel.Type == common.ChannelTypeAzure {
|
if channel.Type == common.ChannelTypeAzure {
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ var channelSyncLock sync.RWMutex
|
|||||||
func InitChannelCache() {
|
func InitChannelCache() {
|
||||||
newChannelId2channel := make(map[int]*Channel)
|
newChannelId2channel := make(map[int]*Channel)
|
||||||
var channels []*Channel
|
var channels []*Channel
|
||||||
DB.Find(&channels)
|
DB.Where("status = ?", common.ChannelStatusEnabled).Find(&channels)
|
||||||
for _, channel := range channels {
|
for _, channel := range channels {
|
||||||
newChannelId2channel[channel.Id] = channel
|
newChannelId2channel[channel.Id] = channel
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ type Channel struct {
|
|||||||
Models string `json:"models"`
|
Models string `json:"models"`
|
||||||
Group string `json:"group" gorm:"type:varchar(32);default:'default'"`
|
Group string `json:"group" gorm:"type:varchar(32);default:'default'"`
|
||||||
UsedQuota int64 `json:"used_quota" gorm:"bigint;default:0"`
|
UsedQuota int64 `json:"used_quota" gorm:"bigint;default:0"`
|
||||||
|
ModelMapping string `json:"model_mapping" gorm:"type:varchar(1024);default:''"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetAllChannels(startIdx int, num int, selectAll bool) ([]*Channel, error) {
|
func GetAllChannels(startIdx int, num int, selectAll bool) ([]*Channel, error) {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ func SetRelayRouter(router *gin.Engine) {
|
|||||||
{
|
{
|
||||||
relayV1Router.POST("/completions", controller.Relay)
|
relayV1Router.POST("/completions", controller.Relay)
|
||||||
relayV1Router.POST("/chat/completions", controller.Relay)
|
relayV1Router.POST("/chat/completions", controller.Relay)
|
||||||
relayV1Router.POST("/edits", controller.RelayNotImplemented)
|
relayV1Router.POST("/edits", controller.Relay)
|
||||||
relayV1Router.POST("/images/generations", controller.RelayNotImplemented)
|
relayV1Router.POST("/images/generations", controller.RelayNotImplemented)
|
||||||
relayV1Router.POST("/images/edits", controller.RelayNotImplemented)
|
relayV1Router.POST("/images/edits", controller.RelayNotImplemented)
|
||||||
relayV1Router.POST("/images/variations", controller.RelayNotImplemented)
|
relayV1Router.POST("/images/variations", controller.RelayNotImplemented)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Button, Form, Header, Message, Segment } from 'semantic-ui-react';
|
import { Button, Form, Header, Message, Segment } from 'semantic-ui-react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { API, showError, showInfo, showSuccess } from '../../helpers';
|
import { API, showError, showInfo, showSuccess, verifyJSON } from '../../helpers';
|
||||||
import { CHANNEL_OPTIONS } from '../../constants';
|
import { CHANNEL_OPTIONS } from '../../constants';
|
||||||
|
|
||||||
const EditChannel = () => {
|
const EditChannel = () => {
|
||||||
@@ -15,6 +15,7 @@ const EditChannel = () => {
|
|||||||
key: '',
|
key: '',
|
||||||
base_url: '',
|
base_url: '',
|
||||||
other: '',
|
other: '',
|
||||||
|
model_mapping:'',
|
||||||
models: [],
|
models: [],
|
||||||
groups: ['default']
|
groups: ['default']
|
||||||
};
|
};
|
||||||
@@ -42,6 +43,9 @@ const EditChannel = () => {
|
|||||||
} else {
|
} else {
|
||||||
data.groups = data.group.split(',');
|
data.groups = data.group.split(',');
|
||||||
}
|
}
|
||||||
|
if (data.model_mapping !== '') {
|
||||||
|
data.model_mapping = JSON.stringify(JSON.parse(data.model_mapping), null, 2);
|
||||||
|
}
|
||||||
setInputs(data);
|
setInputs(data);
|
||||||
} else {
|
} else {
|
||||||
showError(message);
|
showError(message);
|
||||||
@@ -94,6 +98,10 @@ const EditChannel = () => {
|
|||||||
showInfo('请至少选择一个模型!');
|
showInfo('请至少选择一个模型!');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (inputs.model_mapping !== "" && !verifyJSON(inputs.model_mapping)) {
|
||||||
|
showInfo('模型映射必须是合法的 JSON 格式!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
let localInputs = inputs;
|
let localInputs = inputs;
|
||||||
if (localInputs.base_url.endsWith('/')) {
|
if (localInputs.base_url.endsWith('/')) {
|
||||||
localInputs.base_url = localInputs.base_url.slice(0, localInputs.base_url.length - 1);
|
localInputs.base_url = localInputs.base_url.slice(0, localInputs.base_url.length - 1);
|
||||||
@@ -246,6 +254,17 @@ const EditChannel = () => {
|
|||||||
handleInputChange(null, { name: 'models', value: [] });
|
handleInputChange(null, { name: 'models', value: [] });
|
||||||
}}>清除所有模型</Button>
|
}}>清除所有模型</Button>
|
||||||
</div>
|
</div>
|
||||||
|
<Form.Field>
|
||||||
|
<Form.TextArea
|
||||||
|
label='模型映射'
|
||||||
|
placeholder={'为一个 JSON 文本,键为用户请求的模型名称,值为要替换的模型名称'}
|
||||||
|
name='model_mapping'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
value={inputs.model_mapping}
|
||||||
|
style={{ minHeight: 100, fontFamily: 'JetBrains Mono, Consolas' }}
|
||||||
|
autoComplete='new-password'
|
||||||
|
/>
|
||||||
|
</Form.Field>
|
||||||
{
|
{
|
||||||
batch ? <Form.Field>
|
batch ? <Form.Field>
|
||||||
<Form.TextArea
|
<Form.TextArea
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react';
|
|||||||
import { Button, Form, Header, Segment } from 'semantic-ui-react';
|
import { Button, Form, Header, Segment } from 'semantic-ui-react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { API, showError, showSuccess } from '../../helpers';
|
import { API, showError, showSuccess } from '../../helpers';
|
||||||
|
import { renderQuota, renderQuotaWithPrompt } from '../../helpers/render';
|
||||||
|
|
||||||
const EditUser = () => {
|
const EditUser = () => {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
@@ -134,7 +135,7 @@ const EditUser = () => {
|
|||||||
</Form.Field>
|
</Form.Field>
|
||||||
<Form.Field>
|
<Form.Field>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
label='剩余额度'
|
label={`剩余额度${renderQuotaWithPrompt(quota)}`}
|
||||||
name='quota'
|
name='quota'
|
||||||
placeholder={'请输入新的剩余额度'}
|
placeholder={'请输入新的剩余额度'}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
|
|||||||
Reference in New Issue
Block a user