diff --git a/README.md b/README.md index 2c81430..2499142 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,14 @@ # 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 diff --git a/web/public/index.html b/web/public/index.html index 799e52b..c219de9 100644 --- a/web/public/index.html +++ b/web/public/index.html @@ -6,10 +6,9 @@ - Neko API + content="NekoAPI,企业级AI接口调用平台,专为企业级需求打造,提供高性能、高并发、高可用的服务,一站式处理大规模数据和复杂任务。我们的稳定高并发处理能力和高可用性保证您的业务流畅运行,结合OpenAI、Claude、Midjourney等AI接口和专业的技术支持,为您的企业快速部署和实现AI接口应用,释放商业价值"/> - Neko API + NekoAPI diff --git a/web/src/components/LogsTable.js b/web/src/components/LogsTable.js index 10726a7..832acb5 100644 --- a/web/src/components/LogsTable.js +++ b/web/src/components/LogsTable.js @@ -20,7 +20,7 @@ import { } from '@douyinfe/semi-icons'; const {Sider, Content, Header} = Layout; -const { Column } = Table; +const {Column} = Table; function renderTimestamp(timestamp) { @@ -65,13 +65,13 @@ const LogsTable = () => { { title: '渠道', dataIndex: 'channel', - className: isAdmin()?'tableShow':'tableHiddle', + className: isAdmin() ? 'tableShow' : 'tableHiddle', render: (text, record, index) => { return ( isAdminUser ? record.type === 0 || record.type === 2 ?
- { {text} } + { {text} }
: <> @@ -83,7 +83,7 @@ const LogsTable = () => { { title: '用户', dataIndex: 'username', - className: isAdmin()?'tableShow':'tableHiddle', + className: isAdmin() ? 'tableShow' : 'tableHiddle', render: (text, record, index) => { return ( isAdminUser ? @@ -291,15 +291,6 @@ const LogsTable = () => { 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 => { setActivePage(page); if (page === Math.ceil(logs.length / ITEMS_PER_PAGE) + 1) { @@ -307,7 +298,6 @@ const LogsTable = () => { loadLogs(page - 1).then(r => { }); } - // setLoading(false); }; const refresh = async () => { @@ -405,7 +395,7 @@ const LogsTable = () => { } - diff --git a/web/src/components/SiderBar.js b/web/src/components/SiderBar.js index 9d4fe89..685bf65 100644 --- a/web/src/components/SiderBar.js +++ b/web/src/components/SiderBar.js @@ -34,7 +34,7 @@ let headerButtons = [ itemKey: 'channel', to: '/channel', icon: , - admin: true + className: isAdmin()?'semi-navigation-item-normal':'tableHiddle', }, { @@ -48,7 +48,7 @@ let headerButtons = [ itemKey: 'redemption', to: '/redemption', icon: , - admin: true + className: isAdmin()?'semi-navigation-item-normal':'tableHiddle', }, { text: '钱包', @@ -61,7 +61,7 @@ let headerButtons = [ itemKey: 'user', to: '/user', icon: , - admin: true + className: isAdmin()?'semi-navigation-item-normal':'tableHiddle', }, { text: '日志', diff --git a/web/src/components/TokensTable.js b/web/src/components/TokensTable.js index 6772c23..584834b 100644 --- a/web/src/components/TokensTable.js +++ b/web/src/components/TokensTable.js @@ -1,464 +1,460 @@ -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 { API, copy, showError, showSuccess, showWarning, timestamp2string } from '../helpers'; +import React, {useEffect, useState} from 'react'; +import {Link} from 'react-router-dom'; +import {API, copy, isAdmin, showError, showSuccess, showWarning, timestamp2string} from '../helpers'; -import { ITEMS_PER_PAGE } from '../constants'; -import { renderQuota } from '../helpers/render'; +import {ITEMS_PER_PAGE} from '../constants'; +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 = [ - { key: 'next', text: 'ChatGPT Next Web', value: 'next' }, - { key: 'ama', text: 'AMA 问天', value: 'ama' }, - { key: 'opencat', text: 'OpenCat', value: 'opencat' }, + {key: 'next', text: 'ChatGPT Next Web', value: 'next'}, + {key: 'ama', text: 'AMA 问天', value: 'ama'}, + {key: 'opencat', text: 'OpenCat', value: 'opencat'}, ]; const OPEN_LINK_OPTIONS = [ - { key: 'ama', text: 'AMA 问天', value: 'ama' }, - { key: 'opencat', text: 'OpenCat', value: 'opencat' }, + {key: 'ama', text: 'AMA 问天', value: 'ama'}, + {key: 'opencat', text: 'OpenCat', value: 'opencat'}, ]; function renderTimestamp(timestamp) { - return ( - <> - {timestamp2string(timestamp)} - - ); + return ( + <> + {timestamp2string(timestamp)} + + ); } function renderStatus(status) { - switch (status) { - case 1: - return ; - case 2: - return ; - case 3: - return ; - case 4: - return ; - default: - return ; - } + switch (status) { + case 1: + return 已启用; + case 2: + return 已禁用 ; + case 3: + return 已过期 ; + case 4: + return 已耗尽 ; + default: + return 未知状态 ; + } } const TokensTable = () => { - const [tokens, setTokens] = useState([]); - 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 loadTokens = async (startIdx) => { - const res = await API.get(`/api/token/?p=${startIdx}`); - const { success, message, data } = res.data; - if (success) { - if (startIdx === 0) { - setTokens(data); - } else { - let newTokens = [...tokens]; - newTokens.splice(startIdx * ITEMS_PER_PAGE, data.length, ...data); - setTokens(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 () => { - 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; - 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; - case 'next': - url = nextUrl; - break; - default: - url = `sk-${key}`; - } - 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'); - } - - useEffect(() => { - loadTokens(0) - .then() - .catch((reason) => { - showError(reason); - }); - }, []); - - const manageToken = async (id, action, idx) => { - 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') { - 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 handleKeywordChange = async (e, { value }) => { - setSearchKeyword(value.trim()); - }; - - 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); - }; - - return ( - <> -
- - - - - - - { - sortToken('name'); - }} - > - 名称 - - { - sortToken('status'); - }} - > - 状态 - - { - sortToken('used_quota'); - }} - > - 已用额度 - - { - sortToken('remain_quota'); - }} - > - 剩余额度 - - { - sortToken('created_time'); - }} - > - 创建时间 - - { - sortToken('expired_time'); - }} - > - 过期时间 - - 操作 - - - - - {tokens - .slice( - (activePage - 1) * ITEMS_PER_PAGE, - activePage * ITEMS_PER_PAGE - ) - .map((token, idx) => { - if (token.deleted) return <>; - return ( - - {token.name ? token.name : '无'} - {renderStatus(token.status)} - {renderQuota(token.used_quota)} - {token.unlimited_quota ? '无限制' : renderQuota(token.remain_quota, 2)} - {renderTimestamp(token.created_time)} - {token.expired_time === -1 ? '永不过期' : renderTimestamp(token.expired_time)} - + const columns = [ + { + title: '名称', + dataIndex: 'name', + }, + { + title: '状态', + dataIndex: 'status', + key: 'status', + render: (text, record, index) => { + return (
- - - ({ - ...option, - onClick: async () => { - await onCopy(option.value, token.key); - } - }))} - trigger={<>} - /> - - {' '} - - - ({ - ...option, - onClick: async () => { - await onOpenLink(option.value, token.key); - } - }))} - trigger={<>} - /> - - {' '} - { - let key = "sk-" + token.key; - if (await copy(key)) { - showSuccess('已复制到剪贴板!'); - } else { - showWarning('无法复制到剪贴板,请手动复制,已将令牌填入搜索框。'); - setSearchKeyword(key); - } - }} - > - 复制 - - } - on={'hover'} - content={"sk-" + token.key} - /> - - 删除 - - } - on='click' - flowing - hoverable - > - - - - + {renderStatus(text)}
-
-
- ); - })} -
+ ); + }, + }, + { + title: '已用额度', + dataIndex: 'used_quota', + render: (text, record, index) => { + return ( +
+ {renderQuota(parseInt(text))} +
+ ); + }, + }, + { + title: '剩余额度', + dataIndex: 'remain_quota', + render: (text, record, index) => { + return ( +
+ {renderQuota(parseInt(text))} +
+ ); + }, + }, + { + title: '创建时间', + dataIndex: 'created_time', + render: (text, record, index) => { + return ( +
+ {renderTimestamp(text)} +
+ ); + }, + }, + { + title: '过期时间', + dataIndex: 'accessed_time', + render: (text, record, index) => { + return ( +
+ {renderTimestamp(text)} +
+ ); + }, + }, + { + title: '', + dataIndex: 'operate', + render: (text, record, index) => ( +
+ + + + + { + manageToken(record.id, 'delete', record).then( + () => { + removeRecord(record.key); + } + ) + }} + > + + + { + record.status === 1 ? + : + + } + +
+ ), + }, + ]; - - - - - - { + setTokens(tokens); + if (tokens.length === ITEMS_PER_PAGE) { + setTokenCount(tokens.length + ITEMS_PER_PAGE); + } else { + setTokenCount(tokens.length); + } + } + + // let pageData = tokens.slice((activePage - 1) * ITEMS_PER_PAGE, activePage * ITEMS_PER_PAGE); + const loadTokens = async (startIdx) => { + setLoading(true); + 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); + }; + + 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; + 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; + 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 }); + } + } + + 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'); + } + + useEffect(() => { + loadTokens(0) + .then() + .catch((reason) => { + showError(reason); + }); + }, []); + + 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 () => { + 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) { + setTokensFormat(data); + setActivePage(1); + } else { + showError(message); + } + setSearching(false); + }; + + const handleKeywordChange = async (value) => { + setSearchKeyword(value.trim()); + }; + + 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); + 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 => { + }); + } + }; + + const rowSelection = { + onSelect: (record, selected) => { + // console.log(`select row: ${selected}`, record); + }, + onSelectAll: (selected, selectedRows) => { + // console.log(`select all rows: ${selected}`, selectedRows); + }, + onChange: (selectedRowKeys, selectedRows) => { + // console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows); + setSelectedKeys(selectedRows); + }, + }; + + return ( + <> +
+ + + + +
+
+ + + + ); }; export default TokensTable; diff --git a/web/src/helpers/utils.js b/web/src/helpers/utils.js index ac9c976..8f8ac08 100644 --- a/web/src/helpers/utils.js +++ b/web/src/helpers/utils.js @@ -1,6 +1,7 @@ -import { toast } from 'react-toastify'; +import { Toast } from '@douyinfe/semi-ui'; import { toastConstants } from '../constants'; import React from 'react'; +import {toast} from "react-toastify"; const HTMLToastContent = ({ htmlContent }) => { return
; @@ -81,42 +82,42 @@ export function showError(error) { window.location.href = '/login?expired=true'; break; case 429: - toast.error('错误:请求次数过多,请稍后再试!', showErrorOptions); + Toast.error('错误:请求次数过多,请稍后再试!'); break; case 500: - toast.error('错误:服务器内部错误,请联系管理员!', showErrorOptions); + Toast.error('错误:服务器内部错误,请联系管理员!'); break; case 405: - toast.info('本站仅作演示之用,无服务端!'); + Toast.info('本站仅作演示之用,无服务端!'); break; default: - toast.error('错误:' + error.message, showErrorOptions); + Toast.error('错误:' + error.message); } return; } - toast.error('错误:' + error.message, showErrorOptions); + Toast.error('错误:' + error.message); } else { - toast.error('错误:' + error, showErrorOptions); + Toast.error('错误:' + error); } } export function showWarning(message) { - toast.warn(message, showWarningOptions); + Toast.warning(message); } export function showSuccess(message) { - toast.success(message, showSuccessOptions); + Toast.success(message); } export function showInfo(message) { - toast.info(message, showInfoOptions); + Toast.info(message); } export function showNotice(message, isHTML = false) { if (isHTML) { toast(, showNoticeOptions); } else { - toast.info(message, showNoticeOptions); + Toast.info(message); } } diff --git a/web/src/pages/Token/index.js b/web/src/pages/Token/index.js index c995131..5583bfb 100644 --- a/web/src/pages/Token/index.js +++ b/web/src/pages/Token/index.js @@ -1,13 +1,17 @@ import React from 'react'; -import { Segment, Header } from 'semantic-ui-react'; import TokensTable from '../../components/TokensTable'; - +import {Layout} from "@douyinfe/semi-ui"; +const {Content, Header} = Layout; const Token = () => ( <> - -
我的令牌
- -
+ +
+

我的令牌

+
+ + + +
);