Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
phsl
/
new-api
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit
607358c9
authored
Oct 31, 2023
by
CaIon
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
完善令牌界面
parent
4a4ce233
Hide whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
478 additions
and
476 deletions
+478
-476
README.md
+12
-0
web/public/index.html
+2
-3
web/src/components/LogsTable.js
+5
-15
web/src/components/SiderBar.js
+3
-3
web/src/components/TokensTable.js
+434
-438
web/src/helpers/utils.js
+12
-11
web/src/pages/Token/index.js
+10
-6
No files found.
README.md
View file @
607358c9
# Neko API
# Neko API
> **Note**
> 本项目为开源项目,在[One API](https://github.com/songquanpeng/one-api)的基础上进行二次开发,感谢原作者的无私奉献。
> 使用者必须在遵循 OpenAI 的[使用条款](https://openai.com/policies/terms-of-use)以及**法律法规**的情况下使用,不得用于非法用途。
> **Warning**
> 本项目为个人学习使用,不保证稳定性,且不提供任何技术支持,使用者必须在遵循 OpenAI 的使用条款以及法律法规的情况下使用,不得用于非法用途。
> 根据[《生成式人工智能服务管理暂行办法》](http://www.cac.gov.cn/2023-07/13/c_1690898327029107.htm)的要求,请勿对中国地区公众提供一切未经备案的生成式人工智能服务。
> **Note**
> 最新版Docker镜像 calciumion/neko-api:main
\ No newline at end of file
web/public/index.html
View file @
607358c9
...
@@ -6,10 +6,9 @@
...
@@ -6,10 +6,9 @@
<meta
name=
"viewport"
content=
"width=device-width, initial-scale=1"
/>
<meta
name=
"viewport"
content=
"width=device-width, initial-scale=1"
/>
<meta
name=
"theme-color"
content=
"#ffffff"
/>
<meta
name=
"theme-color"
content=
"#ffffff"
/>
<meta
name=
"description"
<meta
name=
"description"
content=
"Neko API,企业级AI接口调用平台,专为企业级需求打造,提供高性能、高并发、高可用的服务,一站式处理大规模数据和复杂任务。我们的稳定高并发处理能力和高可用性保证您的业务流畅运行,结合OpenAI、Claude、Midjourney等AI接口和专业的技术支持,为您的企业快速部署和实现AI接口应用,释放商业价值"
/>
content=
"NekoAPI,企业级AI接口调用平台,专为企业级需求打造,提供高性能、高并发、高可用的服务,一站式处理大规模数据和复杂任务。我们的稳定高并发处理能力和高可用性保证您的业务流畅运行,结合OpenAI、Claude、Midjourney等AI接口和专业的技术支持,为您的企业快速部署和实现AI接口应用,释放商业价值"
/>
<title>
Neko API
</title>
<meta
name=
"keywords"
content=
"Neko API,OpenAI,Claude,Midjourney,高并发,高可用,高性能,企业级AI接口调用平台"
/>
<meta
name=
"keywords"
content=
"Neko API,OpenAI,Claude,Midjourney,高并发,高可用,高性能,企业级AI接口调用平台"
/>
<title>
Neko
API
</title>
<title>
NekoAPI
</title>
</head>
</head>
<body>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
...
...
web/src/components/LogsTable.js
View file @
607358c9
...
@@ -20,7 +20,7 @@ import {
...
@@ -20,7 +20,7 @@ import {
}
from
'@douyinfe/semi-icons'
;
}
from
'@douyinfe/semi-icons'
;
const
{
Sider
,
Content
,
Header
}
=
Layout
;
const
{
Sider
,
Content
,
Header
}
=
Layout
;
const
{
Column
}
=
Table
;
const
{
Column
}
=
Table
;
function
renderTimestamp
(
timestamp
)
{
function
renderTimestamp
(
timestamp
)
{
...
@@ -65,13 +65,13 @@ const LogsTable = () => {
...
@@ -65,13 +65,13 @@ const LogsTable = () => {
{
{
title
:
'渠道'
,
title
:
'渠道'
,
dataIndex
:
'channel'
,
dataIndex
:
'channel'
,
className
:
isAdmin
()
?
'tableShow'
:
'tableHiddle'
,
className
:
isAdmin
()
?
'tableShow'
:
'tableHiddle'
,
render
:
(
text
,
record
,
index
)
=>
{
render
:
(
text
,
record
,
index
)
=>
{
return
(
return
(
isAdminUser
?
isAdminUser
?
record
.
type
===
0
||
record
.
type
===
2
?
record
.
type
===
0
||
record
.
type
===
2
?
<
div
>
<
div
>
{
<
Tag
color
=
{
colors
[
parseInt
(
text
)
%
10
]
}
size
=
'large'
>
{
text
}
<
/Tag>
}
{
<
Tag
color
=
{
stringToColor
(
text
)
}
size
=
'large'
>
{
text
}
<
/Tag>
}
<
/div
>
<
/div
>
:
:
<><
/
>
<><
/
>
...
@@ -83,7 +83,7 @@ const LogsTable = () => {
...
@@ -83,7 +83,7 @@ const LogsTable = () => {
{
{
title
:
'用户'
,
title
:
'用户'
,
dataIndex
:
'username'
,
dataIndex
:
'username'
,
className
:
isAdmin
()
?
'tableShow'
:
'tableHiddle'
,
className
:
isAdmin
()
?
'tableShow'
:
'tableHiddle'
,
render
:
(
text
,
record
,
index
)
=>
{
render
:
(
text
,
record
,
index
)
=>
{
return
(
return
(
isAdminUser
?
isAdminUser
?
...
@@ -291,15 +291,6 @@ const LogsTable = () => {
...
@@ -291,15 +291,6 @@ const LogsTable = () => {
const
pageData
=
logs
.
slice
((
activePage
-
1
)
*
ITEMS_PER_PAGE
,
activePage
*
ITEMS_PER_PAGE
);
const
pageData
=
logs
.
slice
((
activePage
-
1
)
*
ITEMS_PER_PAGE
,
activePage
*
ITEMS_PER_PAGE
);
// const onPaginationChange = (e, { activePage }) => {
// (async () => {
// if (activePage === Math.ceil(logs.length / ITEMS_PER_PAGE) + 1) {
// // In this case we have to load more data and then append them.
// await loadLogs(activePage - 1);
// }
// setActivePage(activePage);
// })();
// };
const
handlePageChange
=
page
=>
{
const
handlePageChange
=
page
=>
{
setActivePage
(
page
);
setActivePage
(
page
);
if
(
page
===
Math
.
ceil
(
logs
.
length
/
ITEMS_PER_PAGE
)
+
1
)
{
if
(
page
===
Math
.
ceil
(
logs
.
length
/
ITEMS_PER_PAGE
)
+
1
)
{
...
@@ -307,7 +298,6 @@ const LogsTable = () => {
...
@@ -307,7 +298,6 @@ const LogsTable = () => {
loadLogs
(
page
-
1
).
then
(
r
=>
{
loadLogs
(
page
-
1
).
then
(
r
=>
{
});
});
}
}
// setLoading(false);
};
};
const
refresh
=
async
()
=>
{
const
refresh
=
async
()
=>
{
...
@@ -405,7 +395,7 @@ const LogsTable = () => {
...
@@ -405,7 +395,7 @@ const LogsTable = () => {
<
/
>
<
/
>
}
}
<
Form
.
Section
>
<
Form
.
Section
>
<
Button
label
=
'
操作
'
type
=
"primary"
htmlType
=
"submit"
className
=
"btn-margin-right"
<
Button
label
=
'
查询
'
type
=
"primary"
htmlType
=
"submit"
className
=
"btn-margin-right"
onClick
=
{
refresh
}
>
查询
<
/Button
>
onClick
=
{
refresh
}
>
查询
<
/Button
>
<
/Form.Section
>
<
/Form.Section
>
<
/
>
<
/
>
...
...
web/src/components/SiderBar.js
View file @
607358c9
...
@@ -34,7 +34,7 @@ let headerButtons = [
...
@@ -34,7 +34,7 @@ let headerButtons = [
itemKey
:
'channel'
,
itemKey
:
'channel'
,
to
:
'/channel'
,
to
:
'/channel'
,
icon
:
<
IconLayers
/>
,
icon
:
<
IconLayers
/>
,
admin
:
true
className
:
isAdmin
()?
'semi-navigation-item-normal'
:
'tableHiddle'
,
},
},
{
{
...
@@ -48,7 +48,7 @@ let headerButtons = [
...
@@ -48,7 +48,7 @@ let headerButtons = [
itemKey
:
'redemption'
,
itemKey
:
'redemption'
,
to
:
'/redemption'
,
to
:
'/redemption'
,
icon
:
<
IconGift
/>
,
icon
:
<
IconGift
/>
,
admin
:
true
className
:
isAdmin
()?
'semi-navigation-item-normal'
:
'tableHiddle'
,
},
},
{
{
text
:
'钱包'
,
text
:
'钱包'
,
...
@@ -61,7 +61,7 @@ let headerButtons = [
...
@@ -61,7 +61,7 @@ let headerButtons = [
itemKey
:
'user'
,
itemKey
:
'user'
,
to
:
'/user'
,
to
:
'/user'
,
icon
:
<
IconUser
/>
,
icon
:
<
IconUser
/>
,
admin
:
true
className
:
isAdmin
()?
'semi-navigation-item-normal'
:
'tableHiddle'
,
},
},
{
{
text
:
'日志'
,
text
:
'日志'
,
...
...
web/src/components/TokensTable.js
View file @
607358c9
import
React
,
{
useEffect
,
useState
}
from
'react'
;
import
React
,
{
useEffect
,
useState
}
from
'react'
;
import
{
Button
,
Dropdown
,
Form
,
Label
,
Pagination
,
Popup
,
Table
}
from
'semantic-ui-react'
;
import
{
Link
}
from
'react-router-dom'
;
import
{
Link
}
from
'react-router-dom'
;
import
{
API
,
copy
,
isAdmin
,
showError
,
showSuccess
,
showWarning
,
timestamp2string
}
from
'../helpers'
;
import
{
API
,
copy
,
showError
,
showSuccess
,
showWarning
,
timestamp2string
}
from
'../helpers'
;
import
{
ITEMS_PER_PAGE
}
from
'../constants'
;
import
{
ITEMS_PER_PAGE
}
from
'../constants'
;
import
{
renderQuota
}
from
'../helpers/render'
;
import
{
renderQuota
,
stringToColor
}
from
'../helpers/render'
;
import
{
Avatar
,
Tag
,
Table
,
Button
,
Popover
,
Form
,
Modal
,
Popconfirm
}
from
"@douyinfe/semi-ui"
;
const
{
Column
}
=
Table
;
const
COPY_OPTIONS
=
[
const
COPY_OPTIONS
=
[
{
key
:
'next'
,
text
:
'ChatGPT Next Web'
,
value
:
'next'
},
{
key
:
'next'
,
text
:
'ChatGPT Next Web'
,
value
:
'next'
},
{
key
:
'ama'
,
text
:
'AMA 问天'
,
value
:
'ama'
},
{
key
:
'ama'
,
text
:
'AMA 问天'
,
value
:
'ama'
},
{
key
:
'opencat'
,
text
:
'OpenCat'
,
value
:
'opencat'
},
{
key
:
'opencat'
,
text
:
'OpenCat'
,
value
:
'opencat'
},
];
];
const
OPEN_LINK_OPTIONS
=
[
const
OPEN_LINK_OPTIONS
=
[
{
key
:
'ama'
,
text
:
'AMA 问天'
,
value
:
'ama'
},
{
key
:
'ama'
,
text
:
'AMA 问天'
,
value
:
'ama'
},
{
key
:
'opencat'
,
text
:
'OpenCat'
,
value
:
'opencat'
},
{
key
:
'opencat'
,
text
:
'OpenCat'
,
value
:
'opencat'
},
];
];
function
renderTimestamp
(
timestamp
)
{
function
renderTimestamp
(
timestamp
)
{
return
(
return
(
<>
<>
{
timestamp2string
(
timestamp
)}
{
timestamp2string
(
timestamp
)}
<
/
>
<
/
>
);
);
}
}
function
renderStatus
(
status
)
{
function
renderStatus
(
status
)
{
switch
(
status
)
{
switch
(
status
)
{
case
1
:
case
1
:
return
<
Label
basic
color
=
'green'
>
已启用
<
/Label
>
;
return
<
Tag
color
=
'green'
size
=
'large'
>
已启用
<
/Tag
>
;
case
2
:
case
2
:
return
<
Label
basic
color
=
'red'
>
已禁用
<
/Label
>
;
return
<
Tag
color
=
'red'
size
=
'large'
>
已禁用
<
/Tag
>
;
case
3
:
case
3
:
return
<
Label
basic
color
=
'yellow'
>
已过期
<
/Label
>
;
return
<
Tag
color
=
'yellow'
size
=
'large'
>
已过期
<
/Tag
>
;
case
4
:
case
4
:
return
<
Label
basic
color
=
'grey'
>
已耗尽
<
/Label
>
;
return
<
Tag
color
=
'grey'
size
=
'large'
>
已耗尽
<
/Tag
>
;
default
:
default
:
return
<
Label
basic
color
=
'black'
>
未知状态
<
/Label
>
;
return
<
Tag
color
=
'black'
size
=
'large'
>
未知状态
<
/Tag
>
;
}
}
}
}
const
TokensTable
=
()
=>
{
const
TokensTable
=
()
=>
{
const
[
tokens
,
setTokens
]
=
useState
([]);
const
columns
=
[
const
[
loading
,
setLoading
]
=
useState
(
true
);
{
const
[
activePage
,
setActivePage
]
=
useState
(
1
);
title
:
'名称'
,
const
[
searchKeyword
,
setSearchKeyword
]
=
useState
(
''
);
dataIndex
:
'name'
,
const
[
searching
,
setSearching
]
=
useState
(
false
);
},
const
[
showTopUpModal
,
setShowTopUpModal
]
=
useState
(
false
);
{
const
[
targetTokenIdx
,
setTargetTokenIdx
]
=
useState
(
0
);
title
:
'状态'
,
dataIndex
:
'status'
,
const
loadTokens
=
async
(
startIdx
)
=>
{
key
:
'status'
,
const
res
=
await
API
.
get
(
`/api/token/?p=
${
startIdx
}
`
);
render
:
(
text
,
record
,
index
)
=>
{
const
{
success
,
message
,
data
}
=
res
.
data
;
return
(
if
(
success
)
{
<
div
>
if
(
startIdx
===
0
)
{
{
renderStatus
(
text
)}
setTokens
(
data
);
<
/div
>
}
else
{
);
let
newTokens
=
[...
tokens
];
},
newTokens
.
splice
(
startIdx
*
ITEMS_PER_PAGE
,
data
.
length
,
...
data
);
},
setTokens
(
newTokens
);
{
}
title
:
'已用额度'
,
}
else
{
dataIndex
:
'used_quota'
,
showError
(
message
);
render
:
(
text
,
record
,
index
)
=>
{
return
(
<
div
>
{
renderQuota
(
parseInt
(
text
))}
<
/div
>
);
},
},
{
title
:
'剩余额度'
,
dataIndex
:
'remain_quota'
,
render
:
(
text
,
record
,
index
)
=>
{
return
(
<
div
>
{
renderQuota
(
parseInt
(
text
))}
<
/div
>
);
},
},
{
title
:
'创建时间'
,
dataIndex
:
'created_time'
,
render
:
(
text
,
record
,
index
)
=>
{
return
(
<
div
>
{
renderTimestamp
(
text
)}
<
/div
>
);
},
},
{
title
:
'过期时间'
,
dataIndex
:
'accessed_time'
,
render
:
(
text
,
record
,
index
)
=>
{
return
(
<
div
>
{
renderTimestamp
(
text
)}
<
/div
>
);
},
},
{
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
}}
>
查看
<
/Button
>
<
/Popover
>
<
Button
theme
=
'light'
type
=
'secondary'
style
=
{{
marginRight
:
1
}}
onClick
=
{
async
(
text
)
=>
{
await
copyText
(
'sk-'
+
record
.
key
)
}}
>
复制
<
/Button
>
<
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
}}
>
删除
<
/Button
>
<
/Popconfirm
>
{
record
.
status
===
1
?
<
Button
theme
=
'light'
type
=
'warning'
style
=
{{
marginRight
:
1
}}
onClick
=
{
async
()
=>
{
manageToken
(
record
.
id
,
'disable'
,
record
)
}
}
>
禁用
<
/Button>
:
<
Button
theme
=
'light'
type
=
'secondary'
style
=
{{
marginRight
:
1
}}
onClick
=
{
async
()
=>
{
manageToken
(
record
.
id
,
'enable'
,
record
);
}
}
>
启用
<
/Button
>
}
<
Button
theme
=
'light'
type
=
'tertiary'
style
=
{{
marginRight
:
1
}}
>
编辑
<
/Button
>
<
/div
>
),
},
];
const
[
tokens
,
setTokens
]
=
useState
([]);
const
[
selectedKeys
,
setSelectedKeys
]
=
useState
([]);
const
[
tokenCount
,
setTokenCount
]
=
useState
(
ITEMS_PER_PAGE
);
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
activePage
,
setActivePage
]
=
useState
(
1
);
const
[
searchKeyword
,
setSearchKeyword
]
=
useState
(
''
);
const
[
searching
,
setSearching
]
=
useState
(
false
);
const
[
showTopUpModal
,
setShowTopUpModal
]
=
useState
(
false
);
const
[
targetTokenIdx
,
setTargetTokenIdx
]
=
useState
(
0
);
const
setTokensFormat
=
(
tokens
)
=>
{
setTokens
(
tokens
);
if
(
tokens
.
length
===
ITEMS_PER_PAGE
)
{
setTokenCount
(
tokens
.
length
+
ITEMS_PER_PAGE
);
}
else
{
setTokenCount
(
tokens
.
length
);
}
}
}
setLoading
(
false
);
};
const
onPaginationChange
=
(
e
,
{
activePage
})
=>
{
// let pageData = tokens.slice((activePage - 1) * ITEMS_PER_PAGE, activePage * ITEMS_PER_PAGE);
(
async
()
=>
{
const
loadTokens
=
async
(
startIdx
)
=>
{
if
(
activePage
===
Math
.
ceil
(
tokens
.
length
/
ITEMS_PER_PAGE
)
+
1
)
{
setLoading
(
true
);
// In this case we have to load more data and then append them.
const
res
=
await
API
.
get
(
`/api/token/?p=
${
startIdx
}
`
);
const
{
success
,
message
,
data
}
=
res
.
data
;
if
(
success
)
{
if
(
startIdx
===
0
)
{
setTokensFormat
(
data
);
}
else
{
let
newTokens
=
[...
tokens
];
newTokens
.
splice
(
startIdx
*
ITEMS_PER_PAGE
,
data
.
length
,
...
data
);
setTokensFormat
(
newTokens
);
}
}
else
{
showError
(
message
);
}
setLoading
(
false
);
};
const
onPaginationChange
=
(
e
,
{
activePage
})
=>
{
(
async
()
=>
{
if
(
activePage
===
Math
.
ceil
(
tokens
.
length
/
ITEMS_PER_PAGE
)
+
1
)
{
// In this case we have to load more data and then append them.
await
loadTokens
(
activePage
-
1
);
}
setActivePage
(
activePage
);
})();
};
const
refresh
=
async
()
=>
{
await
loadTokens
(
activePage
-
1
);
await
loadTokens
(
activePage
-
1
);
}
};
setActivePage
(
activePage
);
})();
};
const
refresh
=
async
()
=>
{
setLoading
(
true
);
await
loadTokens
(
activePage
-
1
);
};
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'
);
let
nextUrl
;
if
(
nextLink
)
{
nextUrl
=
nextLink
+
`/#/?settings={"key":"sk-
${
key
}
","url":"
${
serverAddress
}
"}`
;
}
else
{
nextUrl
=
`https://chat.oneapi.pro/#/?settings={"key":"sk-
${
key
}
","url":"
${
serverAddress
}
"}`
;
}
let
url
;
const
onCopy
=
async
(
type
,
key
)
=>
{
switch
(
type
)
{
let
status
=
localStorage
.
getItem
(
'status'
);
case
'ama'
:
let
serverAddress
=
''
;
url
=
`ama://set-api-key?server=
${
encodedServerAddress
}
&key=sk-
${
key
}
`
;
if
(
status
)
{
break
;
status
=
JSON
.
parse
(
status
);
case
'opencat'
:
serverAddress
=
status
.
server_address
;
url
=
`opencat://team/join?domain=
${
encodedServerAddress
}
&token=sk-
${
key
}
`
;
}
break
;
if
(
serverAddress
===
''
)
{
case
'next'
:
serverAddress
=
window
.
location
.
origin
;
url
=
nextUrl
;
}
break
;
let
encodedServerAddress
=
encodeURIComponent
(
serverAddress
);
default
:
const
nextLink
=
localStorage
.
getItem
(
'chat_link'
);
url
=
`sk-
${
key
}
`
;
let
nextUrl
;
}
if
(
await
copy
(
url
))
{
showSuccess
(
'已复制到剪贴板!'
);
}
else
{
showWarning
(
'无法复制到剪贴板,请手动复制,已将令牌填入搜索框。'
);
setSearchKeyword
(
url
);
}
};
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'
);
let
defaultUrl
;
if
(
chatLink
)
{
defaultUrl
=
chatLink
+
`/#/?settings={"key":"sk-
${
key
}
","url":"
${
serverAddress
}
"}`
;
}
else
{
defaultUrl
=
`https://chat.oneapi.pro/#/?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
;
default
:
url
=
defaultUrl
;
}
window
.
open
(
url
,
'_blank'
);
if
(
nextLink
)
{
}
nextUrl
=
nextLink
+
`/#/?settings={"key":"sk-
${
key
}
","url":"
${
serverAddress
}
"}`
;
}
else
{
useEffect
(()
=>
{
nextUrl
=
`https://chat.oneapi.pro/#/?settings={"key":"sk-
${
key
}
","url":"
${
serverAddress
}
"}`
;
loadTokens
(
0
)
}
.
then
()
.
catch
((
reason
)
=>
{
let
url
;
showError
(
reason
);
switch
(
type
)
{
});
case
'ama'
:
},
[]);
url
=
`ama://set-api-key?server=
${
encodedServerAddress
}
&key=sk-
${
key
}
`
;
break
;
const
manageToken
=
async
(
id
,
action
,
idx
)
=>
{
case
'opencat'
:
let
data
=
{
id
};
url
=
`opencat://team/join?domain=
${
encodedServerAddress
}
&token=sk-
${
key
}
`
;
let
res
;
break
;
switch
(
action
)
{
case
'next'
:
case
'delete'
:
url
=
nextUrl
;
res
=
await
API
.
delete
(
`/api/token/
${
id
}
/`
);
break
;
break
;
default
:
case
'enable'
:
url
=
`sk-
${
key
}
`
;
data
.
status
=
1
;
}
res
=
await
API
.
put
(
'/api/token/?status_only=true'
,
data
);
// if (await copy(url)) {
break
;
// showSuccess('已复制到剪贴板!');
case
'disable'
:
// } else {
data
.
status
=
2
;
// showWarning('无法复制到剪贴板,请手动复制,已将令牌填入搜索框。');
res
=
await
API
.
put
(
'/api/token/?status_only=true'
,
data
);
// setSearchKeyword(url);
break
;
// }
}
};
const
{
success
,
message
}
=
res
.
data
;
if
(
success
)
{
const
copyText
=
async
(
text
)
=>
{
showSuccess
(
'操作成功完成!'
);
if
(
await
copy
(
text
))
{
let
token
=
res
.
data
.
data
;
showSuccess
(
'已复制到剪贴板!'
);
let
newTokens
=
[...
tokens
];
}
else
{
let
realIdx
=
(
activePage
-
1
)
*
ITEMS_PER_PAGE
+
idx
;
// setSearchKeyword(text);
if
(
action
===
'delete'
)
{
Modal
.
error
({
title
:
'无法复制到剪贴板,请手动复制'
,
content
:
text
});
newTokens
[
realIdx
].
deleted
=
true
;
}
}
else
{
newTokens
[
realIdx
].
status
=
token
.
status
;
}
setTokens
(
newTokens
);
}
else
{
showError
(
message
);
}
};
const
searchTokens
=
async
()
=>
{
if
(
searchKeyword
===
''
)
{
// if keyword is blank, load files instead.
await
loadTokens
(
0
);
setActivePage
(
1
);
return
;
}
setSearching
(
true
);
const
res
=
await
API
.
get
(
`/api/token/search?keyword=
${
searchKeyword
}
`
);
const
{
success
,
message
,
data
}
=
res
.
data
;
if
(
success
)
{
setTokens
(
data
);
setActivePage
(
1
);
}
else
{
showError
(
message
);
}
}
setSearching
(
false
);
};
const
onOpenLink
=
async
(
type
,
key
)
=>
{
let
status
=
localStorage
.
getItem
(
'status'
);
const
handleKeywordChange
=
async
(
e
,
{
value
})
=>
{
let
serverAddress
=
''
;
setSearchKeyword
(
value
.
trim
());
if
(
status
)
{
};
status
=
JSON
.
parse
(
status
);
serverAddress
=
status
.
server_address
;
const
sortToken
=
(
key
)
=>
{
}
if
(
tokens
.
length
===
0
)
return
;
if
(
serverAddress
===
''
)
{
setLoading
(
true
);
serverAddress
=
window
.
location
.
origin
;
let
sortedTokens
=
[...
tokens
];
}
sortedTokens
.
sort
((
a
,
b
)
=>
{
let
encodedServerAddress
=
encodeURIComponent
(
serverAddress
);
return
(
''
+
a
[
key
]).
localeCompare
(
b
[
key
]);
const
chatLink
=
localStorage
.
getItem
(
'chat_link'
);
});
let
defaultUrl
;
if
(
sortedTokens
[
0
].
id
===
tokens
[
0
].
id
)
{
sortedTokens
.
reverse
();
if
(
chatLink
)
{
defaultUrl
=
chatLink
+
`/#/?settings={"key":"sk-
${
key
}
","url":"
${
serverAddress
}
"}`
;
}
else
{
defaultUrl
=
`https://chat.oneapi.pro/#/?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
;
default
:
url
=
defaultUrl
;
}
window
.
open
(
url
,
'_blank'
);
}
}
setTokens
(
sortedTokens
);
setLoading
(
false
);
useEffect
(()
=>
{
};
loadTokens
(
0
)
.
then
()
return
(
.
catch
((
reason
)
=>
{
<>
showError
(
reason
);
<
Form
onSubmit
=
{
searchTokens
}
>
});
<
Form
.
Input
},
[]);
icon
=
'search'
fluid
const
removeRecord
=
key
=>
{
iconPosition
=
'left'
let
newDataSource
=
[...
tokens
];
placeholder
=
'搜索令牌的名称 ...'
if
(
key
!=
null
)
{
value
=
{
searchKeyword
}
let
idx
=
newDataSource
.
findIndex
(
data
=>
data
.
key
===
key
);
loading
=
{
searching
}
onChange
=
{
handleKeywordChange
}
if
(
idx
>
-
1
)
{
/
>
newDataSource
.
splice
(
idx
,
1
);
<
/Form
>
setTokensFormat
(
newDataSource
);
}
<
Table
basic
compact
size
=
'small'
>
}
<
Table
.
Header
>
};
<
Table
.
Row
>
<
Table
.
HeaderCell
const
manageToken
=
async
(
id
,
action
,
record
)
=>
{
style
=
{{
cursor
:
'pointer'
}}
setLoading
(
true
);
onClick
=
{()
=>
{
let
data
=
{
id
};
sortToken
(
'name'
);
let
res
;
}}
switch
(
action
)
{
>
case
'delete'
:
名称
res
=
await
API
.
delete
(
`/api/token/
${
id
}
/`
);
<
/Table.HeaderCell
>
break
;
<
Table
.
HeaderCell
case
'enable'
:
style
=
{{
cursor
:
'pointer'
}}
data
.
status
=
1
;
onClick
=
{()
=>
{
res
=
await
API
.
put
(
'/api/token/?status_only=true'
,
data
);
sortToken
(
'status'
);
break
;
}}
case
'disable'
:
>
data
.
status
=
2
;
状态
res
=
await
API
.
put
(
'/api/token/?status_only=true'
,
data
);
<
/Table.HeaderCell
>
break
;
<
Table
.
HeaderCell
}
style
=
{{
cursor
:
'pointer'
}}
const
{
success
,
message
}
=
res
.
data
;
onClick
=
{()
=>
{
if
(
success
)
{
sortToken
(
'used_quota'
);
showSuccess
(
'操作成功完成!'
);
}}
let
token
=
res
.
data
.
data
;
>
let
newTokens
=
[...
tokens
];
已用额度
// let realIdx = (activePage - 1) * ITEMS_PER_PAGE + idx;
<
/Table.HeaderCell
>
if
(
action
===
'delete'
)
{
<
Table
.
HeaderCell
style
=
{{
cursor
:
'pointer'
}}
}
else
{
onClick
=
{()
=>
{
record
.
status
=
token
.
status
;
sortToken
(
'remain_quota'
);
// newTokens[realIdx].status = token.status;
}}
}
>
setTokensFormat
(
newTokens
);
剩余额度
}
else
{
<
/Table.HeaderCell
>
showError
(
message
);
<
Table
.
HeaderCell
}
style
=
{{
cursor
:
'pointer'
}}
setLoading
(
false
);
onClick
=
{()
=>
{
};
sortToken
(
'created_time'
);
}}
const
searchTokens
=
async
()
=>
{
>
if
(
searchKeyword
===
''
)
{
创建时间
// if keyword is blank, load files instead.
<
/Table.HeaderCell
>
await
loadTokens
(
0
);
<
Table
.
HeaderCell
setActivePage
(
1
);
style
=
{{
cursor
:
'pointer'
}}
return
;
onClick
=
{()
=>
{
}
sortToken
(
'expired_time'
);
setSearching
(
true
);
}}
const
res
=
await
API
.
get
(
`/api/token/search?keyword=
${
searchKeyword
}
`
);
>
const
{
success
,
message
,
data
}
=
res
.
data
;
过期时间
if
(
success
)
{
<
/Table.HeaderCell
>
setTokensFormat
(
data
);
<
Table
.
HeaderCell
>
操作
<
/Table.HeaderCell
>
setActivePage
(
1
);
<
/Table.Row
>
}
else
{
<
/Table.Header
>
showError
(
message
);
}
<
Table
.
Body
>
setSearching
(
false
);
{
tokens
};
.
slice
(
(
activePage
-
1
)
*
ITEMS_PER_PAGE
,
const
handleKeywordChange
=
async
(
value
)
=>
{
activePage
*
ITEMS_PER_PAGE
setSearchKeyword
(
value
.
trim
());
)
};
.
map
((
token
,
idx
)
=>
{
if
(
token
.
deleted
)
return
<><
/>
;
const
sortToken
=
(
key
)
=>
{
return
(
if
(
tokens
.
length
===
0
)
return
;
<
Table
.
Row
key
=
{
token
.
id
}
>
setLoading
(
true
);
<
Table
.
Cell
>
{
token
.
name
?
token
.
name
:
'无'
}
<
/Table.Cell
>
let
sortedTokens
=
[...
tokens
];
<
Table
.
Cell
>
{
renderStatus
(
token
.
status
)}
<
/Table.Cell
>
sortedTokens
.
sort
((
a
,
b
)
=>
{
<
Table
.
Cell
>
{
renderQuota
(
token
.
used_quota
)}
<
/Table.Cell
>
return
(
''
+
a
[
key
]).
localeCompare
(
b
[
key
]);
<
Table
.
Cell
>
{
token
.
unlimited_quota
?
'无限制'
:
renderQuota
(
token
.
remain_quota
,
2
)}
<
/Table.Cell
>
});
<
Table
.
Cell
>
{
renderTimestamp
(
token
.
created_time
)}
<
/Table.Cell
>
if
(
sortedTokens
[
0
].
id
===
tokens
[
0
].
id
)
{
<
Table
.
Cell
>
{
token
.
expired_time
===
-
1
?
'永不过期'
:
renderTimestamp
(
token
.
expired_time
)}
<
/Table.Cell
>
sortedTokens
.
reverse
();
<
Table
.
Cell
>
}
<
div
>
setTokens
(
sortedTokens
);
<
Button
.
Group
color
=
'green'
size
=
{
'small'
}
>
setLoading
(
false
);
<
Button
};
size
=
{
'small'
}
positive
onClick
=
{
async
()
=>
{
const
handlePageChange
=
page
=>
{
await
onCopy
(
''
,
token
.
key
);
setActivePage
(
page
);
}}
if
(
page
===
Math
.
ceil
(
tokens
.
length
/
ITEMS_PER_PAGE
)
+
1
)
{
>
// In this case we have to load more data and then append them.
复制
loadTokens
(
page
-
1
).
then
(
r
=>
{
<
/Button
>
});
<
Dropdown
}
className
=
'button icon'
};
floating
options
=
{
COPY_OPTIONS
.
map
(
option
=>
({
const
rowSelection
=
{
...
option
,
onSelect
:
(
record
,
selected
)
=>
{
onClick
:
async
()
=>
{
// console.log(`select row: ${selected}`, record);
await
onCopy
(
option
.
value
,
token
.
key
);
},
}
onSelectAll
:
(
selected
,
selectedRows
)
=>
{
}))}
// console.log(`select all rows: ${selected}`, selectedRows);
trigger
=
{
<><
/>
}
},
/>
onChange
:
(
selectedRowKeys
,
selectedRows
)
=>
{
<
/Button.Group
>
// console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
{
' '
}
setSelectedKeys
(
selectedRows
);
<
Button
.
Group
color
=
'blue'
size
=
{
'small'
}
>
},
<
Button
};
size
=
{
'small'
}
positive
return
(
onClick
=
{()
=>
{
<>
onOpenLink
(
''
,
token
.
key
);
<
Form
layout
=
'horizontal'
style
=
{{
marginTop
:
10
}}
labelPosition
=
{
'left'
}
>
}}
>
<
Form
.
Input
聊天
field
=
"keyword"
<
/Button
>
label
=
'搜索关键字'
<
Dropdown
placeholder
=
'令牌名称'
className
=
"button icon"
value
=
{
searchKeyword
}
floating
loading
=
{
searching
}
options
=
{
OPEN_LINK_OPTIONS
.
map
(
option
=>
({
onChange
=
{
handleKeywordChange
}
...
option
,
/
>
onClick
:
async
()
=>
{
<
Button
label
=
'查询'
type
=
"primary"
htmlType
=
"submit"
className
=
"btn-margin-right"
await
onOpenLink
(
option
.
value
,
token
.
key
);
onClick
=
{
searchTokens
}
style
=
{{
marginRight
:
8
}}
>
查询
<
/Button
>
}
<
/Form
>
}))}
trigger
=
{
<><
/>
}
<
Table
style
=
{{
marginTop
:
20
}}
columns
=
{
columns
}
dataSource
=
{
tokens
}
pagination
=
{{
/>
currentPage
:
activePage
,
<
/Button.Group
>
pageSize
:
ITEMS_PER_PAGE
,
{
' '
}
total
:
tokenCount
,
<
Popup
pageSizeOpts
:
[
10
,
20
,
50
,
100
],
trigger
=
{
onPageChange
:
handlePageChange
,
<
Button
}}
loading
=
{
loading
}
rowSelection
=
{
rowSelection
}
>
size
=
{
'small'
}
<
/Table
>
positive
<
Button
theme
=
'light'
type
=
'primary'
style
=
{{
marginRight
:
8
}}
>
添加令牌
<
/Button
>
onClick
=
{
async
()
=>
{
<
Button
label
=
'复制所选令牌'
type
=
"warning"
onClick
=
{
let
key
=
"sk-"
+
token
.
key
;
async
()
=>
{
if
(
await
copy
(
key
))
{
let
keys
=
""
;
showSuccess
(
'已复制到剪贴板!'
);
for
(
let
i
=
0
;
i
<
selectedKeys
.
length
;
i
++
)
{
}
else
{
keys
+=
selectedKeys
[
i
].
name
+
" sk-"
+
selectedKeys
[
i
].
key
+
"\n"
;
showWarning
(
'无法复制到剪贴板,请手动复制,已将令牌填入搜索框。'
);
}
setSearchKeyword
(
key
);
await
copyText
(
keys
);
}
}}
>
复制
<
/Button
>
}
on
=
{
'hover'
}
content
=
{
"sk-"
+
token
.
key
}
/
>
<
Popup
trigger
=
{
<
Button
size
=
'small'
negative
>
删除
<
/Button
>
}
on
=
'click'
flowing
hoverable
>
<
Button
negative
onClick
=
{()
=>
{
manageToken
(
token
.
id
,
'delete'
,
idx
);
}}
>
删除令牌
{
token
.
name
}
<
/Button
>
<
/Popup
>
<
Button
size
=
{
'small'
}
onClick
=
{()
=>
{
manageToken
(
token
.
id
,
token
.
status
===
1
?
'disable'
:
'enable'
,
idx
);
}}
>
{
token
.
status
===
1
?
'禁用'
:
'启用'
}
<
/Button
>
<
Button
size
=
{
'small'
}
as
=
{
Link
}
to
=
{
'/token/edit/'
+
token
.
id
}
>
编辑
<
/Button
>
<
/div
>
<
/Table.Cell
>
<
/Table.Row
>
);
})}
<
/Table.Body
>
<
Table
.
Footer
>
<
Table
.
Row
>
<
Table
.
HeaderCell
colSpan
=
'7'
>
<
Button
size
=
'small'
as
=
{
Link
}
to
=
'/token/add'
loading
=
{
loading
}
>
添加新的令牌
<
/Button
>
<
Button
size
=
'small'
onClick
=
{
refresh
}
loading
=
{
loading
}
>
刷新
<
/Button
>
<
Pagination
floated
=
'right'
activePage
=
{
activePage
}
onPageChange
=
{
onPaginationChange
}
size
=
'small'
siblingRange
=
{
1
}
totalPages
=
{
Math
.
ceil
(
tokens
.
length
/
ITEMS_PER_PAGE
)
+
(
tokens
.
length
%
ITEMS_PER_PAGE
===
0
?
1
:
0
)
}
}
/
>
}
>
复制所选令牌到剪贴板
<
/Button
>
<
/Table.HeaderCell
>
<
/
>
<
/Table.Row
>
);
<
/Table.Footer
>
<
/Table
>
<
/
>
);
};
};
export
default
TokensTable
;
export
default
TokensTable
;
web/src/helpers/utils.js
View file @
607358c9
import
{
toast
}
from
'react-toastify
'
;
import
{
Toast
}
from
'@douyinfe/semi-ui
'
;
import
{
toastConstants
}
from
'../constants'
;
import
{
toastConstants
}
from
'../constants'
;
import
React
from
'react'
;
import
React
from
'react'
;
import
{
toast
}
from
"react-toastify"
;
const
HTMLToastContent
=
({
htmlContent
})
=>
{
const
HTMLToastContent
=
({
htmlContent
})
=>
{
return
<
div
dangerouslySetInnerHTML
=
{{
__html
:
htmlContent
}}
/>
;
return
<
div
dangerouslySetInnerHTML
=
{{
__html
:
htmlContent
}}
/>
;
...
@@ -81,42 +82,42 @@ export function showError(error) {
...
@@ -81,42 +82,42 @@ export function showError(error) {
window
.
location
.
href
=
'/login?expired=true'
;
window
.
location
.
href
=
'/login?expired=true'
;
break
;
break
;
case
429
:
case
429
:
toast
.
error
(
'错误:请求次数过多,请稍后再试!'
,
showErrorOptions
);
Toast
.
error
(
'错误:请求次数过多,请稍后再试!'
);
break
;
break
;
case
500
:
case
500
:
toast
.
error
(
'错误:服务器内部错误,请联系管理员!'
,
showErrorOptions
);
Toast
.
error
(
'错误:服务器内部错误,请联系管理员!'
);
break
;
break
;
case
405
:
case
405
:
t
oast
.
info
(
'本站仅作演示之用,无服务端!'
);
T
oast
.
info
(
'本站仅作演示之用,无服务端!'
);
break
;
break
;
default
:
default
:
toast
.
error
(
'错误:'
+
error
.
message
,
showErrorOptions
);
Toast
.
error
(
'错误:'
+
error
.
message
);
}
}
return
;
return
;
}
}
toast
.
error
(
'错误:'
+
error
.
message
,
showErrorOptions
);
Toast
.
error
(
'错误:'
+
error
.
message
);
}
else
{
}
else
{
toast
.
error
(
'错误:'
+
error
,
showErrorOptions
);
Toast
.
error
(
'错误:'
+
error
);
}
}
}
}
export
function
showWarning
(
message
)
{
export
function
showWarning
(
message
)
{
toast
.
warn
(
message
,
showWarningOptions
);
Toast
.
warning
(
message
);
}
}
export
function
showSuccess
(
message
)
{
export
function
showSuccess
(
message
)
{
toast
.
success
(
message
,
showSuccessOptions
);
Toast
.
success
(
message
);
}
}
export
function
showInfo
(
message
)
{
export
function
showInfo
(
message
)
{
toast
.
info
(
message
,
showInfoOptions
);
Toast
.
info
(
message
);
}
}
export
function
showNotice
(
message
,
isHTML
=
false
)
{
export
function
showNotice
(
message
,
isHTML
=
false
)
{
if
(
isHTML
)
{
if
(
isHTML
)
{
toast
(
<
HTMLToastContent
htmlContent
=
{
message
}
/>, showNoticeOptions
)
;
toast
(
<
HTMLToastContent
htmlContent
=
{
message
}
/>, showNoticeOptions
)
;
}
else
{
}
else
{
toast
.
info
(
message
,
showNoticeOptions
);
Toast
.
info
(
message
);
}
}
}
}
...
...
web/src/pages/Token/index.js
View file @
607358c9
import
React
from
'react'
;
import
React
from
'react'
;
import
{
Segment
,
Header
}
from
'semantic-ui-react'
;
import
TokensTable
from
'../../components/TokensTable'
;
import
TokensTable
from
'../../components/TokensTable'
;
import
{
Layout
}
from
"@douyinfe/semi-ui"
;
const
{
Content
,
Header
}
=
Layout
;
const
Token
=
()
=>
(
const
Token
=
()
=>
(
<>
<>
<
Segment
>
<
Layout
>
<
Header
as
=
'h3'
>
我的令牌
<
/Header
>
<
Header
>
<
TokensTable
/>
<
h3
>
我的令牌
<
/h3
>
<
/Segment
>
<
/Header
>
<
Content
>
<
TokensTable
/>
<
/Content
>
<
/Layout
>
<
/
>
<
/
>
);
);
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment