diff --git a/controller/channel-billing.go b/controller/channel-billing.go index 6ddad7e..30c9cbc 100644 --- a/controller/channel-billing.go +++ b/controller/channel-billing.go @@ -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 } diff --git a/controller/channel-test.go b/controller/channel-test.go index c0ac9a6..c441a48 100644 --- a/controller/channel-test.go +++ b/controller/channel-test.go @@ -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 } diff --git a/controller/channel.go b/controller/channel.go index 98f4242..f57c4d7 100644 --- a/controller/channel.go +++ b/controller/channel.go @@ -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, diff --git a/model/channel.go b/model/channel.go index e2ffda0..ae69734 100644 --- a/model/channel.go +++ b/model/channel.go @@ -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"` } - err = DB.Omit("key").Where("id = ? or name LIKE ? or "+keyCol+" = ?", common.String2Int(keyword), keyword+"%", keyword).Find(&channels).Error + 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 } diff --git a/web/src/components/ChannelsTable.js b/web/src/components/ChannelsTable.js index 50e58dd..bd54b15 100644 --- a/web/src/components/ChannelsTable.js +++ b/web/src/components/ChannelsTable.js @@ -1,583 +1,633 @@ -import React, { useEffect, useState } from 'react'; -import { Button, Form, Input, Label, Message, Pagination, Popup, Table } from 'semantic-ui-react'; -import { Link } from 'react-router-dom'; -import { API, setPromptShown, shouldShowPrompt, showError, showInfo, showSuccess, timestamp2string } from '../helpers'; +import React, {useEffect, useState} from '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 {CHANNEL_OPTIONS, ITEMS_PER_PAGE} from '../constants'; +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 ( - <> - {timestamp2string(timestamp)} - - ); + return ( + <> + {timestamp2string(timestamp)} + + ); } let type2label = undefined; function renderType(type) { - if (!type2label) { - type2label = new Map; - for (let i = 0; i < CHANNEL_OPTIONS.length; i++) { - type2label[CHANNEL_OPTIONS[i].value] = CHANNEL_OPTIONS[i]; + if (!type2label) { + type2label = new Map; + for (let i = 0; i < CHANNEL_OPTIONS.length; i++) { + type2label[CHANNEL_OPTIONS[i].value] = CHANNEL_OPTIONS[i]; + } + type2label[0] = {value: 0, text: '未知类型', color: 'grey'}; } - type2label[0] = { value: 0, text: '未知类型', color: 'grey' }; - } - return ; + return {type2label[type]?.text}; } function renderBalance(type, balance) { - switch (type) { - case 1: // OpenAI - return ${balance.toFixed(2)}; - case 4: // CloseAI - return ¥{balance.toFixed(2)}; - case 8: // 自定义 - return ${balance.toFixed(2)}; - case 5: // OpenAI-SB - return ¥{(balance / 10000).toFixed(2)}; - case 10: // AI Proxy - return {renderNumber(balance)}; - case 12: // API2GPT - return ¥{balance.toFixed(2)}; - case 13: // AIGC2D - return {renderNumber(balance)}; - default: - return 不支持; - } + switch (type) { + case 1: // OpenAI + return ${balance.toFixed(2)}; + case 4: // CloseAI + return ¥{balance.toFixed(2)}; + case 8: // 自定义 + return ${balance.toFixed(2)}; + case 5: // OpenAI-SB + return ¥{(balance / 10000).toFixed(2)}; + case 10: // AI Proxy + return {renderNumber(balance)}; + case 12: // API2GPT + return ¥{balance.toFixed(2)}; + case 13: // AIGC2D + return {renderNumber(balance)}; + default: + return 不支持; + } } const ChannelsTable = () => { - const [channels, setChannels] = useState([]); - const [loading, setLoading] = useState(true); - const [activePage, setActivePage] = useState(1); - const [searchKeyword, setSearchKeyword] = 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 loadChannels = async (startIdx) => { - const res = await API.get(`/api/channel/?p=${startIdx}&page_size=${pageSize}`); - const { success, message, data } = res.data; - if (success) { - if (startIdx === 0) { - setChannels(data); - } else { - let newChannels = [...channels]; - newChannels.splice(startIdx * pageSize, data.length, ...data); - setChannels(newChannels); - } - } else { - showError(message); - } - 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); - } - - const refresh = async () => { - setLoading(true); - await loadChannels(activePage - 1); - }; - - useEffect(() => { - loadChannels(0) - .then() - .catch((reason) => { - showError(reason); - }); - }, []); - - const manageChannel = async (id, action, idx, value) => { - let data = { id }; - let res; - switch (action) { - case 'delete': - res = await API.delete(`/api/channel/${id}/`); - break; - case 'enable': - data.status = 1; - res = await API.put('/api/channel/', data); - break; - case 'disable': - data.status = 2; - res = await API.put('/api/channel/', data); - break; - case 'priority': - if (value === '') { - return; - } - data.priority = parseInt(value); - res = await API.put('/api/channel/', data); - break; - case 'weight': - if (value === '') { - return; - } - data.weight = parseInt(value); - if (data.weight < 0) { - data.weight = 0; - } - res = await API.put('/api/channel/', data); - break; - } - const { success, message } = res.data; - if (success) { - 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; - } - setChannels(newChannels); - } else { - showError(message); - } - }; - - const renderStatus = (status) => { - switch (status) { - case 1: - return ; - case 2: - return ( - - 已禁用 - } - content='本渠道被手动禁用' - basic - /> - ); - case 3: - return ( - - 已禁用 - } - content='本渠道被程序自动禁用' - basic - /> - ); - default: - return ( - - ); - } - }; - - const renderResponseTime = (responseTime) => { - let time = responseTime / 1000; - time = time.toFixed(2) + ' 秒'; - if (responseTime === 0) { - return ; - } else if (responseTime <= 1000) { - return ; - } else if (responseTime <= 3000) { - return ; - } else if (responseTime <= 5000) { - return ; - } else { - return ; - } - }; - - const searchChannels = async () => { - if (searchKeyword === '') { - // 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 { success, message, data } = res.data; - if (success) { - setChannels(data); - setActivePage(1); - } else { - showError(message); - } - setSearching(false); - }; - - const testChannel = async (id, name, idx) => { - const res = await API.get(`/api/channel/test/${id}/`); - const { success, message, time } = res.data; - if (success) { - let newChannels = [...channels]; - let realIdx = (activePage - 1) * pageSize + idx; - newChannels[realIdx].response_time = time * 1000; - newChannels[realIdx].test_time = Date.now() / 1000; - setChannels(newChannels); - showInfo(`通道 ${name} 测试成功,耗时 ${time.toFixed(2)} 秒。`); - } else { - showError(message); - } - }; - - const testAllChannels = async () => { - const res = await API.get(`/api/channel/test`); - const { success, message } = res.data; - if (success) { - showInfo('已成功开始测试所有已启用通道,请刷新页面查看结果。'); - } else { - showError(message); - } - }; - - const deleteAllDisabledChannels = async () => { - const res = await API.delete(`/api/channel/disabled`); - const { success, message, data } = res.data; - if (success) { - showSuccess(`已删除所有禁用渠道,共计 ${data} 个`); - await refresh(); - } else { - showError(message); - } - }; - - const updateChannelBalance = async (id, name, idx) => { - const res = await API.get(`/api/channel/update_balance/${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} 余额更新成功!`); - } else { - showError(message); - } - }; - - const updateAllChannelsBalance = async () => { - setUpdatingBalance(true); - const res = await API.get(`/api/channel/update_balance`); - const { success, message } = res.data; - if (success) { - showInfo('已更新完毕所有已启用通道余额!'); - } else { - showError(message); - } - setUpdatingBalance(false); - }; - - const handleKeywordChange = async (e, { value }) => { - setSearchKeyword(value.trim()); - }; - - const sortChannel = (key) => { - if (channels.length === 0) return; - setLoading(true); - let sortedChannels = [...channels]; - if (typeof sortedChannels[0][key] === 'string') { - sortedChannels.sort((a, b) => { - return ('' + a[key]).localeCompare(b[key]); - }); - } else { - sortedChannels.sort((a, b) => { - if (a[key] === b[key]) return 0; - if (a[key] > b[key]) return -1; - if (a[key] < b[key]) return 1; - }); - } - if (sortedChannels[0].id === channels[0].id) { - sortedChannels.reverse(); - } - setChannels(sortedChannels); - setLoading(false); - }; - - return ( - <> -
- - - { - showPrompt && ( - { - setShowPrompt(false); - setPromptShown("channel-test"); - }}> - 当前版本测试是通过按照 OpenAI API 格式使用 gpt-3.5-turbo - 模型进行非流式请求实现的,因此测试报错并不一定代表通道不可用,该功能后续会修复。 - - 另外,OpenAI 渠道已经不再支持通过 key 获取余额,因此余额显示为 0。对于支持的渠道类型,请点击余额进行刷新。 - - ) - } - - - - { - sortChannel('id'); - }} - > - ID - - { - sortChannel('name'); - }} - > - 名称 - - { - sortChannel('group'); - }} - width={1} - > - 分组 - - { - sortChannel('type'); - }} - width={2} - > - 类型 - - { - sortChannel('status'); - }} - width={2} - > - 状态 - - { - sortChannel('response_time'); - }} - > - 响应时间 - - { - sortChannel('used_quota'); - }} - width={1} - > - 已使用 - - { - sortChannel('balance'); - }} - > - 余额 - - { - sortChannel('priority'); - }} - > - 优先级 - - 操作 - - - - - {channels - .slice( - (activePage - 1) * pageSize, - activePage * pageSize - ) - .map((channel, idx) => { - if (channel.deleted) return <>; - return ( - - {channel.id} - {channel.name ? channel.name : '无'} - {renderGroup(channel.group)} - {renderType(channel.type)} - {renderStatus(channel.status)} - - - - {renderQuota(channel.used_quota)} - - { - updateChannelBalance(channel.id, channel.name, idx); - }} style={{ cursor: 'pointer' }}> - {renderBalance(channel.type, channel.balance)} - } - content='点击更新' - basic - /> - - - { - manageChannel( - channel.id, - 'priority', - idx, - event.target.value - ); - }}> - - } - content='渠道选择优先级,越高越优先' - basic - /> - - + const columns = [ + { + title: 'ID', + dataIndex: 'id', + }, + { + title: '名称', + dataIndex: 'name', + }, + { + title: '分组', + dataIndex: 'group', + render: (text, record, index) => { + return (
- - - 删除 - - } - on='click' - flowing - hoverable - > - - - - + + { + text.split(',').map((item, index) => { + return (renderGroup(item)) + }) + } +
-
-
- ); - })} -
- - - - - - - - -
-
-
每页数量
- -
- { + return ( +
+ {renderType(text)} +
+ ); + }, + }, + { + title: '状态', + dataIndex: 'status', + render: (text, record, index) => { + return ( +
+ {renderStatus(text)} +
+ ); + }, + }, + { + title: '响应时间', + dataIndex: 'response_time', + render: (text, record, index) => { + return ( +
+ {renderResponseTime(text)} +
+ ); + }, + }, + { + title: '已用/剩余', + dataIndex: 'expired_time', + render: (text, record, index) => { + return ( +
+ + + {renderQuota(record.used_quota)} + + + {updateChannelBalance(record)}}>{renderQuota(record.balance)} + + +
+ ); + }, + }, + { + title: '优先级', + dataIndex: 'priority', + render: (text, record, index) => { + return ( +
+ { + manageChannel(record.id, 'priority', record, value); + }} + defaultValue={record.priority} + min={0} + /> +
+ ); + }, + }, + { + title: '', + dataIndex: 'operate', + render: (text, record, index) => ( +
+ { + manageChannel(record.id, 'delete', record).then( + () => { + removeRecord(record.id); + } + ) + }} + > + + + { + record.status === 1 ? + : + } - /> -
- - 删除禁用渠道 - - } - on='click' - flowing - hoverable - > - - - + +
+ ), + }, + ]; -
-
-
-
- - ); + 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) => { + 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) { + setChannelFormat(data); + } else { + let newChannels = [...channels]; + newChannels.splice(startIdx * pageSize, data.length, ...data); + setChannelFormat(newChannels); + } + } else { + showError(message); + } + setLoading(false); + }; + + useEffect(() => { + loadChannels(0) + .then() + .catch((reason) => { + showError(reason); + }); + }, [pageSize]); + + const refresh = async () => { + await loadChannels(activePage - 1); + }; + + useEffect(() => { + loadChannels(0) + .then() + .catch((reason) => { + showError(reason); + }); + fetchGroups().then(); + console.log(localStorage.getItem('id-sort')) + if (localStorage.getItem('id-sort') === 'true') { + setIdSort(true) + } + }, []); + + 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) { + case 'delete': + res = await API.delete(`/api/channel/${id}/`); + break; + case 'enable': + data.status = 1; + res = await API.put('/api/channel/', data); + break; + case 'disable': + data.status = 2; + res = await API.put('/api/channel/', data); + break; + case 'priority': + if (value === '') { + return; + } + data.priority = parseInt(value); + res = await API.put('/api/channel/', data); + break; + case 'weight': + if (value === '') { + return; + } + data.weight = parseInt(value); + if (data.weight < 0) { + data.weight = 0; + } + res = await API.put('/api/channel/', data); + break; + } + const {success, message} = res.data; + if (success) { + showSuccess('操作成功完成!'); + let channel = res.data.data; + let newChannels = [...channels]; + if (action === 'delete') { + + } else { + record.status = channel.status; + } + setChannels(newChannels); + } else { + showError(message); + } + }; + + const renderStatus = (status) => { + switch (status) { + case 1: + return 已启用; + case 2: + return ( + + 已禁用 + } + content='本渠道被手动禁用' + basic + /> + ); + case 3: + return ( + + 已禁用 + } + content='本渠道被程序自动禁用' + basic + /> + ); + default: + return ( + + 未知状态 + + ); + } + }; + + const renderResponseTime = (responseTime) => { + let time = responseTime / 1000; + time = time.toFixed(2) + ' 秒'; + if (responseTime === 0) { + return 未测试; + } else if (responseTime <= 1000) { + return {time}; + } else if (responseTime <= 3000) { + return {time}; + } else if (responseTime <= 5000) { + return {time}; + } else { + return {time}; + } + }; + + const searchChannels = async () => { + 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}&group=${searchGroup}`); + const {success, message, data} = res.data; + if (success) { + setChannels(data); + setActivePage(1); + } else { + showError(message); + } + setSearching(false); + }; + + const testChannel = async (id, name, idx) => { + const res = await API.get(`/api/channel/test/${id}/`); + const {success, message, time} = res.data; + if (success) { + let newChannels = [...channels]; + let realIdx = (activePage - 1) * pageSize + idx; + newChannels[realIdx].response_time = time * 1000; + newChannels[realIdx].test_time = Date.now() / 1000; + setChannels(newChannels); + showInfo(`通道 ${name} 测试成功,耗时 ${time.toFixed(2)} 秒。`); + } else { + showError(message); + } + }; + + const testAllChannels = async () => { + const res = await API.get(`/api/channel/test`); + const {success, message} = res.data; + if (success) { + showInfo('已成功开始测试所有已启用通道,请刷新页面查看结果。'); + } else { + showError(message); + } + }; + + const deleteAllDisabledChannels = async () => { + const res = await API.delete(`/api/channel/disabled`); + const {success, message, data} = res.data; + if (success) { + showSuccess(`已删除所有禁用渠道,共计 ${data} 个`); + await refresh(); + } else { + showError(message); + } + }; + + const updateChannelBalance = async (record) => { + const res = await API.get(`/api/channel/update_balance/${record.id}/`); + const {success, message, balance} = res.data; + if (success) { + record.balance = balance; + record.balance_updated_time = Date.now() / 1000; + showInfo(`通道 ${record.name} 余额更新成功!`); + } else { + showError(message); + } + }; + + const updateAllChannelsBalance = async () => { + setUpdatingBalance(true); + const res = await API.get(`/api/channel/update_balance`); + const {success, message} = res.data; + if (success) { + showInfo('已更新完毕所有已启用通道余额!'); + } else { + showError(message); + } + setUpdatingBalance(false); + }; + + const sortChannel = (key) => { + if (channels.length === 0) return; + setLoading(true); + let sortedChannels = [...channels]; + if (typeof sortedChannels[0][key] === 'string') { + sortedChannels.sort((a, b) => { + return ('' + a[key]).localeCompare(b[key]); + }); + } else { + sortedChannels.sort((a, b) => { + if (a[key] === b[key]) return 0; + if (a[key] > b[key]) return -1; + if (a[key] < b[key]) return 1; + }); + } + if (sortedChannels[0].id === channels[0].id) { + sortedChannels.reverse(); + } + setChannels(sortedChannels); + 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 ( + <> + +
+ +
+ + { + setSearchKeyword(v.trim()) + }} + /> + { + setSearchGroup(v) + }}/> + +
+
+
+ + 使用ID排序 + { + setIdSort(v) + }}> + +
+ + { + setPageSize(size) + setActivePage(1) + }, + onPageChange: handlePageChange, + }} loading={loading} onRow={handleRow}/> +
+ + + + + + + + + + + + + + +
+ + ); }; export default ChannelsTable; diff --git a/web/src/components/RedemptionsTable.js b/web/src/components/RedemptionsTable.js index ae9b219..b75d4fa 100644 --- a/web/src/components/RedemptionsTable.js +++ b/web/src/components/RedemptionsTable.js @@ -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); diff --git a/web/src/constants/channel.constants.js b/web/src/constants/channel.constants.js index 6da8daf..82d21c6 100644 --- a/web/src/constants/channel.constants.js +++ b/web/src/constants/channel.constants.js @@ -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' } -]; \ No newline at end of file + { 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' }, +]; diff --git a/web/src/pages/Channel/EditChannel.js b/web/src/pages/Channel/EditChannel.js index a1cd4d7..d38f267 100644 --- a/web/src/pages/Channel/EditChannel.js +++ b/web/src/pages/Channel/EditChannel.js @@ -1,513 +1,596 @@ -import React, { useEffect, useState } from 'react'; -import { Button, Form, Header, Input, Message, Segment } from 'semantic-ui-react'; -import { useNavigate, useParams } from 'react-router-dom'; -import { API, showError, showInfo, showSuccess, verifyJSON } from '../../helpers'; -import { CHANNEL_OPTIONS } from '../../constants'; +import React, {useEffect, useRef, useState} from 'react'; +import {useNavigate, useParams} from 'react-router-dom'; +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', - 'gpt-4-0314': 'gpt-4', - 'gpt-4-32k-0314': 'gpt-4-32k' + 'gpt-3.5-turbo-0301': 'gpt-3.5-turbo', + 'gpt-4-0314': 'gpt-4', + 'gpt-4-32k-0314': 'gpt-4-32k' }; function type2secretPrompt(type) { - // inputs.type === 15 ? '按照如下格式输入:APIKey|SecretKey' : (inputs.type === 18 ? '按照如下格式输入:APPID|APISecret|APIKey' : '请输入渠道对应的鉴权密钥') - switch (type) { - case 15: - return '按照如下格式输入:APIKey|SecretKey'; - case 18: - return '按照如下格式输入:APPID|APISecret|APIKey'; - case 22: - return '按照如下格式输入:APIKey-AppId,例如:fastgpt-0sp2gtvfdgyi4k30jwlgwf1i-64f335d84283f05518e9e041'; - case 23: - return '按照如下格式输入:AppId|SecretId|SecretKey'; - default: - return '请输入渠道对应的鉴权密钥'; - } + // inputs.type === 15 ? '按照如下格式输入:APIKey|SecretKey' : (inputs.type === 18 ? '按照如下格式输入:APPID|APISecret|APIKey' : '请输入渠道对应的鉴权密钥') + switch (type) { + case 15: + return '按照如下格式输入:APIKey|SecretKey'; + case 18: + return '按照如下格式输入:APPID|APISecret|APIKey'; + case 22: + return '按照如下格式输入:APIKey-AppId,例如:fastgpt-0sp2gtvfdgyi4k30jwlgwf1i-64f335d84283f05518e9e041'; + case 23: + return '按照如下格式输入:AppId|SecretId|SecretKey'; + default: + return '请输入渠道对应的鉴权密钥'; + } } -const EditChannel = () => { - const params = useParams(); - const navigate = useNavigate(); - const channelId = params.id; - const isEdit = channelId !== undefined; - const [loading, setLoading] = useState(isEdit); - const handleCancel = () => { - navigate('/channel'); - }; +const EditChannel = (props) => { + const navigate = useNavigate(); + const channelId = props.editingChannel.id; + const isEdit = channelId !== undefined; + const [loading, setLoading] = useState(isEdit); + const handleCancel = () => { + props.handleClose() + }; + const originInputs = { + name: '', + type: 1, + key: '', + openai_organization: '', + base_url: '', + other: '', + model_mapping: '', + models: [], + auto_ban: 1, + groups: ['default'] + }; + const [batch, setBatch] = useState(false); + const [autoBan, setAutoBan] = useState(true); + // const [autoBan, setAutoBan] = useState(true); + const [inputs, setInputs] = useState(originInputs); + const [originModelOptions, setOriginModelOptions] = useState([]); + const [modelOptions, setModelOptions] = useState([]); + const [groupOptions, setGroupOptions] = useState([]); + const [basicModels, setBasicModels] = useState([]); + const [fullModels, setFullModels] = useState([]); + const [customModel, setCustomModel] = useState(''); + const handleInputChange = (name, value) => { + setInputs((inputs) => ({...inputs, [name]: value})); + if (name === 'type' && inputs.models.length === 0) { + let localModels = []; + switch (value) { + case 14: + localModels = ['claude-instant-1', 'claude-2']; + break; + case 11: + localModels = ['PaLM-2']; + break; + case 15: + localModels = ['ERNIE-Bot', 'ERNIE-Bot-turbo', 'ERNIE-Bot-4', 'Embedding-V1']; + break; + case 17: + localModels = ['qwen-turbo', 'qwen-plus', 'text-embedding-v1']; + break; + case 16: + localModels = ['chatglm_pro', 'chatglm_std', 'chatglm_lite']; + break; + case 18: + localModels = ['SparkDesk']; + break; + case 19: + localModels = ['360GPT_S2_V9', 'embedding-bert-512-v1', 'embedding_s1_v1', 'semantic_similarity_s1_v1']; + break; + case 23: + localModels = ['hunyuan']; + break; + } + setInputs((inputs) => ({...inputs, models: localModels})); + } + //setAutoBan + }; - const originInputs = { - name: '', - type: 1, - key: '', - openai_organization:'', - base_url: '', - other: '', - model_mapping: '', - models: [], - auto_ban: 1, - groups: ['default'] - }; - const [batch, setBatch] = useState(false); - const [autoBan, setAutoBan] = useState(true); - // const [autoBan, setAutoBan] = useState(true); - const [inputs, setInputs] = useState(originInputs); - const [originModelOptions, setOriginModelOptions] = useState([]); - const [modelOptions, setModelOptions] = useState([]); - const [groupOptions, setGroupOptions] = useState([]); - const [basicModels, setBasicModels] = useState([]); - const [fullModels, setFullModels] = useState([]); - const [customModel, setCustomModel] = useState(''); - const handleInputChange = (e, { name, value }) => { - setInputs((inputs) => ({ ...inputs, [name]: value })); - if (name === 'type' && inputs.models.length === 0) { - let localModels = []; - switch (value) { - case 14: - localModels = ['claude-instant-1', 'claude-2']; - break; - case 11: - localModels = ['PaLM-2']; - break; - case 15: - localModels = ['ERNIE-Bot', 'ERNIE-Bot-turbo', 'ERNIE-Bot-4', 'Embedding-V1']; - break; - case 17: - localModels = ['qwen-turbo', 'qwen-plus', 'text-embedding-v1']; - break; - case 16: - localModels = ['chatglm_pro', 'chatglm_std', 'chatglm_lite']; - break; - case 18: - localModels = ['SparkDesk']; - break; - case 19: - localModels = ['360GPT_S2_V9', 'embedding-bert-512-v1', 'embedding_s1_v1', 'semantic_similarity_s1_v1']; - break; - case 23: - localModels = ['hunyuan']; - break; - } - setInputs((inputs) => ({ ...inputs, models: localModels })); - } - //setAutoBan - }; - const loadChannel = async () => { - let res = await API.get(`/api/channel/${channelId}`); - const { success, message, data } = res.data; - if (success) { - if (data.models === '') { - data.models = []; - } else { - data.models = data.models.split(','); - } - if (data.group === '') { - data.groups = []; - } else { - data.groups = data.group.split(','); - } - if (data.model_mapping !== '') { - data.model_mapping = JSON.stringify(JSON.parse(data.model_mapping), null, 2); - } - setInputs(data); - if (data.auto_ban === 0) { - setAutoBan(false); - } else { - setAutoBan(true); - } - // console.log(data); - } else { - showError(message); - } - setLoading(false); - }; + const loadChannel = async () => { + setLoading(true) + let res = await API.get(`/api/channel/${channelId}`); + const {success, message, data} = res.data; + if (success) { + if (data.models === '') { + data.models = []; + } else { + data.models = data.models.split(','); + } + if (data.group === '') { + data.groups = []; + } else { + data.groups = data.group.split(','); + } + if (data.model_mapping !== '') { + data.model_mapping = JSON.stringify(JSON.parse(data.model_mapping), null, 2); + } + setInputs(data); + if (data.auto_ban === 0) { + setAutoBan(false); + } else { + setAutoBan(true); + } + // console.log(data); + } else { + showError(message); + } + setLoading(false); + }; - const fetchModels = async () => { - try { - let res = await API.get(`/api/channel/models`); - let localModelOptions = res.data.data.map((model) => ({ - key: model.id, - text: model.id, - value: model.id - })); - setOriginModelOptions(localModelOptions); - setFullModels(res.data.data.map((model) => model.id)); - setBasicModels(res.data.data.filter((model) => { - return model.id.startsWith('gpt-3') || model.id.startsWith('text-'); - }).map((model) => model.id)); - } catch (error) { - showError(error.message); - } - }; + const fetchModels = async () => { + try { + let res = await API.get(`/api/channel/models`); + let localModelOptions = res.data.data.map((model) => ({ + label: model.id, + value: model.id + })); + setOriginModelOptions(localModelOptions); + setFullModels(res.data.data.map((model) => model.id)); + setBasicModels(res.data.data.filter((model) => { + return model.id.startsWith('gpt-3') || model.id.startsWith('text-'); + }).map((model) => model.id)); + } catch (error) { + showError(error.message); + } + }; - const fetchGroups = async () => { - try { - let res = await API.get(`/api/group/`); - setGroupOptions(res.data.data.map((group) => ({ - key: group, - text: group, - value: group - }))); - } catch (error) { - showError(error.message); - } - }; + const fetchGroups = async () => { + try { + let res = await API.get(`/api/group/`); + setGroupOptions(res.data.data.map((group) => ({ + label: group, + value: group + }))); + } catch (error) { + showError(error.message); + } + }; - useEffect(() => { - let localModelOptions = [...originModelOptions]; - inputs.models.forEach((model) => { - if (!localModelOptions.find((option) => option.key === model)) { - localModelOptions.push({ - key: model, - text: model, - value: model + useEffect(() => { + let localModelOptions = [...originModelOptions]; + inputs.models.forEach((model) => { + if (!localModelOptions.find((option) => option.key === model)) { + localModelOptions.push({ + label: model, + value: model + }); + } }); - } - }); - setModelOptions(localModelOptions); - }, [originModelOptions, inputs.models]); + setModelOptions(localModelOptions); + }, [originModelOptions, inputs.models]); - useEffect(() => { - if (isEdit) { - loadChannel().then(); - } - fetchModels().then(); - fetchGroups().then(); - }, []); + useEffect(() => { + fetchModels().then(); + fetchGroups().then(); + if (isEdit) { + loadChannel().then( + () => { - useEffect(() => { - setInputs((inputs) => ({ ...inputs, auto_ban: autoBan ? 1 : 0 })); - console.log(autoBan); - }, [autoBan]); - - const submit = async () => { - if (!isEdit && (inputs.name === '' || inputs.key === '')) { - showInfo('请填写渠道名称和渠道密钥!'); - return; - } - if (inputs.models.length === 0) { - showInfo('请至少选择一个模型!'); - return; - } - if (inputs.model_mapping !== '' && !verifyJSON(inputs.model_mapping)) { - showInfo('模型映射必须是合法的 JSON 格式!'); - return; - } - let localInputs = inputs; - if (localInputs.base_url && localInputs.base_url.endsWith('/')) { - localInputs.base_url = localInputs.base_url.slice(0, localInputs.base_url.length - 1); - } - if (localInputs.type === 3 && localInputs.other === '') { - localInputs.other = '2023-06-01-preview'; - } - if (localInputs.type === 18 && localInputs.other === '') { - localInputs.other = 'v2.1'; - } - let res; - if (!Array.isArray(localInputs.models)) { - showError('提交失败,请勿重复提交!'); - handleCancel(); - return; - } - localInputs.models = localInputs.models.join(','); - localInputs.group = localInputs.groups.join(','); - if (isEdit) { - res = await API.put(`/api/channel/`, { ...localInputs, id: parseInt(channelId) }); - } else { - res = await API.post(`/api/channel/`, localInputs); - } - const { success, message } = res.data; - if (success) { - if (isEdit) { - showSuccess('渠道更新成功!'); - } else { - showSuccess('渠道创建成功!'); - setInputs(originInputs); - } - } else { - showError(message); - } - }; - - const addCustomModel = () => { - if (customModel.trim() === '') return; - if (inputs.models.includes(customModel)) return; - let localModels = [...inputs.models]; - localModels.push(customModel); - let localModelOptions = []; - localModelOptions.push({ - key: customModel, - text: customModel, - value: customModel - }); - setModelOptions(modelOptions => { - return [...modelOptions, ...localModelOptions]; - }); - setCustomModel(''); - handleInputChange(null, { name: 'models', value: localModels }); - }; - - return ( - <> - -
{isEdit ? '更新渠道信息' : '创建新的渠道'}
-
- - - - { - inputs.type === 3 && ( - <> - - 注意,模型部署名称必须和模型名称保持一致,因为 One API 会把请求体中的 model - 参数替换为你的部署名称(模型名称中的点会被剔除),图片演示。 - - - - - - - - - ) - } - { - inputs.type === 8 && ( - - - - ) - } - - - - - - - { - inputs.type === 18 && ( - - - - ) - } - { - inputs.type === 21 && ( - - - - ) - } - - - -
- - - - 填入 - } - placeholder='输入自定义模型名称' - value={customModel} - onChange={(e, { value }) => { - setCustomModel(value); - }} - onKeyDown={(e) => { - if (e.key === 'Enter') { - addCustomModel(); - e.preventDefault(); } - }} - /> -
- - - - { - batch ? - - : - - - } - - - - - { - setAutoBan(!autoBan); + ); + } else { + setInputs(originInputs) + } + }, [props.editingChannel.id]); + + const submit = async () => { + if (!isEdit && (inputs.name === '' || inputs.key === '')) { + showInfo('请填写渠道名称和渠道密钥!'); + return; + } + if (inputs.models.length === 0) { + showInfo('请至少选择一个模型!'); + return; + } + if (inputs.model_mapping !== '' && !verifyJSON(inputs.model_mapping)) { + showInfo('模型映射必须是合法的 JSON 格式!'); + return; + } + let localInputs = {...inputs}; + if (localInputs.base_url && localInputs.base_url.endsWith('/')) { + localInputs.base_url = localInputs.base_url.slice(0, localInputs.base_url.length - 1); + } + if (localInputs.type === 3 && localInputs.other === '') { + localInputs.other = '2023-06-01-preview'; + } + if (localInputs.type === 18 && localInputs.other === '') { + localInputs.other = 'v2.1'; + } + let res; + if (!Array.isArray(localInputs.models)) { + showError('提交失败,请勿重复提交!'); + handleCancel(); + return; + } + localInputs.models = localInputs.models.join(','); + localInputs.group = localInputs.groups.join(','); + if (isEdit) { + res = await API.put(`/api/channel/`, {...localInputs, id: parseInt(channelId)}); + } else { + res = await API.post(`/api/channel/`, localInputs); + } + const {success, message} = res.data; + if (success) { + if (isEdit) { + showSuccess('渠道更新成功!'); + } else { + showSuccess('渠道创建成功!'); + setInputs(originInputs); + } + props.refresh(); + props.handleClose(); + } else { + showError(message); + } + }; + + const addCustomModel = () => { + if (customModel.trim() === '') return; + if (inputs.models.includes(customModel)) return; + let localModels = [...inputs.models]; + localModels.push(customModel); + let localModelOptions = []; + localModelOptions.push({ + key: customModel, + text: customModel, + value: customModel + }); + setModelOptions(modelOptions => { + return [...modelOptions, ...localModelOptions]; + }); + setCustomModel(''); + handleInputChange('models', localModels); + }; + + return ( + <> + {isEdit ? '更新渠道信息' : '创建新的渠道'}} + headerStyle={{borderBottom: '1px solid var(--semi-color-border)'}} + bodyStyle={{borderBottom: '1px solid var(--semi-color-border)'}} + visible={props.visible} + footer={ +
+ + + + +
+ } + closeIcon={null} + onCancel={() => handleCancel()} + width={isMobile() ? '100%' : 600} + > + +
+ 类型: +
+ { + handleInputChange('base_url', value) + }} + value={inputs.base_url} + autoComplete='new-password' + /> +
+ 默认 API 版本: +
+ { + handleInputChange('other', value) + }} + value={inputs.other} + autoComplete='new-password' + /> + + ) } - } - // onChange={handleInputChange} - /> -
- { - !isEdit && ( - setBatch(!batch)} - /> - ) - } - { - inputs.type !== 3 && inputs.type !== 8 && inputs.type !== 22 && ( - - - - ) - } - { - inputs.type === 22 && ( - - - - ) - } - - - -
- - ); + { + inputs.type === 8 && ( + <> +
+ Base URL: +
+ { + handleInputChange('base_url', value) + }} + value={inputs.base_url} + autoComplete='new-password' + /> + + ) + } +
+ 名称: +
+ { + handleInputChange('name', value) + }} + value={inputs.name} + autoComplete='new-password' + /> +
+ 分组: +
+ { + handleInputChange('other', value) + }} + value={inputs.other} + autoComplete='new-password' + /> + + ) + } + { + inputs.type === 21 && ( + <> +
+ 知识库 ID: +
+ { + handleInputChange('other', value) + }} + value={inputs.other} + autoComplete='new-password' + /> + + ) + } +
+ 模型: +
+ 填入 + } + placeholder='输入自定义模型名称' + value={customModel} + onChange={(value) => { + setCustomModel(value); + }} + /> + +
+ 模型重定向: +
+