2023-10-31 22:41:34 +08:00
import React , { useEffect , useState } from 'react' ;
import { API , copy , isAdmin , showError , showSuccess , showWarning , timestamp2string } from '../helpers' ;
2023-04-23 11:31:00 +08:00
2023-10-31 22:41:34 +08:00
import { ITEMS _PER _PAGE } from '../constants' ;
import { renderQuota , stringToColor } from '../helpers/render' ;
2023-12-11 20:11:26 +08:00
import {
Avatar ,
Tag ,
Table ,
Button ,
Popover ,
Form ,
Modal ,
Popconfirm ,
SplitButtonGroup ,
Dropdown
} from "@douyinfe/semi-ui" ;
import {
IconTreeTriangleDown ,
} from '@douyinfe/semi-icons' ;
2023-11-01 02:50:14 +08:00
import EditToken from "../pages/Token/EditToken" ;
2023-08-06 13:56:59 +08:00
const COPY _OPTIONS = [
2023-10-31 22:41:34 +08:00
{ key : 'next' , text : 'ChatGPT Next Web' , value : 'next' } ,
2024-01-16 18:15:55 +00:00
{ key : 'ama' , text : 'ChatGPT Web & Midjourney' , value : 'ama' } ,
2023-10-31 22:41:34 +08:00
{ key : 'opencat' , text : 'OpenCat' , value : 'opencat' } ,
2023-08-06 13:56:59 +08:00
] ;
2023-08-06 22:02:58 +08:00
const OPEN _LINK _OPTIONS = [
2024-01-16 18:15:55 +00:00
{ key : 'ama' , text : 'ChatGPT Web & Midjourney' , value : 'ama' } ,
2023-10-31 22:41:34 +08:00
{ key : 'opencat' , text : 'OpenCat' , value : 'opencat' } ,
2023-08-06 22:02:58 +08:00
] ;
2023-04-23 12:43:10 +08:00
function renderTimestamp ( timestamp ) {
2023-10-31 22:41:34 +08:00
return (
< >
{ timestamp2string ( timestamp ) }
< / >
) ;
2023-04-23 11:31:00 +08:00
}
2024-01-08 16:23:54 +08:00
function renderStatus ( status , model _limits _enabled = false ) {
2023-10-31 22:41:34 +08:00
switch ( status ) {
case 1 :
2024-01-08 16:23:54 +08:00
if ( model _limits _enabled ) {
return < Tag color = 'green' size = 'large' > 已启用 : 限制模型 < / T a g > ;
} else {
return < Tag color = 'green' size = 'large' > 已启用 < / T a g > ;
}
2023-10-31 22:41:34 +08:00
case 2 :
return < Tag color = 'red' size = 'large' > 已禁用 < / T a g > ;
case 3 :
return < Tag color = 'yellow' size = 'large' > 已过期 < / T a g > ;
case 4 :
return < Tag color = 'grey' size = 'large' > 已耗尽 < / T a g > ;
default :
return < Tag color = 'black' size = 'large' > 未知状态 < / T a g > ;
}
2023-04-24 20:52:40 +08:00
}
2023-04-23 11:31:00 +08:00
const TokensTable = ( ) => {
2023-12-11 20:11:26 +08:00
const link _menu = [
{ node : 'item' , key : 'next' , name : 'ChatGPT Next Web' , onClick : ( ) => { onOpenLink ( 'next' ) } } ,
{ node : 'item' , key : 'ama' , name : 'AMA 问天' , value : 'ama' } ,
2024-01-21 17:43:40 +08:00
{ node : 'item' , key : 'next-mj' , name : 'ChatGPT Web & Midjourney' , value : 'next-mj' , onClick : ( ) => { onOpenLink ( 'next-mj' ) } } ,
2023-12-11 20:11:26 +08:00
{ node : 'item' , key : 'opencat' , name : 'OpenCat' , value : 'opencat' } ,
] ;
2023-10-31 22:41:34 +08:00
const columns = [
{
title : '名称' ,
dataIndex : 'name' ,
} ,
{
title : '状态' ,
dataIndex : 'status' ,
key : 'status' ,
render : ( text , record , index ) => {
return (
< div >
2024-01-08 16:23:54 +08:00
{ renderStatus ( text , record . model _limits _enabled ) }
2023-10-31 22:41:34 +08:00
< / d i v >
) ;
} ,
} ,
{
title : '已用额度' ,
dataIndex : 'used_quota' ,
render : ( text , record , index ) => {
return (
< div >
{ renderQuota ( parseInt ( text ) ) }
< / d i v >
) ;
} ,
} ,
{
title : '剩余额度' ,
dataIndex : 'remain_quota' ,
render : ( text , record , index ) => {
return (
< div >
2023-11-01 02:50:14 +08:00
{ record . unlimited _quota ? < Tag size = { 'large' } color = { 'white' } > 无限制 < / T a g > : < T a g s i z e = { ' l a r g e ' } c o l o r = { ' l i g h t - b l u e ' } > { r e n d e r Q u o t a ( p a r s e I n t ( t e x t ) ) } < / T a g > }
2023-10-31 22:41:34 +08:00
< / d i v >
) ;
} ,
} ,
{
title : '创建时间' ,
dataIndex : 'created_time' ,
render : ( text , record , index ) => {
return (
< div >
{ renderTimestamp ( text ) }
< / d i v >
) ;
} ,
} ,
{
title : '过期时间' ,
2023-11-01 02:50:14 +08:00
dataIndex : 'expired_time' ,
2023-10-31 22:41:34 +08:00
render : ( text , record , index ) => {
return (
< div >
2023-11-01 02:50:14 +08:00
{ record . expired _time === - 1 ? "永不过期" : renderTimestamp ( text ) }
2023-10-31 22:41:34 +08:00
< / d i v >
) ;
} ,
} ,
{
title : '' ,
dataIndex : 'operate' ,
render : ( text , record , index ) => (
< div >
< Popover
content = {
'sk-' + record . key
}
style = { { padding : 20 } }
position = "top"
>
< Button theme = 'light' type = 'tertiary' style = { { marginRight : 1 } } > 查看 < / B u t t o n >
< / P o p o v e r >
< Button theme = 'light' type = 'secondary' style = { { marginRight : 1 } }
onClick = { async ( text ) => {
await copyText ( 'sk-' + record . key )
} }
> 复制 < / B u t t o n >
2023-12-11 20:11:26 +08:00
< SplitButtonGroup style = { { marginRight : 1 } } aria - label = "项目操作按钮组" >
< Button theme = "light" style = { { color : 'rgba(var(--semi-teal-7), 1)' } } onClick = { ( ) => { onOpenLink ( 'next' , record . key ) } } > 聊天 < / B u t t o n >
< Dropdown trigger = "click" position = "bottomRight" menu = {
[
2024-01-21 17:43:40 +08:00
{ node : 'item' , key : 'next' , disabled : ! localStorage . getItem ( 'chat_link' ) , name : 'ChatGPT Next Web' , onClick : ( ) => { onOpenLink ( 'next' , record . key ) } } ,
{ node : 'item' , key : 'next-mj' , disabled : ! localStorage . getItem ( 'chat_link2' ) , name : 'ChatGPT Web & Midjourney' , onClick : ( ) => { onOpenLink ( 'next-mj' , record . key ) } } ,
2024-03-01 22:39:42 +08:00
{ node : 'item' , key : 'ama' , name : 'AMA 问天( BotGem) ' , onClick : ( ) => { onOpenLink ( 'ama' , record . key ) } } ,
2023-12-11 20:11:26 +08:00
{ node : 'item' , key : 'opencat' , name : 'OpenCat' , onClick : ( ) => { onOpenLink ( 'opencat' , record . key ) } } ,
]
}
>
< Button style = { { padding : '8px 4px' , color : 'rgba(var(--semi-teal-7), 1)' } } type = "primary" icon = { < IconTreeTriangleDown / > } > < / B u t t o n >
< / D r o p d o w n >
< / S p l i t B u t t o n G r o u p >
2023-10-31 22:41:34 +08:00
< Popconfirm
title = "确定是否要删除此令牌?"
content = "此修改将不可逆"
okType = { 'danger' }
position = { 'left' }
onConfirm = { ( ) => {
manageToken ( record . id , 'delete' , record ) . then (
( ) => {
removeRecord ( record . key ) ;
}
)
} }
>
< Button theme = 'light' type = 'danger' style = { { marginRight : 1 } } > 删除 < / B u t t o n >
< / P o p c o n f i r m >
{
record . status === 1 ?
< Button theme = 'light' type = 'warning' style = { { marginRight : 1 } } onClick = {
async ( ) => {
manageToken (
record . id ,
'disable' ,
record
)
}
} > 禁用 < / B u t t o n > :
< Button theme = 'light' type = 'secondary' style = { { marginRight : 1 } } onClick = {
async ( ) => {
manageToken (
record . id ,
'enable' ,
record
) ;
}
} > 启用 < / B u t t o n >
}
2023-11-01 02:50:14 +08:00
< Button theme = 'light' type = 'tertiary' style = { { marginRight : 1 } } onClick = {
( ) => {
setEditingToken ( record ) ;
setShowEdit ( true ) ;
}
} > 编辑 < / B u t t o n >
2023-10-31 22:41:34 +08:00
< / d i v >
) ,
} ,
] ;
2023-11-03 22:47:55 +08:00
const [ pageSize , setPageSize ] = useState ( ITEMS _PER _PAGE ) ;
2023-11-01 02:50:14 +08:00
const [ showEdit , setShowEdit ] = useState ( false ) ;
2023-10-31 22:41:34 +08:00
const [ tokens , setTokens ] = useState ( [ ] ) ;
const [ selectedKeys , setSelectedKeys ] = useState ( [ ] ) ;
2023-11-03 22:47:55 +08:00
const [ tokenCount , setTokenCount ] = useState ( pageSize ) ;
2023-10-31 22:41:34 +08:00
const [ loading , setLoading ] = useState ( true ) ;
const [ activePage , setActivePage ] = useState ( 1 ) ;
const [ searchKeyword , setSearchKeyword ] = useState ( '' ) ;
2023-11-10 00:10:41 +08:00
const [ searchToken , setSearchToken ] = useState ( '' ) ;
2023-10-31 22:41:34 +08:00
const [ searching , setSearching ] = useState ( false ) ;
const [ showTopUpModal , setShowTopUpModal ] = useState ( false ) ;
const [ targetTokenIdx , setTargetTokenIdx ] = useState ( 0 ) ;
2023-11-01 02:50:14 +08:00
const [ editingToken , setEditingToken ] = useState ( {
id : undefined ,
} ) ;
const closeEdit = ( ) => {
setShowEdit ( false ) ;
2024-01-08 16:23:54 +08:00
setTimeout ( ( ) => {
setEditingToken ( {
id : undefined ,
} ) ;
} , 500 ) ;
2023-11-01 02:50:14 +08:00
}
2023-10-31 22:41:34 +08:00
const setTokensFormat = ( tokens ) => {
setTokens ( tokens ) ;
2023-11-03 22:47:55 +08:00
if ( tokens . length >= pageSize ) {
setTokenCount ( tokens . length + pageSize ) ;
2023-10-31 22:41:34 +08:00
} else {
setTokenCount ( tokens . length ) ;
}
2023-04-23 11:31:00 +08:00
}
2023-11-03 22:47:55 +08:00
let pageData = tokens . slice ( ( activePage - 1 ) * pageSize , activePage * pageSize ) ;
2023-10-31 22:41:34 +08:00
const loadTokens = async ( startIdx ) => {
setLoading ( true ) ;
2023-11-03 22:47:55 +08:00
const res = await API . get ( ` /api/token/?p= ${ startIdx } &size= ${ pageSize } ` ) ;
2023-10-31 22:41:34 +08:00
const { success , message , data } = res . data ;
if ( success ) {
if ( startIdx === 0 ) {
setTokensFormat ( data ) ;
} else {
let newTokens = [ ... tokens ] ;
2023-11-03 22:47:55 +08:00
newTokens . splice ( startIdx * pageSize , data . length , ... data ) ;
2023-10-31 22:41:34 +08:00
setTokensFormat ( newTokens ) ;
}
} else {
showError ( message ) ;
}
setLoading ( false ) ;
} ;
const onPaginationChange = ( e , { activePage } ) => {
( async ( ) => {
2023-11-03 22:47:55 +08:00
if ( activePage === Math . ceil ( tokens . length / pageSize ) + 1 ) {
2023-10-31 22:41:34 +08:00
// In this case we have to load more data and then append them.
await loadTokens ( activePage - 1 ) ;
}
setActivePage ( activePage ) ;
} ) ( ) ;
} ;
const refresh = async ( ) => {
2023-04-23 12:43:10 +08:00
await loadTokens ( activePage - 1 ) ;
2023-10-31 22:41:34 +08:00
} ;
2023-08-06 22:02:58 +08:00
2023-10-31 22:41:34 +08:00
const onCopy = async ( type , key ) => {
let status = localStorage . getItem ( 'status' ) ;
let serverAddress = '' ;
if ( status ) {
status = JSON . parse ( status ) ;
serverAddress = status . server _address ;
}
if ( serverAddress === '' ) {
serverAddress = window . location . origin ;
}
let encodedServerAddress = encodeURIComponent ( serverAddress ) ;
const nextLink = localStorage . getItem ( 'chat_link' ) ;
2024-01-16 18:15:55 +00:00
const mjLink = localStorage . getItem ( 'chat_link2' ) ;
2023-10-31 22:41:34 +08:00
let nextUrl ;
2023-08-25 00:51:02 +08:00
2023-10-31 22:41:34 +08:00
if ( nextLink ) {
nextUrl = nextLink + ` /#/?settings={"key":"sk- ${ key } ","url":" ${ serverAddress } "} ` ;
} else {
nextUrl = ` https://chat.oneapi.pro/#/?settings={"key":"sk- ${ key } ","url":" ${ serverAddress } "} ` ;
}
let url ;
switch ( type ) {
case 'ama' :
2024-01-16 18:15:55 +00:00
url = mjLink + ` /#/?settings={"key":"sk- ${ key } ","url":" ${ serverAddress } "} ` ;
2023-10-31 22:41:34 +08:00
break ;
case 'opencat' :
url = ` opencat://team/join?domain= ${ encodedServerAddress } &token=sk- ${ key } ` ;
break ;
case 'next' :
url = nextUrl ;
break ;
default :
url = ` sk- ${ key } ` ;
}
// if (await copy(url)) {
// showSuccess('已复制到剪贴板!');
// } else {
// showWarning('无法复制到剪贴板,请手动复制,已将令牌填入搜索框。');
// setSearchKeyword(url);
// }
} ;
const copyText = async ( text ) => {
if ( await copy ( text ) ) {
showSuccess ( '已复制到剪贴板!' ) ;
} else {
// setSearchKeyword(text);
Modal . error ( { title : '无法复制到剪贴板,请手动复制' , content : text } ) ;
}
2023-04-23 11:31:00 +08:00
}
2023-10-31 22:41:34 +08:00
const onOpenLink = async ( type , key ) => {
let status = localStorage . getItem ( 'status' ) ;
let serverAddress = '' ;
if ( status ) {
status = JSON . parse ( status ) ;
serverAddress = status . server _address ;
}
if ( serverAddress === '' ) {
serverAddress = window . location . origin ;
}
let encodedServerAddress = encodeURIComponent ( serverAddress ) ;
const chatLink = localStorage . getItem ( 'chat_link' ) ;
2024-01-16 18:50:01 +00:00
const mjLink = localStorage . getItem ( 'chat_link2' ) ;
2023-10-31 22:41:34 +08:00
let defaultUrl ;
if ( chatLink ) {
defaultUrl = chatLink + ` /#/?settings={"key":"sk- ${ key } ","url":" ${ serverAddress } "} ` ;
}
let url ;
switch ( type ) {
case 'ama' :
url = ` ama://set-api-key?server= ${ encodedServerAddress } &key=sk- ${ key } ` ;
break ;
case 'opencat' :
url = ` opencat://team/join?domain= ${ encodedServerAddress } &token=sk- ${ key } ` ;
break ;
2024-01-21 17:43:40 +08:00
case 'next-mj' :
url = mjLink + ` /#/?settings={"key":"sk- ${ key } ","url":" ${ serverAddress } "} ` ;
break ;
2023-10-31 22:41:34 +08:00
default :
2024-01-21 17:43:40 +08:00
if ( ! chatLink ) {
showError ( '管理员未设置聊天链接' )
return ;
}
2023-10-31 22:41:34 +08:00
url = defaultUrl ;
}
window . open ( url , '_blank' ) ;
2023-04-23 11:31:00 +08:00
}
2023-10-31 22:41:34 +08:00
useEffect ( ( ) => {
loadTokens ( 0 )
. then ( )
. catch ( ( reason ) => {
showError ( reason ) ;
} ) ;
2023-11-03 22:47:55 +08:00
} , [ pageSize ] ) ;
2023-10-31 22:41:34 +08:00
const removeRecord = key => {
let newDataSource = [ ... tokens ] ;
if ( key != null ) {
let idx = newDataSource . findIndex ( data => data . key === key ) ;
if ( idx > - 1 ) {
newDataSource . splice ( idx , 1 ) ;
setTokensFormat ( newDataSource ) ;
}
}
} ;
const manageToken = async ( id , action , record ) => {
setLoading ( true ) ;
let data = { id } ;
let res ;
switch ( action ) {
case 'delete' :
res = await API . delete ( ` /api/token/ ${ id } / ` ) ;
break ;
case 'enable' :
data . status = 1 ;
res = await API . put ( '/api/token/?status_only=true' , data ) ;
break ;
case 'disable' :
data . status = 2 ;
res = await API . put ( '/api/token/?status_only=true' , data ) ;
break ;
}
const { success , message } = res . data ;
if ( success ) {
showSuccess ( '操作成功完成!' ) ;
let token = res . data . data ;
let newTokens = [ ... tokens ] ;
// let realIdx = (activePage - 1) * ITEMS_PER_PAGE + idx;
if ( action === 'delete' ) {
} else {
record . status = token . status ;
// newTokens[realIdx].status = token.status;
}
setTokensFormat ( newTokens ) ;
} else {
showError ( message ) ;
}
setLoading ( false ) ;
} ;
const searchTokens = async ( ) => {
2023-11-10 00:46:17 +08:00
if ( searchKeyword === '' && searchToken === '' ) {
2023-10-31 22:41:34 +08:00
// if keyword is blank, load files instead.
await loadTokens ( 0 ) ;
setActivePage ( 1 ) ;
return ;
}
setSearching ( true ) ;
2023-11-10 00:46:17 +08:00
const res = await API . get ( ` /api/token/search?keyword= ${ searchKeyword } &token= ${ searchToken } ` ) ;
2023-10-31 22:41:34 +08:00
const { success , message , data } = res . data ;
if ( success ) {
setTokensFormat ( data ) ;
setActivePage ( 1 ) ;
} else {
showError ( message ) ;
}
setSearching ( false ) ;
} ;
const handleKeywordChange = async ( value ) => {
setSearchKeyword ( value . trim ( ) ) ;
} ;
2023-11-10 00:10:41 +08:00
const handleSearchTokenChange = async ( value ) => {
setSearchToken ( value . trim ( ) ) ;
} ;
2023-10-31 22:41:34 +08:00
const sortToken = ( key ) => {
if ( tokens . length === 0 ) return ;
setLoading ( true ) ;
let sortedTokens = [ ... tokens ] ;
sortedTokens . sort ( ( a , b ) => {
return ( '' + a [ key ] ) . localeCompare ( b [ key ] ) ;
} ) ;
if ( sortedTokens [ 0 ] . id === tokens [ 0 ] . id ) {
sortedTokens . reverse ( ) ;
}
setTokens ( sortedTokens ) ;
setLoading ( false ) ;
} ;
const handlePageChange = page => {
setActivePage ( page ) ;
2023-11-03 22:47:55 +08:00
if ( page === Math . ceil ( tokens . length / pageSize ) + 1 ) {
2023-10-31 22:41:34 +08:00
// In this case we have to load more data and then append them.
loadTokens ( page - 1 ) . then ( r => {
} ) ;
}
} ;
const rowSelection = {
onSelect : ( record , selected ) => {
} ,
onSelectAll : ( selected , selectedRows ) => {
} ,
onChange : ( selectedRowKeys , selectedRows ) => {
setSelectedKeys ( selectedRows ) ;
} ,
} ;
2023-12-01 01:30:24 +08:00
const handleRow = ( record , index ) => {
if ( record . status !== 1 ) {
return {
style : {
background : 'var(--semi-color-disabled-border)' ,
} ,
} ;
} else {
return { } ;
}
} ;
2023-10-31 22:41:34 +08:00
return (
< >
2023-11-01 02:50:14 +08:00
< EditToken refresh = { refresh } editingToken = { editingToken } visiable = { showEdit } handleClose = { closeEdit } > < / E d i t T o k e n >
2023-10-31 22:41:34 +08:00
< Form layout = 'horizontal' style = { { marginTop : 10 } } labelPosition = { 'left' } >
< Form . Input
field = "keyword"
label = '搜索关键字'
placeholder = '令牌名称'
value = { searchKeyword }
loading = { searching }
onChange = { handleKeywordChange }
/ >
2023-11-10 00:10:41 +08:00
< Form . Input
field = "token"
label = 'Key'
placeholder = '密钥'
value = { searchToken }
loading = { searching }
onChange = { handleSearchTokenChange }
/ >
2023-10-31 22:41:34 +08:00
< Button label = '查询' type = "primary" htmlType = "submit" className = "btn-margin-right"
onClick = { searchTokens } style = { { marginRight : 8 } } > 查询 < / B u t t o n >
< / F o r m >
2023-11-03 22:38:17 +08:00
< Table style = { { marginTop : 20 } } columns = { columns } dataSource = { pageData } pagination = { {
2023-10-31 22:41:34 +08:00
currentPage : activePage ,
2023-11-03 22:47:55 +08:00
pageSize : pageSize ,
2023-10-31 22:41:34 +08:00
total : tokenCount ,
2023-11-03 22:38:17 +08:00
showSizeChanger : true ,
pageSizeOptions : [ 10 , 20 , 50 , 100 ] ,
2023-11-07 23:32:43 +08:00
formatPageText : ( page ) => ` 第 ${ page . currentStart } - ${ page . currentEnd } 条,共 ${ tokens . length } 条 ` ,
2023-11-03 22:47:55 +08:00
onPageSizeChange : ( size ) => {
setPageSize ( size ) ;
setActivePage ( 1 ) ;
} ,
2023-10-31 22:41:34 +08:00
onPageChange : handlePageChange ,
2023-12-01 01:30:24 +08:00
} } loading = { loading } rowSelection = { rowSelection } onRow = { handleRow } >
2023-10-31 22:41:34 +08:00
< / T a b l e >
2023-11-01 02:50:14 +08:00
< Button theme = 'light' type = 'primary' style = { { marginRight : 8 } } onClick = {
( ) => {
setEditingToken ( {
id : undefined ,
} ) ;
setShowEdit ( true ) ;
}
} > 添加令牌 < / B u t t o n >
2023-10-31 22:41:34 +08:00
< Button label = '复制所选令牌' type = "warning" onClick = {
async ( ) => {
2023-11-01 02:50:14 +08:00
if ( selectedKeys . length === 0 ) {
showError ( '请至少选择一个令牌!' ) ;
return ;
}
2023-10-31 22:41:34 +08:00
let keys = "" ;
for ( let i = 0 ; i < selectedKeys . length ; i ++ ) {
keys += selectedKeys [ i ] . name + " sk-" + selectedKeys [ i ] . key + "\n" ;
}
await copyText ( keys ) ;
2023-04-23 11:31:00 +08:00
}
2023-10-31 22:41:34 +08:00
} > 复制所选令牌到剪贴板 < / B u t t o n >
< / >
) ;
2023-04-23 11:31:00 +08:00
} ;
export default TokensTable ;