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