diff --git a/controller/relay-image.go b/controller/relay-image.go index 735976e..7421621 100644 --- a/controller/relay-image.go +++ b/controller/relay-image.go @@ -11,11 +11,10 @@ import ( "net/http" "one-api/common" "one-api/model" + "strings" ) func relayImageHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { - imageModel := "dall-e" - tokenId := c.GetInt("token_id") channelType := c.GetInt("channel") channelId := c.GetInt("channel_id") @@ -31,14 +30,21 @@ func relayImageHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode } } + if imageRequest.Model == "" { + imageRequest.Model = "dall-e" + } // Prompt validation if imageRequest.Prompt == "" { return errorWrapper(errors.New("prompt is required"), "required_field_missing", http.StatusBadRequest) } + if strings.Contains(imageRequest.Size, "×") { + return errorWrapper(errors.New("size an unexpected error occurred in the parameter, please use 'x' instead of the multiplication sign '×'"), "invalid_field_value", http.StatusBadRequest) + } // Not "256x256", "512x512", or "1024x1024" - if imageRequest.Size != "" && imageRequest.Size != "256x256" && imageRequest.Size != "512x512" && imageRequest.Size != "1024x1024" { - return errorWrapper(errors.New("size must be one of 256x256, 512x512, or 1024x1024"), "invalid_field_value", http.StatusBadRequest) + if imageRequest.Size != "" && imageRequest.Size != "256x256" && imageRequest.Size != "512x512" && imageRequest.Size != "1024x1024" && + (imageRequest.Model == "dall-e-3" && (imageRequest.Size != "1024x1792" && imageRequest.Size != "1792x1024")) { + return errorWrapper(errors.New("size must be one of 256x256, 512x512, or 1024x1024, dall-e-3 1024x1792 or 1792x1024"), "invalid_field_value", http.StatusBadRequest) } // N should between 1 and 10 @@ -55,8 +61,8 @@ func relayImageHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode if err != nil { return errorWrapper(err, "unmarshal_model_mapping_failed", http.StatusInternalServerError) } - if modelMap[imageModel] != "" { - imageModel = modelMap[imageModel] + if modelMap[imageRequest.Model] != "" { + imageRequest.Model = modelMap[imageRequest.Model] isModelMapped = true } } @@ -77,7 +83,7 @@ func relayImageHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode requestBody = c.Request.Body } - modelRatio := common.GetModelRatio(imageModel) + modelRatio := common.GetModelRatio(imageRequest.Model) groupRatio := common.GetGroupRatio(group) ratio := modelRatio * groupRatio userQuota, err := model.CacheGetUserQuota(userId) @@ -90,8 +96,19 @@ func relayImageHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode sizeRatio = 1.125 } else if imageRequest.Size == "1024x1024" { sizeRatio = 1.25 + } else if imageRequest.Size == "1024x1792" || imageRequest.Size == "1792x1024" { + sizeRatio = 2.5 } - quota := int(ratio*sizeRatio*1000) * imageRequest.N + + qualityRatio := 1.0 + if imageRequest.Model == "dall-e-3" && imageRequest.Quality == "hd" { + qualityRatio = 2.0 + if imageRequest.Size == "1024×1792" || imageRequest.Size == "1792×1024" { + qualityRatio = 1.5 + } + } + + quota := int(ratio*sizeRatio*qualityRatio*1000) * imageRequest.N if consumeQuota && userQuota-quota < 0 { return errorWrapper(errors.New("user quota is not enough"), "insufficient_user_quota", http.StatusForbidden) @@ -120,7 +137,6 @@ func relayImageHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode return errorWrapper(err, "close_request_body_failed", http.StatusInternalServerError) } var textResponse ImageResponse - defer func(ctx context.Context) { if consumeQuota { err := model.PostConsumeTokenQuota(tokenId, quota) @@ -134,7 +150,7 @@ func relayImageHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode if quota != 0 { tokenName := c.GetString("token_name") logContent := fmt.Sprintf("模型倍率 %.2f,分组倍率 %.2f", modelRatio, groupRatio) - model.RecordConsumeLog(ctx, userId, channelId, 0, 0, imageModel, tokenName, quota, logContent, tokenId) + model.RecordConsumeLog(ctx, userId, channelId, 0, 0, imageRequest.Model, tokenName, quota, logContent, tokenId) model.UpdateUserUsedQuotaAndRequestCount(userId, quota) channelId := c.GetInt("channel_id") model.UpdateChannelUsedQuota(channelId, quota) diff --git a/controller/relay.go b/controller/relay.go index cd9d6bf..c505c22 100644 --- a/controller/relay.go +++ b/controller/relay.go @@ -85,9 +85,11 @@ type TextRequest struct { } type ImageRequest struct { - Prompt string `json:"prompt"` - N int `json:"n"` - Size string `json:"size"` + Model string `json:"model"` + Quality string `json:"quality"` + Prompt string `json:"prompt"` + N int `json:"n"` + Size string `json:"size"` } type AudioResponse struct { diff --git a/model/redemption.go b/model/redemption.go index f16412b..5e44817 100644 --- a/model/redemption.go +++ b/model/redemption.go @@ -17,6 +17,7 @@ type Redemption struct { CreatedTime int64 `json:"created_time" gorm:"bigint"` RedeemedTime int64 `json:"redeemed_time" gorm:"bigint"` Count int `json:"count" gorm:"-:all"` // only for api request + UsedUserId int `json:"used_user_id"` } func GetAllRedemptions(startIdx int, num int) ([]*Redemption, error) { @@ -69,6 +70,7 @@ func Redeem(key string, userId int) (quota int, err error) { } redemption.RedeemedTime = common.GetTimestamp() redemption.Status = common.RedemptionCodeStatusUsed + redemption.UsedUserId = userId err = tx.Save(redemption).Error return err }) diff --git a/web/src/components/LogsTable.js b/web/src/components/LogsTable.js index 0101417..e2aa8aa 100644 --- a/web/src/components/LogsTable.js +++ b/web/src/components/LogsTable.js @@ -1,6 +1,6 @@ import React, {useEffect, useState} from 'react'; import {Label} from 'semantic-ui-react'; -import {API, isAdmin, showError, timestamp2string} from '../helpers'; +import {API, copy, isAdmin, showError, showSuccess, timestamp2string} from '../helpers'; import {Table, Avatar, Tag, Form, Button, Layout, Select, Popover, Modal} from '@douyinfe/semi-ui'; import {ITEMS_PER_PAGE} from '../constants'; @@ -106,7 +106,9 @@ const LogsTable = () => { return ( record.type === 0 || record.type === 2 ?
- { {text} } + { + copyText(text) + }}> {text}
: <> @@ -131,7 +133,9 @@ const LogsTable = () => { return ( record.type === 0 || record.type === 2 ?
- { {text} } + { + copyText(text) + }}> {text}
: <> @@ -329,6 +333,15 @@ const LogsTable = () => { await loadLogs(0); }; + const copyText = async (text) => { + if (await copy(text)) { + showSuccess('已复制:' + text); + } else { + // setSearchKeyword(text); + Modal.error({ title: '无法复制到剪贴板,请手动复制', content: text }); + } + } + useEffect(() => { refresh().then(); }, [logType]); @@ -397,7 +410,7 @@ const LogsTable = () => { handlePageChange(value, 'model_name')}/> + onChange={value => handleInputChange(value, 'model_name')}/> 未使用; + return 未使用; case 2: - return ; + return 已禁用 ; case 3: - return ; + return 已使用 ; default: - return ; + return 未知状态 ; } } const RedemptionsTable = () => { + const columns = [ + { + title: 'ID', + dataIndex: 'id', + }, + { + title: '名称', + dataIndex: 'name', + }, + { + title: '状态', + dataIndex: 'status', + key: 'status', + render: (text, record, index) => { + return ( +
+ {renderStatus(text)} +
+ ); + }, + }, + { + title: '额度', + dataIndex: 'quota', + render: (text, record, index) => { + return ( +
+ {renderQuota(parseInt(text))} +
+ ); + }, + }, + { + title: '创建时间', + dataIndex: 'created_time', + render: (text, record, index) => { + return ( +
+ {renderTimestamp(text)} +
+ ); + }, + }, + { + title: '', + dataIndex: 'operate', + render: (text, record, index) => ( +
+ + + + + { + manageRedemption(record.id, 'delete', record).then( + () => { + removeRecord(record.key); + } + ) + }} + > + + + { + record.status === 1 ? + : + + } + {/**/} +
+ ), + }, + ]; + const [redemptions, setRedemptions] = useState([]); const [loading, setLoading] = useState(true); const [activePage, setActivePage] = useState(1); const [searchKeyword, setSearchKeyword] = useState(''); const [searching, setSearching] = useState(false); + const [tokenCount, setTokenCount] = useState(ITEMS_PER_PAGE); + const [selectedKeys, setSelectedKeys] = useState([]); const loadRedemptions = async (startIdx) => { const res = await API.get(`/api/redemption/?p=${startIdx}`); @@ -51,6 +163,27 @@ const RedemptionsTable = () => { setLoading(false); }; + const removeRecord = key => { + let newDataSource = [...redemptions]; + if (key != null) { + let idx = newDataSource.findIndex(data => data.key === key); + + if (idx > -1) { + newDataSource.splice(idx, 1); + setRedemptions(newDataSource); + } + } + }; + + const copyText = async (text) => { + if (await copy(text)) { + showSuccess('已复制到剪贴板!'); + } else { + // setSearchKeyword(text); + Modal.error({ title: '无法复制到剪贴板,请手动复制', content: text }); + } + } + const onPaginationChange = (e, { activePage }) => { (async () => { if (activePage === Math.ceil(redemptions.length / ITEMS_PER_PAGE) + 1) { @@ -69,7 +202,7 @@ const RedemptionsTable = () => { }); }, []); - const manageRedemption = async (id, action, idx) => { + const manageRedemption = async (id, action, record) => { let data = { id }; let res; switch (action) { @@ -90,11 +223,11 @@ const RedemptionsTable = () => { showSuccess('操作成功完成!'); let redemption = res.data.data; let newRedemptions = [...redemptions]; - let realIdx = (activePage - 1) * ITEMS_PER_PAGE + idx; + // let realIdx = (activePage - 1) * ITEMS_PER_PAGE + idx; if (action === 'delete') { - newRedemptions[realIdx].deleted = true; + } else { - newRedemptions[realIdx].status = redemption.status; + record.status = redemption.status; } setRedemptions(newRedemptions); } else { @@ -139,6 +272,25 @@ const RedemptionsTable = () => { setLoading(false); }; + const handlePageChange = page => { + setActivePage(page); + if (page === Math.ceil(redemptions.length / ITEMS_PER_PAGE) + 1) { + // In this case we have to load more data and then append them. + loadRedemptions(page - 1).then(r => {}); + } + }; + + let pageData = redemptions.slice((activePage - 1) * ITEMS_PER_PAGE, activePage * ITEMS_PER_PAGE); + const rowSelection = { + onSelect: (record, selected) => { + }, + onSelectAll: (selected, selectedRows) => { + }, + onChange: (selectedRowKeys, selectedRows) => { + setSelectedKeys(selectedRows); + }, + }; + return ( <>
@@ -153,159 +305,19 @@ const RedemptionsTable = () => { />
- - - - { - sortRedemption('id'); - }} - > - ID - - { - sortRedemption('name'); - }} - > - 名称 - - { - sortRedemption('status'); - }} - > - 状态 - - { - sortRedemption('quota'); - }} - > - 额度 - - { - sortRedemption('created_time'); - }} - > - 创建时间 - - { - sortRedemption('redeemed_time'); - }} - > - 兑换时间 - - 操作 - - - - - {redemptions - .slice( - (activePage - 1) * ITEMS_PER_PAGE, - activePage * ITEMS_PER_PAGE - ) - .map((redemption, idx) => { - if (redemption.deleted) return <>; - return ( - - {redemption.id} - {redemption.name ? redemption.name : '无'} - {renderStatus(redemption.status)} - {renderQuota(redemption.quota)} - {renderTimestamp(redemption.created_time)} - {redemption.redeemed_time ? renderTimestamp(redemption.redeemed_time) : "尚未兑换"} - -
- - - 删除 - - } - on='click' - flowing - hoverable - > - - - - -
-
-
- ); - })} -
- - - - - - - - - +
`第 ${page.currentStart} - ${page.currentEnd} 条,共 ${redemptions.length} 条`, + // onPageSizeChange: (size) => { + // setPageSize(size); + // setActivePage(1); + // }, + onPageChange: handlePageChange, + }} loading={loading} rowSelection={rowSelection}>
);