mirror of
https://github.com/linux-do/new-api.git
synced 2025-09-23 10:26:38 +08:00
更新渠道管理
This commit is contained in:
parent
6a2ebf7578
commit
7dc8b0ea93
@ -292,7 +292,7 @@ func UpdateChannelBalance(c *gin.Context) {
|
||||
}
|
||||
|
||||
func updateAllChannelsBalance() error {
|
||||
channels, err := model.GetAllChannels(0, 0, true)
|
||||
channels, err := model.GetAllChannels(0, 0, true, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -163,7 +163,7 @@ func testAllChannels(notify bool) error {
|
||||
}
|
||||
testAllChannelsRunning = true
|
||||
testAllChannelsLock.Unlock()
|
||||
channels, err := model.GetAllChannels(0, 0, true)
|
||||
channels, err := model.GetAllChannels(0, 0, true, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -18,7 +18,8 @@ func GetAllChannels(c *gin.Context) {
|
||||
if pageSize < 0 {
|
||||
pageSize = common.ItemsPerPage
|
||||
}
|
||||
channels, err := model.GetAllChannels(p*pageSize, pageSize, false)
|
||||
idSort, _ := strconv.ParseBool(c.Query("id_sort"))
|
||||
channels, err := model.GetAllChannels(p*pageSize, pageSize, false, idSort)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
@ -36,7 +37,9 @@ func GetAllChannels(c *gin.Context) {
|
||||
|
||||
func SearchChannels(c *gin.Context) {
|
||||
keyword := c.Query("keyword")
|
||||
channels, err := model.SearchChannels(keyword)
|
||||
group := c.Query("group")
|
||||
//idSort, _ := strconv.ParseBool(c.Query("id_sort"))
|
||||
channels, err := model.SearchChannels(keyword, group)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
|
@ -28,23 +28,35 @@ type Channel struct {
|
||||
AutoBan *int `json:"auto_ban" gorm:"default:1"`
|
||||
}
|
||||
|
||||
func GetAllChannels(startIdx int, num int, selectAll bool) ([]*Channel, error) {
|
||||
func GetAllChannels(startIdx int, num int, selectAll bool, idSort bool) ([]*Channel, error) {
|
||||
var channels []*Channel
|
||||
var err error
|
||||
order := "priority desc"
|
||||
if idSort {
|
||||
order = "id desc"
|
||||
}
|
||||
if selectAll {
|
||||
err = DB.Order("priority desc").Find(&channels).Error
|
||||
err = DB.Order(order).Find(&channels).Error
|
||||
} else {
|
||||
err = DB.Order("priority desc").Limit(num).Offset(startIdx).Omit("key").Find(&channels).Error
|
||||
err = DB.Order(order).Limit(num).Offset(startIdx).Omit("key").Find(&channels).Error
|
||||
}
|
||||
return channels, err
|
||||
}
|
||||
|
||||
func SearchChannels(keyword string) (channels []*Channel, err error) {
|
||||
func SearchChannels(keyword string, group string) (channels []*Channel, err error) {
|
||||
keyCol := "`key`"
|
||||
if common.UsingPostgreSQL {
|
||||
keyCol = `"key"`
|
||||
}
|
||||
if group != "" {
|
||||
groupCol := "`group`"
|
||||
if common.UsingPostgreSQL {
|
||||
groupCol = `"group"`
|
||||
}
|
||||
err = DB.Omit("key").Where("(id = ? or name LIKE ? or "+keyCol+" = ?) and "+groupCol+" LIKE ?", common.String2Int(keyword), keyword+"%", keyword, "%"+group+"%").Find(&channels).Error
|
||||
} else {
|
||||
err = DB.Omit("key").Where("id = ? or name LIKE ? or "+keyCol+" = ?", common.String2Int(keyword), keyword+"%", keyword).Find(&channels).Error
|
||||
}
|
||||
return channels, err
|
||||
}
|
||||
|
||||
|
@ -1,10 +1,25 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import { Button, Form, Input, Label, Message, Pagination, Popup, Table } from 'semantic-ui-react';
|
||||
import {Input, Label, Message, Popup} from 'semantic-ui-react';
|
||||
import {Link} from 'react-router-dom';
|
||||
import {API, setPromptShown, shouldShowPrompt, showError, showInfo, showSuccess, timestamp2string} from '../helpers';
|
||||
|
||||
import {CHANNEL_OPTIONS, ITEMS_PER_PAGE} from '../constants';
|
||||
import {renderGroup, renderNumber, renderQuota} from '../helpers/render';
|
||||
import {renderGroup, renderNumber, renderQuota, renderQuotaWithPrompt} from '../helpers/render';
|
||||
import {
|
||||
Avatar,
|
||||
Tag,
|
||||
Table,
|
||||
Button,
|
||||
Popover,
|
||||
Form,
|
||||
Modal,
|
||||
Popconfirm,
|
||||
Space,
|
||||
Tooltip,
|
||||
Switch,
|
||||
Typography, InputNumber
|
||||
} from "@douyinfe/semi-ui";
|
||||
import EditChannel from "../pages/Channel/EditChannel";
|
||||
|
||||
function renderTimestamp(timestamp) {
|
||||
return (
|
||||
@ -24,7 +39,7 @@ function renderType(type) {
|
||||
}
|
||||
type2label[0] = {value: 0, text: '未知类型', color: 'grey'};
|
||||
}
|
||||
return <Label basic color={type2label[type]?.color}>{type2label[type]?.text}</Label>;
|
||||
return <Tag size='large' color={type2label[type]?.color}>{type2label[type]?.text}</Tag>;
|
||||
}
|
||||
|
||||
function renderBalance(type, balance) {
|
||||
@ -49,25 +64,207 @@ function renderBalance(type, balance) {
|
||||
}
|
||||
|
||||
const ChannelsTable = () => {
|
||||
const columns = [
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
},
|
||||
{
|
||||
title: '名称',
|
||||
dataIndex: 'name',
|
||||
},
|
||||
{
|
||||
title: '分组',
|
||||
dataIndex: 'group',
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<div>
|
||||
<Space spacing={2}>
|
||||
{
|
||||
text.split(',').map((item, index) => {
|
||||
return (renderGroup(item))
|
||||
})
|
||||
}
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '类型',
|
||||
dataIndex: 'type',
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<div>
|
||||
{renderType(text)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<div>
|
||||
{renderStatus(text)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '响应时间',
|
||||
dataIndex: 'response_time',
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<div>
|
||||
{renderResponseTime(text)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '已用/剩余',
|
||||
dataIndex: 'expired_time',
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<div>
|
||||
<Space spacing={1}>
|
||||
<Tooltip content={'已用额度'}>
|
||||
<Tag color='white' type='ghost' size='large'>{renderQuota(record.used_quota)}</Tag>
|
||||
</Tooltip>
|
||||
<Tooltip content={'剩余额度,点击更新'}>
|
||||
<Tag color='white' type='ghost' size='large' onClick={() => {updateChannelBalance(record)}}>{renderQuota(record.balance)}</Tag>
|
||||
</Tooltip>
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '优先级',
|
||||
dataIndex: 'priority',
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<div>
|
||||
<InputNumber
|
||||
style={{width: 70}}
|
||||
name='name'
|
||||
onChange={value => {
|
||||
manageChannel(record.id, 'priority', record, value);
|
||||
}}
|
||||
defaultValue={record.priority}
|
||||
min={0}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
dataIndex: 'operate',
|
||||
render: (text, record, index) => (
|
||||
<div>
|
||||
<Popconfirm
|
||||
title="确定是否要删除此渠道?"
|
||||
content="此修改将不可逆"
|
||||
okType={'danger'}
|
||||
position={'left'}
|
||||
onConfirm={() => {
|
||||
manageChannel(record.id, 'delete', record).then(
|
||||
() => {
|
||||
removeRecord(record.id);
|
||||
}
|
||||
)
|
||||
}}
|
||||
>
|
||||
<Button theme='light' type='danger' style={{marginRight: 1}}>删除</Button>
|
||||
</Popconfirm>
|
||||
{
|
||||
record.status === 1 ?
|
||||
<Button theme='light' type='warning' style={{marginRight: 1}} onClick={
|
||||
async () => {
|
||||
manageChannel(
|
||||
record.id,
|
||||
'disable',
|
||||
record
|
||||
)
|
||||
}
|
||||
}>禁用</Button> :
|
||||
<Button theme='light' type='secondary' style={{marginRight: 1}} onClick={
|
||||
async () => {
|
||||
manageChannel(
|
||||
record.id,
|
||||
'enable',
|
||||
record
|
||||
);
|
||||
}
|
||||
}>启用</Button>
|
||||
}
|
||||
<Button theme='light' type='tertiary' style={{marginRight: 1}} onClick={
|
||||
() => {
|
||||
setEditingChannel(record);
|
||||
setShowEdit(true);
|
||||
}
|
||||
}>编辑</Button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const [channels, setChannels] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [activePage, setActivePage] = useState(1);
|
||||
const [idSort, setIdSort] = useState(false);
|
||||
const [searchKeyword, setSearchKeyword] = useState('');
|
||||
const [searchGroup, setSearchGroup] = useState('');
|
||||
const [searching, setSearching] = useState(false);
|
||||
const [updatingBalance, setUpdatingBalance] = useState(false);
|
||||
const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
|
||||
const [showPrompt, setShowPrompt] = useState(shouldShowPrompt("channel-test"));
|
||||
const [channelCount, setChannelCount] = useState(pageSize);
|
||||
const [groupOptions, setGroupOptions] = useState([]);
|
||||
const [showEdit, setShowEdit] = useState(false);
|
||||
const [editingChannel, setEditingChannel] = useState({
|
||||
id: undefined,
|
||||
});
|
||||
|
||||
const removeRecord = id => {
|
||||
let newDataSource = [...channels];
|
||||
if (id != null) {
|
||||
let idx = newDataSource.findIndex(data => data.id === id);
|
||||
|
||||
if (idx > -1) {
|
||||
newDataSource.splice(idx, 1);
|
||||
setChannels(newDataSource);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const setChannelFormat = (channels) => {
|
||||
for (let i = 0; i < channels.length; i++) {
|
||||
channels[i].key = '' + channels[i].id;
|
||||
}
|
||||
// data.key = '' + data.id
|
||||
setChannels(channels);
|
||||
if (channels.length >= pageSize) {
|
||||
setChannelCount(channels.length + pageSize);
|
||||
} else {
|
||||
setChannelCount(channels.length);
|
||||
}
|
||||
}
|
||||
|
||||
const loadChannels = async (startIdx) => {
|
||||
const res = await API.get(`/api/channel/?p=${startIdx}&page_size=${pageSize}`);
|
||||
setLoading(true);
|
||||
const res = await API.get(`/api/channel/?p=${startIdx}&page_size=${pageSize}&id_sort=${idSort}`);
|
||||
const {success, message, data} = res.data;
|
||||
if (success) {
|
||||
if (startIdx === 0) {
|
||||
setChannels(data);
|
||||
setChannelFormat(data);
|
||||
} else {
|
||||
let newChannels = [...channels];
|
||||
newChannels.splice(startIdx * pageSize, data.length, ...data);
|
||||
setChannels(newChannels);
|
||||
setChannelFormat(newChannels);
|
||||
}
|
||||
} else {
|
||||
showError(message);
|
||||
@ -75,25 +272,15 @@ const ChannelsTable = () => {
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const onPaginationChange = (e, { activePage }) => {
|
||||
(async () => {
|
||||
if (activePage === Math.ceil(channels.length / pageSize) + 1) {
|
||||
// In this case we have to load more data and then append them.
|
||||
await loadChannels(activePage - 1, pageSize);
|
||||
}
|
||||
setActivePage(activePage);
|
||||
})();
|
||||
};
|
||||
|
||||
const setItemsPerPage = (e) => {
|
||||
console.log(e.target.value);
|
||||
//parseInt(e.target.value);
|
||||
setPageSize(parseInt(e.target.value));
|
||||
loadChannels(0);
|
||||
}
|
||||
useEffect(() => {
|
||||
loadChannels(0)
|
||||
.then()
|
||||
.catch((reason) => {
|
||||
showError(reason);
|
||||
});
|
||||
}, [pageSize]);
|
||||
|
||||
const refresh = async () => {
|
||||
setLoading(true);
|
||||
await loadChannels(activePage - 1);
|
||||
};
|
||||
|
||||
@ -103,9 +290,23 @@ const ChannelsTable = () => {
|
||||
.catch((reason) => {
|
||||
showError(reason);
|
||||
});
|
||||
fetchGroups().then();
|
||||
console.log(localStorage.getItem('id-sort'))
|
||||
if (localStorage.getItem('id-sort') === 'true') {
|
||||
setIdSort(true)
|
||||
}
|
||||
}, []);
|
||||
|
||||
const manageChannel = async (id, action, idx, value) => {
|
||||
useEffect(() => {
|
||||
searchChannels()
|
||||
}, [searchGroup]);
|
||||
|
||||
useEffect(() => {
|
||||
refresh()
|
||||
localStorage.setItem('id-sort', idSort + '');
|
||||
}, [idSort]);
|
||||
|
||||
const manageChannel = async (id, action, record, value) => {
|
||||
let data = {id};
|
||||
let res;
|
||||
switch (action) {
|
||||
@ -143,11 +344,10 @@ const ChannelsTable = () => {
|
||||
showSuccess('操作成功完成!');
|
||||
let channel = res.data.data;
|
||||
let newChannels = [...channels];
|
||||
let realIdx = (activePage - 1) * pageSize + idx;
|
||||
if (action === 'delete') {
|
||||
newChannels[realIdx].deleted = true;
|
||||
|
||||
} else {
|
||||
newChannels[realIdx].status = channel.status;
|
||||
record.status = channel.status;
|
||||
}
|
||||
setChannels(newChannels);
|
||||
} else {
|
||||
@ -158,13 +358,13 @@ const ChannelsTable = () => {
|
||||
const renderStatus = (status) => {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return <Label basic color='green'>已启用</Label>;
|
||||
return <Tag size='large' color='green'>已启用</Tag>;
|
||||
case 2:
|
||||
return (
|
||||
<Popup
|
||||
trigger={<Label basic color='red'>
|
||||
trigger={<Tag size='large' color='red'>
|
||||
已禁用
|
||||
</Label>}
|
||||
</Tag>}
|
||||
content='本渠道被手动禁用'
|
||||
basic
|
||||
/>
|
||||
@ -172,18 +372,18 @@ const ChannelsTable = () => {
|
||||
case 3:
|
||||
return (
|
||||
<Popup
|
||||
trigger={<Label basic color='yellow'>
|
||||
trigger={<Tag size='large' color='yellow'>
|
||||
已禁用
|
||||
</Label>}
|
||||
</Tag>}
|
||||
content='本渠道被程序自动禁用'
|
||||
basic
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Label basic color='grey'>
|
||||
<Tag size='large' color='grey'>
|
||||
未知状态
|
||||
</Label>
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
};
|
||||
@ -192,27 +392,27 @@ const ChannelsTable = () => {
|
||||
let time = responseTime / 1000;
|
||||
time = time.toFixed(2) + ' 秒';
|
||||
if (responseTime === 0) {
|
||||
return <Label basic color='grey'>未测试</Label>;
|
||||
return <Tag size='large' color='grey'>未测试</Tag>;
|
||||
} else if (responseTime <= 1000) {
|
||||
return <Label basic color='green'>{time}</Label>;
|
||||
return <Tag size='large' color='green'>{time}</Tag>;
|
||||
} else if (responseTime <= 3000) {
|
||||
return <Label basic color='olive'>{time}</Label>;
|
||||
return <Tag size='large' color='lime'>{time}</Tag>;
|
||||
} else if (responseTime <= 5000) {
|
||||
return <Label basic color='yellow'>{time}</Label>;
|
||||
return <Tag size='large' color='yellow'>{time}</Tag>;
|
||||
} else {
|
||||
return <Label basic color='red'>{time}</Label>;
|
||||
return <Tag size='large' color='red'>{time}</Tag>;
|
||||
}
|
||||
};
|
||||
|
||||
const searchChannels = async () => {
|
||||
if (searchKeyword === '') {
|
||||
if (searchKeyword === '' && searchGroup === '') {
|
||||
// if keyword is blank, load files instead.
|
||||
await loadChannels(0);
|
||||
setActivePage(1);
|
||||
return;
|
||||
}
|
||||
setSearching(true);
|
||||
const res = await API.get(`/api/channel/search?keyword=${searchKeyword}`);
|
||||
const res = await API.get(`/api/channel/search?keyword=${searchKeyword}&group=${searchGroup}`);
|
||||
const {success, message, data} = res.data;
|
||||
if (success) {
|
||||
setChannels(data);
|
||||
@ -259,16 +459,13 @@ const ChannelsTable = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const updateChannelBalance = async (id, name, idx) => {
|
||||
const res = await API.get(`/api/channel/update_balance/${id}/`);
|
||||
const updateChannelBalance = async (record) => {
|
||||
const res = await API.get(`/api/channel/update_balance/${record.id}/`);
|
||||
const {success, message, balance} = res.data;
|
||||
if (success) {
|
||||
let newChannels = [...channels];
|
||||
let realIdx = (activePage - 1) * pageSize + idx;
|
||||
newChannels[realIdx].balance = balance;
|
||||
newChannels[realIdx].balance_updated_time = Date.now() / 1000;
|
||||
setChannels(newChannels);
|
||||
showInfo(`通道 ${name} 余额更新成功!`);
|
||||
record.balance = balance;
|
||||
record.balance_updated_time = Date.now() / 1000;
|
||||
showInfo(`通道 ${record.name} 余额更新成功!`);
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
@ -286,10 +483,6 @@ const ChannelsTable = () => {
|
||||
setUpdatingBalance(false);
|
||||
};
|
||||
|
||||
const handleKeywordChange = async (e, { value }) => {
|
||||
setSearchKeyword(value.trim());
|
||||
};
|
||||
|
||||
const sortChannel = (key) => {
|
||||
if (channels.length === 0) return;
|
||||
setLoading(true);
|
||||
@ -312,270 +505,127 @@ const ChannelsTable = () => {
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
let pageData = channels.slice((activePage - 1) * pageSize, activePage * pageSize);
|
||||
|
||||
const handlePageChange = page => {
|
||||
setActivePage(page);
|
||||
if (page === Math.ceil(channels.length / pageSize) + 1) {
|
||||
// In this case we have to load more data and then append them.
|
||||
loadChannels(page - 1).then(r => {
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const fetchGroups = async () => {
|
||||
try {
|
||||
let res = await API.get(`/api/group/`);
|
||||
// add 'all' option
|
||||
// res.data.data.unshift('all');
|
||||
setGroupOptions(res.data.data.map((group) => ({
|
||||
label: group,
|
||||
value: group,
|
||||
})));
|
||||
} catch (error) {
|
||||
showError(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
const closeEdit = () => {
|
||||
setShowEdit(false);
|
||||
}
|
||||
|
||||
const handleRow = (record, index) => {
|
||||
if (record.status !== 1) {
|
||||
return {
|
||||
style: {
|
||||
background: 'var(--semi-color-disabled-border)',
|
||||
},
|
||||
};
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form onSubmit={searchChannels}>
|
||||
<EditChannel refresh={refresh} visible={showEdit} handleClose={closeEdit} editingChannel={editingChannel}/>
|
||||
<Form onSubmit={searchChannels} labelPosition='left'>
|
||||
|
||||
<div style={{display: 'flex'}}>
|
||||
<Space>
|
||||
<Form.Input
|
||||
icon='search'
|
||||
fluid
|
||||
iconPosition='left'
|
||||
placeholder='搜索渠道的 ID,名称和密钥 ...'
|
||||
field='search'
|
||||
label='关键词'
|
||||
placeholder='ID,名称和密钥 ...'
|
||||
value={searchKeyword}
|
||||
loading={searching}
|
||||
onChange={handleKeywordChange}
|
||||
onChange={(v)=>{
|
||||
setSearchKeyword(v.trim())
|
||||
}}
|
||||
/>
|
||||
<Form.Select field="group" label='分组' optionList={groupOptions} onChange={(v) => {
|
||||
setSearchGroup(v)
|
||||
}}/>
|
||||
</Space>
|
||||
</div>
|
||||
</Form>
|
||||
{
|
||||
showPrompt && (
|
||||
<Message onDismiss={() => {
|
||||
setShowPrompt(false);
|
||||
setPromptShown("channel-test");
|
||||
}}>
|
||||
当前版本测试是通过按照 OpenAI API 格式使用 gpt-3.5-turbo
|
||||
模型进行非流式请求实现的,因此测试报错并不一定代表通道不可用,该功能后续会修复。
|
||||
|
||||
另外,OpenAI 渠道已经不再支持通过 key 获取余额,因此余额显示为 0。对于支持的渠道类型,请点击余额进行刷新。
|
||||
</Message>
|
||||
)
|
||||
}
|
||||
<Table basic compact size='small'>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
sortChannel('id');
|
||||
}}
|
||||
>
|
||||
ID
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
sortChannel('name');
|
||||
}}
|
||||
>
|
||||
名称
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
sortChannel('group');
|
||||
}}
|
||||
width={1}
|
||||
>
|
||||
分组
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
sortChannel('type');
|
||||
}}
|
||||
width={2}
|
||||
>
|
||||
类型
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
sortChannel('status');
|
||||
}}
|
||||
width={2}
|
||||
>
|
||||
状态
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
sortChannel('response_time');
|
||||
}}
|
||||
>
|
||||
响应时间
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
sortChannel('used_quota');
|
||||
}}
|
||||
width={1}
|
||||
>
|
||||
已使用
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
sortChannel('balance');
|
||||
}}
|
||||
>
|
||||
余额
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
sortChannel('priority');
|
||||
}}
|
||||
>
|
||||
优先级
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell>操作</Table.HeaderCell>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
|
||||
<Table.Body>
|
||||
{channels
|
||||
.slice(
|
||||
(activePage - 1) * pageSize,
|
||||
activePage * pageSize
|
||||
)
|
||||
.map((channel, idx) => {
|
||||
if (channel.deleted) return <></>;
|
||||
return (
|
||||
<Table.Row key={channel.id}>
|
||||
<Table.Cell>{channel.id}</Table.Cell>
|
||||
<Table.Cell>{channel.name ? channel.name : '无'}</Table.Cell>
|
||||
<Table.Cell>{renderGroup(channel.group)}</Table.Cell>
|
||||
<Table.Cell>{renderType(channel.type)}</Table.Cell>
|
||||
<Table.Cell>{renderStatus(channel.status)}</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Popup
|
||||
content={channel.test_time ? renderTimestamp(channel.test_time) : '未测试'}
|
||||
key={channel.id}
|
||||
trigger={renderResponseTime(channel.response_time)}
|
||||
basic
|
||||
/>
|
||||
</Table.Cell>
|
||||
<Table.Cell>{renderQuota(channel.used_quota)}</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Popup
|
||||
trigger={<span onClick={() => {
|
||||
updateChannelBalance(channel.id, channel.name, idx);
|
||||
}} style={{ cursor: 'pointer' }}>
|
||||
{renderBalance(channel.type, channel.balance)}
|
||||
</span>}
|
||||
content='点击更新'
|
||||
basic
|
||||
/>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Popup
|
||||
trigger={<Input type='number' defaultValue={channel.priority} onBlur={(event) => {
|
||||
manageChannel(
|
||||
channel.id,
|
||||
'priority',
|
||||
idx,
|
||||
event.target.value
|
||||
);
|
||||
}}>
|
||||
<input style={{ maxWidth: '60px' }} />
|
||||
</Input>}
|
||||
content='渠道选择优先级,越高越优先'
|
||||
basic
|
||||
/>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<div>
|
||||
<Button
|
||||
size={'small'}
|
||||
positive
|
||||
onClick={() => {
|
||||
testChannel(channel.id, channel.name, idx);
|
||||
}}
|
||||
>
|
||||
测试
|
||||
</Button>
|
||||
<Popup
|
||||
trigger={
|
||||
<Button size='small' negative>
|
||||
删除
|
||||
</Button>
|
||||
}
|
||||
on='click'
|
||||
flowing
|
||||
hoverable
|
||||
>
|
||||
<Button
|
||||
negative
|
||||
onClick={() => {
|
||||
manageChannel(channel.id, 'delete', idx);
|
||||
}}
|
||||
>
|
||||
删除渠道 {channel.name}
|
||||
</Button>
|
||||
</Popup>
|
||||
<Button
|
||||
size={'small'}
|
||||
onClick={() => {
|
||||
manageChannel(
|
||||
channel.id,
|
||||
channel.status === 1 ? 'disable' : 'enable',
|
||||
idx
|
||||
);
|
||||
}}
|
||||
>
|
||||
{channel.status === 1 ? '禁用' : '启用'}
|
||||
</Button>
|
||||
<Button
|
||||
size={'small'}
|
||||
as={Link}
|
||||
to={'/channel/edit/' + channel.id}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<div style={{marginTop: 10, display: 'flex'}}>
|
||||
<Space>
|
||||
<Typography.Text strong>使用ID排序</Typography.Text>
|
||||
<Switch checked={idSort} label='使用ID排序' uncheckedText="关" aria-label="是否用ID排序" onChange={(v) => {
|
||||
setIdSort(v)
|
||||
}}></Switch>
|
||||
</Space>
|
||||
</div>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
);
|
||||
})}
|
||||
</Table.Body>
|
||||
|
||||
<Table.Footer>
|
||||
<Table.Row>
|
||||
<Table.HeaderCell colSpan='10'>
|
||||
<Button size='small' as={Link} to='/channel/add' loading={loading}>
|
||||
添加新的渠道
|
||||
</Button>
|
||||
<Button size='small' loading={loading} onClick={testAllChannels}>
|
||||
测试所有已启用通道
|
||||
</Button>
|
||||
<Button size='small' onClick={updateAllChannelsBalance}
|
||||
loading={loading || updatingBalance}>更新所有已启用通道余额</Button>
|
||||
|
||||
<div style={{ float: 'right' }}>
|
||||
<div className="ui labeled input" style={{marginRight: '10px'}}>
|
||||
<div className="ui label">每页数量</div>
|
||||
<Input type="number" style={{width: '70px'}} defaultValue={ITEMS_PER_PAGE} onBlur={setItemsPerPage}></Input>
|
||||
</div>
|
||||
<Pagination
|
||||
activePage={activePage}
|
||||
onPageChange={onPaginationChange}
|
||||
size='small'
|
||||
siblingRange={1}
|
||||
totalPages={
|
||||
Math.ceil(channels.length / pageSize) +
|
||||
(channels.length % pageSize === 0 ? 1 : 0)
|
||||
<Table columns={columns} dataSource={pageData} pagination={{
|
||||
currentPage: activePage,
|
||||
pageSize: pageSize,
|
||||
total: channelCount,
|
||||
pageSizeOpts: [10, 20, 50, 100],
|
||||
showSizeChanger: true,
|
||||
onPageSizeChange: (size) => {
|
||||
setPageSize(size)
|
||||
setActivePage(1)
|
||||
},
|
||||
onPageChange: handlePageChange,
|
||||
}} loading={loading} onRow={handleRow}/>
|
||||
<div style={{display: 'flex'}}>
|
||||
<Space>
|
||||
<Button theme='light' type='primary' style={{marginRight: 8}} onClick={
|
||||
() => {
|
||||
setEditingChannel({
|
||||
id: undefined,
|
||||
});
|
||||
setShowEdit(true)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<Popup
|
||||
trigger={
|
||||
<Button size='small' loading={loading}>
|
||||
删除禁用渠道
|
||||
</Button>
|
||||
}
|
||||
on='click'
|
||||
flowing
|
||||
hoverable
|
||||
}>添加渠道</Button>
|
||||
<Popconfirm
|
||||
title="确定?"
|
||||
okType={'warning'}
|
||||
onConfirm={testAllChannels}
|
||||
>
|
||||
<Button size='small' loading={loading} negative onClick={deleteAllDisabledChannels}>
|
||||
确认删除
|
||||
</Button>
|
||||
</Popup>
|
||||
<Button size='small' onClick={refresh} loading={loading}>刷新</Button>
|
||||
<Button theme='light' type='warning' style={{marginRight: 8}}>测试所有已启用通道</Button>
|
||||
</Popconfirm>
|
||||
<Popconfirm
|
||||
title="确定?"
|
||||
okType={'secondary'}
|
||||
onConfirm={updateAllChannelsBalance}
|
||||
>
|
||||
<Button theme='light' type='secondary' style={{marginRight: 8}}>更新所有已启用通道余额</Button>
|
||||
</Popconfirm>
|
||||
<Popconfirm
|
||||
title="确定是否要删除禁用通道?"
|
||||
content="此修改将不可逆"
|
||||
okType={'danger'}
|
||||
onConfirm={deleteAllDisabledChannels}
|
||||
>
|
||||
<Button theme='light' type='danger' style={{marginRight: 8}}>删除禁用通道</Button>
|
||||
</Popconfirm>
|
||||
|
||||
</Table.HeaderCell>
|
||||
</Table.Row>
|
||||
</Table.Footer>
|
||||
</Table>
|
||||
<Button theme='light' type='primary' style={{marginRight: 8}} onClick={refresh}>刷新</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -164,11 +164,24 @@ const RedemptionsTable = () => {
|
||||
setShowEdit(false);
|
||||
}
|
||||
|
||||
const setCount = (data) => {
|
||||
if (data.length >= (activePage) * ITEMS_PER_PAGE) {
|
||||
setTokenCount(data.length + 1);
|
||||
// const setCount = (data) => {
|
||||
// if (data.length >= (activePage) * ITEMS_PER_PAGE) {
|
||||
// setTokenCount(data.length + 1);
|
||||
// } else {
|
||||
// setTokenCount(data.length);
|
||||
// }
|
||||
// }
|
||||
|
||||
const setRedemptionFormat = (redeptions) => {
|
||||
for (let i = 0; i < redeptions.length; i++) {
|
||||
redeptions[i].key = '' + redeptions[i].id;
|
||||
}
|
||||
// data.key = '' + data.id
|
||||
setRedemptions(redeptions);
|
||||
if (redeptions.length >= (activePage) * ITEMS_PER_PAGE) {
|
||||
setTokenCount(redeptions.length + 1);
|
||||
} else {
|
||||
setTokenCount(data.length);
|
||||
setTokenCount(redeptions.length);
|
||||
}
|
||||
}
|
||||
|
||||
@ -177,13 +190,11 @@ const RedemptionsTable = () => {
|
||||
const {success, message, data} = res.data;
|
||||
if (success) {
|
||||
if (startIdx === 0) {
|
||||
setRedemptions(data);
|
||||
setCount(data);
|
||||
setRedemptionFormat(data);
|
||||
} else {
|
||||
let newRedemptions = redemptions;
|
||||
newRedemptions.push(...data);
|
||||
setRedemptions(newRedemptions);
|
||||
setCount(newRedemptions);
|
||||
setRedemptionFormat(newRedemptions);
|
||||
}
|
||||
} else {
|
||||
showError(message);
|
||||
|
@ -1,26 +1,16 @@
|
||||
export const CHANNEL_OPTIONS = [
|
||||
{ key: 1, text: 'OpenAI', value: 1, color: 'green' },
|
||||
{ key: 99, text: 'Midjourney-Proxy', value: 99, color: 'green' },
|
||||
{ key: 14, text: 'Anthropic Claude', value: 14, color: 'black' },
|
||||
{ key: 3, text: 'Azure OpenAI', value: 3, color: 'olive' },
|
||||
{ key: 11, text: 'Google PaLM2', value: 11, color: 'orange' },
|
||||
{ key: 15, text: '百度文心千帆', value: 15, color: 'blue' },
|
||||
{ key: 17, text: '阿里通义千问', value: 17, color: 'orange' },
|
||||
{ key: 18, text: '讯飞星火认知', value: 18, color: 'blue' },
|
||||
{ key: 16, text: '智谱 ChatGLM', value: 16, color: 'violet' },
|
||||
{ key: 19, text: '360 智脑', value: 19, color: 'blue' },
|
||||
{ key: 23, text: '腾讯混元', value: 23, color: 'teal' },
|
||||
{ key: 8, text: '自定义渠道', value: 8, color: 'pink' },
|
||||
{ key: 22, text: '知识库:FastGPT', value: 22, color: 'blue' },
|
||||
{ key: 21, text: '知识库:AI Proxy', value: 21, color: 'purple' },
|
||||
{ key: 20, text: '代理:OpenRouter', value: 20, color: 'black' },
|
||||
{ key: 2, text: '代理:API2D', value: 2, color: 'blue' },
|
||||
{ key: 5, text: '代理:OpenAI-SB', value: 5, color: 'brown' },
|
||||
{ key: 7, text: '代理:OhMyGPT', value: 7, color: 'purple' },
|
||||
{ key: 10, text: '代理:AI Proxy', value: 10, color: 'purple' },
|
||||
{ key: 4, text: '代理:CloseAI', value: 4, color: 'teal' },
|
||||
{ key: 6, text: '代理:OpenAI Max', value: 6, color: 'violet' },
|
||||
{ key: 9, text: '代理:AI.LS', value: 9, color: 'yellow' },
|
||||
{ key: 12, text: '代理:API2GPT', value: 12, color: 'blue' },
|
||||
{ key: 13, text: '代理:AIGC2D', value: 13, color: 'purple' }
|
||||
{ key: 1, text: 'OpenAI', value: 1, color: 'green', label: 'OpenAI' },
|
||||
{ key: 24, text: 'Midjourney Proxy', value: 24, color: 'light-blue', label: 'Midjourney Proxy' },
|
||||
{ key: 14, text: 'Anthropic Claude', value: 14, color: 'black', label: 'Anthropic Claude' },
|
||||
{ key: 3, text: 'Azure OpenAI', value: 3, color: 'olive', label: 'Azure OpenAI' },
|
||||
{ key: 11, text: 'Google PaLM2', value: 11, color: 'orange', label: 'Google PaLM2' },
|
||||
{ key: 15, text: '百度文心千帆', value: 15, color: 'blue', label: '百度文心千帆' },
|
||||
{ key: 17, text: '阿里通义千问', value: 17, color: 'orange', label: '阿里通义千问' },
|
||||
{ key: 18, text: '讯飞星火认知', value: 18, color: 'blue', label: '讯飞星火认知' },
|
||||
{ key: 16, text: '智谱 ChatGLM', value: 16, color: 'violet', label: '智谱 ChatGLM' },
|
||||
{ key: 19, text: '360 智脑', value: 19, color: 'blue', label: '360 智脑' },
|
||||
{ key: 23, text: '腾讯混元', value: 23, color: 'teal', label: '腾讯混元' },
|
||||
{ key: 8, text: '自定义渠道', value: 8, color: 'pink', label: '自定义渠道' },
|
||||
{ key: 22, text: '知识库:FastGPT', value: 22, color: 'blue', label: '知识库:FastGPT' },
|
||||
{ key: 21, text: '知识库:AI Proxy', value: 21, color: 'purple', label: '知识库:AI Proxy' },
|
||||
];
|
@ -1,8 +1,9 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Button, Form, Header, Input, Message, Segment } from 'semantic-ui-react';
|
||||
import React, {useEffect, useRef, useState} from 'react';
|
||||
import {useNavigate, useParams} from 'react-router-dom';
|
||||
import { API, showError, showInfo, showSuccess, verifyJSON } from '../../helpers';
|
||||
import {API, isMobile, showError, showInfo, showSuccess, verifyJSON} from '../../helpers';
|
||||
import {CHANNEL_OPTIONS} from '../../constants';
|
||||
import Title from "@douyinfe/semi-ui/lib/es/typography/title";
|
||||
import {SideSheet, Space, Spin, Button, Input, Typography, Select, TextArea, Checkbox, Banner} from "@douyinfe/semi-ui";
|
||||
|
||||
const MODEL_MAPPING_EXAMPLE = {
|
||||
'gpt-3.5-turbo-0301': 'gpt-3.5-turbo',
|
||||
@ -26,16 +27,14 @@ function type2secretPrompt(type) {
|
||||
}
|
||||
}
|
||||
|
||||
const EditChannel = () => {
|
||||
const params = useParams();
|
||||
const EditChannel = (props) => {
|
||||
const navigate = useNavigate();
|
||||
const channelId = params.id;
|
||||
const channelId = props.editingChannel.id;
|
||||
const isEdit = channelId !== undefined;
|
||||
const [loading, setLoading] = useState(isEdit);
|
||||
const handleCancel = () => {
|
||||
navigate('/channel');
|
||||
props.handleClose()
|
||||
};
|
||||
|
||||
const originInputs = {
|
||||
name: '',
|
||||
type: 1,
|
||||
@ -58,7 +57,7 @@ const EditChannel = () => {
|
||||
const [basicModels, setBasicModels] = useState([]);
|
||||
const [fullModels, setFullModels] = useState([]);
|
||||
const [customModel, setCustomModel] = useState('');
|
||||
const handleInputChange = (e, { name, value }) => {
|
||||
const handleInputChange = (name, value) => {
|
||||
setInputs((inputs) => ({...inputs, [name]: value}));
|
||||
if (name === 'type' && inputs.models.length === 0) {
|
||||
let localModels = [];
|
||||
@ -93,7 +92,9 @@ const EditChannel = () => {
|
||||
//setAutoBan
|
||||
};
|
||||
|
||||
|
||||
const loadChannel = async () => {
|
||||
setLoading(true)
|
||||
let res = await API.get(`/api/channel/${channelId}`);
|
||||
const {success, message, data} = res.data;
|
||||
if (success) {
|
||||
@ -127,8 +128,7 @@ const EditChannel = () => {
|
||||
try {
|
||||
let res = await API.get(`/api/channel/models`);
|
||||
let localModelOptions = res.data.data.map((model) => ({
|
||||
key: model.id,
|
||||
text: model.id,
|
||||
label: model.id,
|
||||
value: model.id
|
||||
}));
|
||||
setOriginModelOptions(localModelOptions);
|
||||
@ -145,8 +145,7 @@ const EditChannel = () => {
|
||||
try {
|
||||
let res = await API.get(`/api/group/`);
|
||||
setGroupOptions(res.data.data.map((group) => ({
|
||||
key: group,
|
||||
text: group,
|
||||
label: group,
|
||||
value: group
|
||||
})));
|
||||
} catch (error) {
|
||||
@ -159,8 +158,7 @@ const EditChannel = () => {
|
||||
inputs.models.forEach((model) => {
|
||||
if (!localModelOptions.find((option) => option.key === model)) {
|
||||
localModelOptions.push({
|
||||
key: model,
|
||||
text: model,
|
||||
label: model,
|
||||
value: model
|
||||
});
|
||||
}
|
||||
@ -169,17 +167,19 @@ const EditChannel = () => {
|
||||
}, [originModelOptions, inputs.models]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isEdit) {
|
||||
loadChannel().then();
|
||||
}
|
||||
fetchModels().then();
|
||||
fetchGroups().then();
|
||||
}, []);
|
||||
if (isEdit) {
|
||||
loadChannel().then(
|
||||
() => {
|
||||
|
||||
}
|
||||
);
|
||||
} else {
|
||||
setInputs(originInputs)
|
||||
}
|
||||
}, [props.editingChannel.id]);
|
||||
|
||||
useEffect(() => {
|
||||
setInputs((inputs) => ({ ...inputs, auto_ban: autoBan ? 1 : 0 }));
|
||||
console.log(autoBan);
|
||||
}, [autoBan]);
|
||||
|
||||
const submit = async () => {
|
||||
if (!isEdit && (inputs.name === '' || inputs.key === '')) {
|
||||
@ -194,7 +194,7 @@ const EditChannel = () => {
|
||||
showInfo('模型映射必须是合法的 JSON 格式!');
|
||||
return;
|
||||
}
|
||||
let localInputs = inputs;
|
||||
let localInputs = {...inputs};
|
||||
if (localInputs.base_url && localInputs.base_url.endsWith('/')) {
|
||||
localInputs.base_url = localInputs.base_url.slice(0, localInputs.base_url.length - 1);
|
||||
}
|
||||
@ -225,6 +225,8 @@ const EditChannel = () => {
|
||||
showSuccess('渠道创建成功!');
|
||||
setInputs(originInputs);
|
||||
}
|
||||
props.refresh();
|
||||
props.handleClose();
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
@ -245,214 +247,278 @@ const EditChannel = () => {
|
||||
return [...modelOptions, ...localModelOptions];
|
||||
});
|
||||
setCustomModel('');
|
||||
handleInputChange(null, { name: 'models', value: localModels });
|
||||
handleInputChange('models', localModels);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Segment loading={loading}>
|
||||
<Header as='h3'>{isEdit ? '更新渠道信息' : '创建新的渠道'}</Header>
|
||||
<Form autoComplete='new-password'>
|
||||
<Form.Field>
|
||||
<Form.Select
|
||||
label='类型'
|
||||
<SideSheet
|
||||
placement={isEdit ? 'right' : 'left'}
|
||||
title={<Title level={3}>{isEdit ? '更新渠道信息' : '创建新的渠道'}</Title>}
|
||||
headerStyle={{borderBottom: '1px solid var(--semi-color-border)'}}
|
||||
bodyStyle={{borderBottom: '1px solid var(--semi-color-border)'}}
|
||||
visible={props.visible}
|
||||
footer={
|
||||
<div style={{display: 'flex', justifyContent: 'flex-end'}}>
|
||||
<Space>
|
||||
<Button theme='solid' size={'large'} onClick={submit}>提交</Button>
|
||||
<Button theme='solid' size={'large'} type={'tertiary'} onClick={handleCancel}>取消</Button>
|
||||
</Space>
|
||||
</div>
|
||||
}
|
||||
closeIcon={null}
|
||||
onCancel={() => handleCancel()}
|
||||
width={isMobile() ? '100%' : 600}
|
||||
>
|
||||
<Spin spinning={loading}>
|
||||
<div style={{marginTop: 10}}>
|
||||
<Typography.Text strong>类型:</Typography.Text>
|
||||
</div>
|
||||
<Select
|
||||
name='type'
|
||||
required
|
||||
options={CHANNEL_OPTIONS}
|
||||
optionList={CHANNEL_OPTIONS}
|
||||
value={inputs.type}
|
||||
onChange={handleInputChange}
|
||||
onChange={value => handleInputChange('type', value)}
|
||||
style={{width: '50%'}}
|
||||
/>
|
||||
</Form.Field>
|
||||
{
|
||||
inputs.type === 3 && (
|
||||
<>
|
||||
<Message>
|
||||
注意,<strong>模型部署名称必须和模型名称保持一致</strong>,因为 One API 会把请求体中的 model
|
||||
<div style={{marginTop: 10}}>
|
||||
<Banner type={"warning"} description={
|
||||
<>
|
||||
注意,<strong>模型部署名称必须和模型名称保持一致</strong>,因为 One API 会把请求体中的
|
||||
model
|
||||
参数替换为你的部署名称(模型名称中的点会被剔除),<a target='_blank'
|
||||
href='https://github.com/songquanpeng/one-api/issues/133?notification_referrer_id=NT_kwDOAmJSYrM2NjIwMzI3NDgyOjM5OTk4MDUw#issuecomment-1571602271'>图片演示</a>。
|
||||
</Message>
|
||||
<Form.Field>
|
||||
<Form.Input
|
||||
</>
|
||||
}>
|
||||
</Banner>
|
||||
</div>
|
||||
<div style={{marginTop: 10}}>
|
||||
<Typography.Text strong>AZURE_OPENAI_ENDPOINT:</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
label='AZURE_OPENAI_ENDPOINT'
|
||||
name='base_url'
|
||||
name='azure_base_url'
|
||||
placeholder={'请输入 AZURE_OPENAI_ENDPOINT,例如:https://docs-test-001.openai.azure.com'}
|
||||
onChange={handleInputChange}
|
||||
onChange={value => {
|
||||
handleInputChange('base_url', value)
|
||||
}}
|
||||
value={inputs.base_url}
|
||||
autoComplete='new-password'
|
||||
/>
|
||||
</Form.Field>
|
||||
<Form.Field>
|
||||
<Form.Input
|
||||
<div style={{marginTop: 10}}>
|
||||
<Typography.Text strong>默认 API 版本:</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
label='默认 API 版本'
|
||||
name='other'
|
||||
name='azure_other'
|
||||
placeholder={'请输入默认 API 版本,例如:2023-06-01-preview,该配置可以被实际的请求查询参数所覆盖'}
|
||||
onChange={handleInputChange}
|
||||
onChange={value => {
|
||||
handleInputChange('other', value)
|
||||
}}
|
||||
value={inputs.other}
|
||||
autoComplete='new-password'
|
||||
/>
|
||||
</Form.Field>
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
inputs.type === 8 && (
|
||||
<Form.Field>
|
||||
<Form.Input
|
||||
label='Base URL'
|
||||
<>
|
||||
<div style={{marginTop: 10}}>
|
||||
<Typography.Text strong>Base URL:</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
name='base_url'
|
||||
placeholder={'请输入自定义渠道的 Base URL,例如:https://openai.justsong.cn'}
|
||||
onChange={handleInputChange}
|
||||
placeholder={'请输入自定义渠道的 Base URL'}
|
||||
onChange={value => {
|
||||
handleInputChange('base_url', value)
|
||||
}}
|
||||
value={inputs.base_url}
|
||||
autoComplete='new-password'
|
||||
/>
|
||||
</Form.Field>
|
||||
</>
|
||||
)
|
||||
}
|
||||
<Form.Field>
|
||||
<Form.Input
|
||||
label='名称'
|
||||
<div style={{marginTop: 10}}>
|
||||
<Typography.Text strong>名称:</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
required
|
||||
name='name'
|
||||
placeholder={'请为渠道命名'}
|
||||
onChange={handleInputChange}
|
||||
onChange={value => {
|
||||
handleInputChange('name', value)
|
||||
}}
|
||||
value={inputs.name}
|
||||
autoComplete='new-password'
|
||||
/>
|
||||
</Form.Field>
|
||||
<Form.Field>
|
||||
<Form.Dropdown
|
||||
label='分组'
|
||||
<div style={{marginTop: 10}}>
|
||||
<Typography.Text strong>分组:</Typography.Text>
|
||||
</div>
|
||||
<Select
|
||||
placeholder={'请选择可以使用该渠道的分组'}
|
||||
name='groups'
|
||||
required
|
||||
fluid
|
||||
multiple
|
||||
selection
|
||||
allowAdditions
|
||||
additionLabel={'请在系统设置页面编辑分组倍率以添加新的分组:'}
|
||||
onChange={handleInputChange}
|
||||
onChange={value => {
|
||||
handleInputChange('groups', value)
|
||||
}}
|
||||
value={inputs.groups}
|
||||
autoComplete='new-password'
|
||||
options={groupOptions}
|
||||
optionList={groupOptions}
|
||||
/>
|
||||
</Form.Field>
|
||||
{
|
||||
inputs.type === 18 && (
|
||||
<Form.Field>
|
||||
<Form.Input
|
||||
label='模型版本'
|
||||
<>
|
||||
<div style={{marginTop: 10}}>
|
||||
<Typography.Text strong>模型版本:</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
name='other'
|
||||
placeholder={'请输入星火大模型版本,注意是接口地址中的版本号,例如:v2.1'}
|
||||
onChange={handleInputChange}
|
||||
onChange={value => {
|
||||
handleInputChange('other', value)
|
||||
}}
|
||||
value={inputs.other}
|
||||
autoComplete='new-password'
|
||||
/>
|
||||
</Form.Field>
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
inputs.type === 21 && (
|
||||
<Form.Field>
|
||||
<Form.Input
|
||||
<>
|
||||
<div style={{marginTop: 10}}>
|
||||
<Typography.Text strong>知识库 ID:</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
label='知识库 ID'
|
||||
name='other'
|
||||
placeholder={'请输入知识库 ID,例如:123456'}
|
||||
onChange={handleInputChange}
|
||||
onChange={value => {
|
||||
handleInputChange('other', value)
|
||||
}}
|
||||
value={inputs.other}
|
||||
autoComplete='new-password'
|
||||
/>
|
||||
</Form.Field>
|
||||
</>
|
||||
)
|
||||
}
|
||||
<Form.Field>
|
||||
<Form.Dropdown
|
||||
label='模型'
|
||||
<div style={{marginTop: 10}}>
|
||||
<Typography.Text strong>模型:</Typography.Text>
|
||||
</div>
|
||||
<Select
|
||||
placeholder={'请选择该渠道所支持的模型'}
|
||||
name='models'
|
||||
required
|
||||
fluid
|
||||
multiple
|
||||
selection
|
||||
onChange={handleInputChange}
|
||||
onChange={value => {
|
||||
handleInputChange('models', value)
|
||||
}}
|
||||
value={inputs.models}
|
||||
autoComplete='new-password'
|
||||
options={modelOptions}
|
||||
optionList={modelOptions}
|
||||
/>
|
||||
</Form.Field>
|
||||
<div style={{lineHeight: '40px', marginBottom: '12px'}}>
|
||||
<Button type={'button'} onClick={() => {
|
||||
handleInputChange(null, { name: 'models', value: basicModels });
|
||||
<Space>
|
||||
<Button type='primary' onClick={() => {
|
||||
handleInputChange('models', basicModels);
|
||||
}}>填入基础模型</Button>
|
||||
<Button type={'button'} onClick={() => {
|
||||
handleInputChange(null, { name: 'models', value: fullModels });
|
||||
<Button type='secondary' onClick={() => {
|
||||
handleInputChange('models', fullModels);
|
||||
}}>填入所有模型</Button>
|
||||
<Button type={'button'} onClick={() => {
|
||||
handleInputChange(null, { name: 'models', value: [] });
|
||||
<Button type='warning' onClick={() => {
|
||||
handleInputChange('models', []);
|
||||
}}>清除所有模型</Button>
|
||||
</Space>
|
||||
<Input
|
||||
action={
|
||||
<Button type={'button'} onClick={addCustomModel}>填入</Button>
|
||||
addonAfter={
|
||||
<Button type='primary' onClick={addCustomModel}>填入</Button>
|
||||
}
|
||||
placeholder='输入自定义模型名称'
|
||||
value={customModel}
|
||||
onChange={(e, { value }) => {
|
||||
onChange={(value) => {
|
||||
setCustomModel(value);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
addCustomModel();
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Form.Field>
|
||||
<Form.TextArea
|
||||
label='模型重定向'
|
||||
<div style={{marginTop: 10}}>
|
||||
<Typography.Text strong>模型重定向:</Typography.Text>
|
||||
</div>
|
||||
<TextArea
|
||||
placeholder={`此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,例如:\n${JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2)}`}
|
||||
name='model_mapping'
|
||||
onChange={handleInputChange}
|
||||
onChange={value => {
|
||||
handleInputChange('model_mapping', value)
|
||||
}}
|
||||
autosize
|
||||
value={inputs.model_mapping}
|
||||
style={{ minHeight: 150, fontFamily: 'JetBrains Mono, Consolas' }}
|
||||
autoComplete='new-password'
|
||||
/>
|
||||
</Form.Field>
|
||||
<Typography.Text style={{
|
||||
color: 'rgba(var(--semi-blue-5), 1)',
|
||||
userSelect: 'none',
|
||||
cursor: 'pointer'
|
||||
}} onClick={
|
||||
() => {
|
||||
handleInputChange('model_mapping', JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2))
|
||||
}
|
||||
}>
|
||||
填入模板
|
||||
</Typography.Text>
|
||||
<div style={{marginTop: 10}}>
|
||||
<Typography.Text strong>密钥:</Typography.Text>
|
||||
</div>
|
||||
{
|
||||
batch ? <Form.Field>
|
||||
<Form.TextArea
|
||||
batch ?
|
||||
<TextArea
|
||||
label='密钥'
|
||||
name='key'
|
||||
required
|
||||
placeholder={'请输入密钥,一行一个'}
|
||||
onChange={handleInputChange}
|
||||
onChange={value => {
|
||||
handleInputChange('key', value)
|
||||
}}
|
||||
value={inputs.key}
|
||||
style={{minHeight: 150, fontFamily: 'JetBrains Mono, Consolas'}}
|
||||
autoComplete='new-password'
|
||||
/>
|
||||
</Form.Field> : <Form.Field>
|
||||
<Form.Input
|
||||
:
|
||||
<Input
|
||||
label='密钥'
|
||||
name='key'
|
||||
required
|
||||
placeholder={type2secretPrompt(inputs.type)}
|
||||
onChange={handleInputChange}
|
||||
onChange={value => {
|
||||
handleInputChange('key', value)
|
||||
}}
|
||||
value={inputs.key}
|
||||
autoComplete='new-password'
|
||||
/>
|
||||
</Form.Field>
|
||||
}
|
||||
<Form.Field>
|
||||
<Form.Input
|
||||
<div style={{marginTop: 10}}>
|
||||
<Typography.Text strong>组织:</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
label='组织,可选,不填则为默认组织'
|
||||
name='openai_organization'
|
||||
placeholder='请输入组织org-xxx'
|
||||
onChange={handleInputChange}
|
||||
onChange={value => {
|
||||
handleInputChange('openai_organization', value)
|
||||
}}
|
||||
value={inputs.openai_organization}
|
||||
/>
|
||||
</Form.Field>
|
||||
<Form.Field>
|
||||
<Form.Checkbox
|
||||
label='是否自动禁用(仅当自动禁用开启时有效),关闭后不会自动禁用该渠道'
|
||||
<div style={{marginTop: 10, display: 'flex'}}>
|
||||
<Space>
|
||||
<Checkbox
|
||||
name='auto_ban'
|
||||
checked={autoBan}
|
||||
onChange={
|
||||
@ -463,49 +529,66 @@ const EditChannel = () => {
|
||||
}
|
||||
// onChange={handleInputChange}
|
||||
/>
|
||||
</Form.Field>
|
||||
<Typography.Text
|
||||
strong>是否自动禁用(仅当自动禁用开启时有效),关闭后不会自动禁用该渠道:</Typography.Text>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
{
|
||||
!isEdit && (
|
||||
<Form.Checkbox
|
||||
<div style={{marginTop: 10, display: 'flex'}}>
|
||||
<Space>
|
||||
<Checkbox
|
||||
checked={batch}
|
||||
label='批量创建'
|
||||
name='batch'
|
||||
onChange={() => setBatch(!batch)}
|
||||
/>
|
||||
<Typography.Text strong>批量创建</Typography.Text>
|
||||
</Space>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
inputs.type !== 3 && inputs.type !== 8 && inputs.type !== 22 && (
|
||||
<Form.Field>
|
||||
<Form.Input
|
||||
<>
|
||||
<div style={{marginTop: 10}}>
|
||||
<Typography.Text strong>代理:</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
label='代理'
|
||||
name='base_url'
|
||||
placeholder={'此项可选,用于通过代理站来进行 API 调用,请输入代理站地址,格式为:https://domain.com'}
|
||||
onChange={handleInputChange}
|
||||
placeholder={'此项可选,用于通过代理站来进行 API 调用'}
|
||||
onChange={value => {
|
||||
handleInputChange('base_url', value)
|
||||
}}
|
||||
value={inputs.base_url}
|
||||
autoComplete='new-password'
|
||||
/>
|
||||
</Form.Field>
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
inputs.type === 22 && (
|
||||
<Form.Field>
|
||||
<Form.Input
|
||||
label='私有部署地址'
|
||||
<>
|
||||
<div style={{marginTop: 10}}>
|
||||
<Typography.Text strong>私有部署地址:</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
name='base_url'
|
||||
placeholder={'请输入私有部署地址,格式为:https://fastgpt.run/api/openapi'}
|
||||
onChange={handleInputChange}
|
||||
onChange={value => {
|
||||
handleInputChange('base_url', value)
|
||||
}}
|
||||
value={inputs.base_url}
|
||||
autoComplete='new-password'
|
||||
/>
|
||||
</Form.Field>
|
||||
</>
|
||||
)
|
||||
}
|
||||
<Button onClick={handleCancel}>取消</Button>
|
||||
<Button type={isEdit ? 'button' : 'submit'} positive onClick={submit}>提交</Button>
|
||||
</Form>
|
||||
</Segment>
|
||||
|
||||
</Spin>
|
||||
</SideSheet>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -1,13 +1,18 @@
|
||||
import React from 'react';
|
||||
import { Header, Segment } from 'semantic-ui-react';
|
||||
import ChannelsTable from '../../components/ChannelsTable';
|
||||
import {Layout} from "@douyinfe/semi-ui";
|
||||
import RedemptionsTable from "../../components/RedemptionsTable";
|
||||
|
||||
const File = () => (
|
||||
<>
|
||||
<Segment>
|
||||
<Header as='h3'>管理渠道</Header>
|
||||
<Layout>
|
||||
<Layout.Header>
|
||||
<h3>管理渠道</h3>
|
||||
</Layout.Header>
|
||||
<Layout.Content>
|
||||
<ChannelsTable/>
|
||||
</Segment>
|
||||
</Layout.Content>
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user