Merge branch 'main' into private

This commit is contained in:
CaIon 2023-11-09 17:09:03 +08:00
commit f569ca270e
5 changed files with 224 additions and 179 deletions

View File

@ -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)

View File

@ -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 {

View File

@ -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
})

View File

@ -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 ?
<div>
{<Tag color='grey' size='large'> {text} </Tag>}
<Tag color='grey' size='large' onClick={()=>{
copyText(text)
}}> {text} </Tag>
</div>
:
<></>
@ -131,7 +133,9 @@ const LogsTable = () => {
return (
record.type === 0 || record.type === 2 ?
<div>
{<Tag color={stringToColor(text)} size='large'> {text} </Tag>}
<Tag color={stringToColor(text)} size='large' onClick={()=>{
copyText(text)
}}> {text} </Tag>
</div>
:
<></>
@ -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 = () => {
<Form.Input field="model_name" label='模型名称' style={{width: 176}} value={model_name}
placeholder='可选值'
name='model_name'
onChange={value => handlePageChange(value, 'model_name')}/>
onChange={value => handleInputChange(value, 'model_name')}/>
<Form.DatePicker field="start_timestamp" label='起始时间' style={{width: 272}}
value={start_timestamp} type='dateTime'
name='start_timestamp'

View File

@ -1,10 +1,11 @@
import React, { useEffect, useState } from 'react';
import { Button, Form, Label, Popup, Pagination, Table } from 'semantic-ui-react';
import { Form, Label, Popup, Pagination } from 'semantic-ui-react';
import { Link } from 'react-router-dom';
import { API, copy, showError, showInfo, showSuccess, showWarning, timestamp2string } from '../helpers';
import { ITEMS_PER_PAGE } from '../constants';
import { renderQuota } from '../helpers/render';
import {Button, Modal, Popconfirm, Popover, Table, Tag} from "@douyinfe/semi-ui";
function renderTimestamp(timestamp) {
return (
@ -17,22 +18,133 @@ function renderTimestamp(timestamp) {
function renderStatus(status) {
switch (status) {
case 1:
return <Label basic color='green'>未使用</Label>;
return <Tag color='green' size='large'>未使用</Tag>;
case 2:
return <Label basic color='red'> 已禁用 </Label>;
return <Tag color='red' size='large'> 已禁用 </Tag>;
case 3:
return <Label basic color='grey'> 已使用 </Label>;
return <Tag color='grey' size='large'> 已使用 </Tag>;
default:
return <Label basic color='black'> 未知状态 </Label>;
return <Tag color='black' size='large'> 未知状态 </Tag>;
}
}
const RedemptionsTable = () => {
const columns = [
{
title: 'ID',
dataIndex: 'id',
},
{
title: '名称',
dataIndex: 'name',
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
render: (text, record, index) => {
return (
<div>
{renderStatus(text)}
</div>
);
},
},
{
title: '额度',
dataIndex: 'quota',
render: (text, record, index) => {
return (
<div>
{renderQuota(parseInt(text))}
</div>
);
},
},
{
title: '创建时间',
dataIndex: 'created_time',
render: (text, record, index) => {
return (
<div>
{renderTimestamp(text)}
</div>
);
},
},
{
title: '',
dataIndex: 'operate',
render: (text, record, index) => (
<div>
<Popover
content={
record.key
}
style={{padding: 20}}
position="top"
>
<Button theme='light' type='tertiary' style={{marginRight: 1}}>查看</Button>
</Popover>
<Button theme='light' type='secondary' style={{marginRight: 1}}
onClick={async (text) => {
await copyText(record.key)
}}
>复制</Button>
<Popconfirm
title="确定是否要删除此令牌?"
content="此修改将不可逆"
okType={'danger'}
position={'left'}
onConfirm={() => {
manageRedemption(record.id, 'delete', record).then(
() => {
removeRecord(record.key);
}
)
}}
>
<Button theme='light' type='danger' style={{marginRight: 1}}>删除</Button>
</Popconfirm>
{
record.status === 1 ?
<Button theme='light' type='warning' style={{marginRight: 1}} onClick={
async () => {
manageRedemption(
record.id,
'disable',
record
)
}
}>禁用</Button> :
<Button theme='light' type='secondary' style={{marginRight: 1}} onClick={
async () => {
manageRedemption(
record.id,
'enable',
record
);
}
} disabled={record.status===3}>启用</Button>
}
{/*<Button theme='light' type='tertiary' style={{marginRight: 1}} onClick={*/}
{/* () => {*/}
{/* setEditingToken(record);*/}
{/* setShowEdit(true);*/}
{/* }*/}
{/*}>编辑</Button>*/}
</div>
),
},
];
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 (
<>
<Form onSubmit={searchRedemptions}>
@ -153,159 +305,19 @@ const RedemptionsTable = () => {
/>
</Form>
<Table basic compact size='small'>
<Table.Header>
<Table.Row>
<Table.HeaderCell
style={{ cursor: 'pointer' }}
onClick={() => {
sortRedemption('id');
}}
>
ID
</Table.HeaderCell>
<Table.HeaderCell
style={{ cursor: 'pointer' }}
onClick={() => {
sortRedemption('name');
}}
>
名称
</Table.HeaderCell>
<Table.HeaderCell
style={{ cursor: 'pointer' }}
onClick={() => {
sortRedemption('status');
}}
>
状态
</Table.HeaderCell>
<Table.HeaderCell
style={{ cursor: 'pointer' }}
onClick={() => {
sortRedemption('quota');
}}
>
额度
</Table.HeaderCell>
<Table.HeaderCell
style={{ cursor: 'pointer' }}
onClick={() => {
sortRedemption('created_time');
}}
>
创建时间
</Table.HeaderCell>
<Table.HeaderCell
style={{ cursor: 'pointer' }}
onClick={() => {
sortRedemption('redeemed_time');
}}
>
兑换时间
</Table.HeaderCell>
<Table.HeaderCell>操作</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{redemptions
.slice(
(activePage - 1) * ITEMS_PER_PAGE,
activePage * ITEMS_PER_PAGE
)
.map((redemption, idx) => {
if (redemption.deleted) return <></>;
return (
<Table.Row key={redemption.id}>
<Table.Cell>{redemption.id}</Table.Cell>
<Table.Cell>{redemption.name ? redemption.name : '无'}</Table.Cell>
<Table.Cell>{renderStatus(redemption.status)}</Table.Cell>
<Table.Cell>{renderQuota(redemption.quota)}</Table.Cell>
<Table.Cell>{renderTimestamp(redemption.created_time)}</Table.Cell>
<Table.Cell>{redemption.redeemed_time ? renderTimestamp(redemption.redeemed_time) : "尚未兑换"} </Table.Cell>
<Table.Cell>
<div>
<Button
size={'small'}
positive
onClick={async () => {
if (await copy(redemption.key)) {
showSuccess('已复制到剪贴板!');
} else {
showWarning('无法复制到剪贴板,请手动复制,已将兑换码填入搜索框。')
setSearchKeyword(redemption.key);
}
}}
>
复制
</Button>
<Popup
trigger={
<Button size='small' negative>
删除
</Button>
}
on='click'
flowing
hoverable
>
<Button
negative
onClick={() => {
manageRedemption(redemption.id, 'delete', idx);
}}
>
确认删除
</Button>
</Popup>
<Button
size={'small'}
disabled={redemption.status === 3} // used
onClick={() => {
manageRedemption(
redemption.id,
redemption.status === 1 ? 'disable' : 'enable',
idx
);
}}
>
{redemption.status === 1 ? '禁用' : '启用'}
</Button>
<Button
size={'small'}
as={Link}
to={'/redemption/edit/' + redemption.id}
>
编辑
</Button>
</div>
</Table.Cell>
</Table.Row>
);
})}
</Table.Body>
<Table.Footer>
<Table.Row>
<Table.HeaderCell colSpan='8'>
<Button size='small' as={Link} to='/redemption/add' loading={loading}>
添加新的兑换码
</Button>
<Pagination
floated='right'
activePage={activePage}
onPageChange={onPaginationChange}
size='small'
siblingRange={1}
totalPages={
Math.ceil(redemptions.length / ITEMS_PER_PAGE) +
(redemptions.length % ITEMS_PER_PAGE === 0 ? 1 : 0)
}
/>
</Table.HeaderCell>
</Table.Row>
</Table.Footer>
<Table style={{marginTop: 20}} columns={columns} dataSource={pageData} pagination={{
currentPage: activePage,
pageSize: ITEMS_PER_PAGE,
total: tokenCount,
// showSizeChanger: true,
// pageSizeOptions: [10, 20, 50, 100],
formatPageText: (page) => `${page.currentStart} - ${page.currentEnd} 条,共 ${redemptions.length}`,
// onPageSizeChange: (size) => {
// setPageSize(size);
// setActivePage(1);
// },
onPageChange: handlePageChange,
}} loading={loading} rowSelection={rowSelection}>
</Table>
</>
);