mirror of
https://github.com/APIParkLab/APIPark.git
synced 2026-06-14 20:41:15 +08:00
feat: ai apis
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
import { Codebox } from '@common/components/postcat/api/Codebox'
|
||||
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
|
||||
import { useFetch } from '@common/hooks/http'
|
||||
import { $t } from '@common/locales'
|
||||
import { AIProvider } from '@core/components/AIProviderSelect'
|
||||
import { App, DatePicker, Form, Input, Switch } from 'antd'
|
||||
import dayjs from 'dayjs'
|
||||
import React, { forwardRef, useEffect, useImperativeHandle, useState } from 'react'
|
||||
import { EditAPIKey } from '../types'
|
||||
|
||||
interface ApiKeyContentProps {
|
||||
provider?: AIProvider
|
||||
entity: EditAPIKey
|
||||
}
|
||||
|
||||
const ApiKeyContent: React.FC<ApiKeyContentProps> = forwardRef(({ provider, entity }, ref) => {
|
||||
const [form] = Form.useForm()
|
||||
const [neverExpire, setNeverExpire] = useState(true)
|
||||
const { fetchData } = useFetch()
|
||||
const { message } = App.useApp()
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
const isNeverExpire = entity.expire_time === 0
|
||||
setNeverExpire(isNeverExpire)
|
||||
form.setFieldsValue({
|
||||
name: entity.name,
|
||||
expire_time: isNeverExpire ? undefined : dayjs(entity.expire_time),
|
||||
config: entity.config
|
||||
})
|
||||
} catch (e) {
|
||||
form.setFieldsValue({
|
||||
name: entity.name,
|
||||
expire_time: undefined,
|
||||
config: ''
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleOk = async () => {
|
||||
try {
|
||||
const values = await form.validateFields()
|
||||
const { expire_time, ...restValues } = values
|
||||
const expireTime = neverExpire ? 0 : expire_time.valueOf()
|
||||
|
||||
const response = await fetchData<BasicResponse<null>>('ai/resource/key', {
|
||||
method: entity.id ? 'PUT' : 'POST',
|
||||
eoParams: { provider: provider?.id, id: entity.id },
|
||||
eoBody: { ...restValues, expire_time: expireTime },
|
||||
eoTransformKeys: ['config']
|
||||
})
|
||||
|
||||
const { code, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
message.success(msg || $t(RESPONSE_TIPS.success))
|
||||
return true
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
return false
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Validation failed:', error)
|
||||
}
|
||||
}
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOk
|
||||
}))
|
||||
const handleNeverExpireChange = (checked: boolean) => {
|
||||
setNeverExpire(checked)
|
||||
if (!checked) {
|
||||
form.setFieldsValue({
|
||||
expire_time: dayjs().add(7, 'days')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item name="name" label={$t('名称')} rules={[{ required: true, message: $t('请输入 APIKey') }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label={$t('API Key')} name="config" rules={[{ required: true, message: $t('请填写 APIKey') }]}>
|
||||
<Codebox
|
||||
editorTheme="vs-dark"
|
||||
readOnly={false}
|
||||
width="100%"
|
||||
height="150px"
|
||||
language="json"
|
||||
enableToolbar={false}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label={$t('过期时间')} name="neverExpire" valuePropName="checked">
|
||||
<div className="flex items-center">
|
||||
<Switch onChange={handleNeverExpireChange} checked={neverExpire} />
|
||||
<span className="ml-2">{neverExpire ? $t('永不过期') : $t('设置过期时间')}</span>
|
||||
</div>
|
||||
</Form.Item>
|
||||
{!neverExpire && (
|
||||
<Form.Item
|
||||
name="expire_time"
|
||||
label={$t('过期时间')}
|
||||
rules={[{ required: true, message: $t('请选择过期时间') }]}
|
||||
>
|
||||
<DatePicker style={{ width: '100%' }} showTime />
|
||||
</Form.Item>
|
||||
)}
|
||||
</Form>
|
||||
)
|
||||
})
|
||||
|
||||
export default ApiKeyContent
|
||||
@@ -0,0 +1,40 @@
|
||||
import { $t } from '@common/locales'
|
||||
import { Select, Space, theme } from 'antd'
|
||||
import React from 'react'
|
||||
|
||||
interface StatusFilterProps {
|
||||
value: string[]
|
||||
onChange: (value: string[]) => void
|
||||
}
|
||||
|
||||
const StatusFilter: React.FC<StatusFilterProps> = ({ value, onChange }) => {
|
||||
const { token } = theme.useToken()
|
||||
|
||||
const options = [
|
||||
{ label: $t('Normal'), value: 'normal', color: token.colorSuccess },
|
||||
{ label: $t('Exceeded'), value: 'exceeded', color: token.colorError },
|
||||
{ label: $t('Expired'), value: 'expired', color: token.colorWarning },
|
||||
{ label: $t('Disabled'), value: 'disabled', color: token.colorTextDisabled },
|
||||
{ label: $t('Error'), value: 'error', color: token.colorError }
|
||||
]
|
||||
|
||||
return (
|
||||
<Space>
|
||||
<span>{$t('Status')}:</span>
|
||||
<Select
|
||||
mode="multiple"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
style={{ width: 300 }}
|
||||
placeholder={$t('Filter by status')}
|
||||
allowClear
|
||||
options={options.map((option) => ({
|
||||
...option,
|
||||
label: <span style={{ color: option.color }}>{option.label}</span>
|
||||
}))}
|
||||
/>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
|
||||
export default StatusFilter
|
||||
@@ -1,17 +1,357 @@
|
||||
import Icon from '@ant-design/icons'
|
||||
import { ActionType } from '@ant-design/pro-components'
|
||||
import InsidePage from '@common/components/aoplatform/InsidePage'
|
||||
import PageList, { PageProColumns } from '@common/components/aoplatform/PageList'
|
||||
import TableBtnWithPermission from '@common/components/aoplatform/TableBtnWithPermission'
|
||||
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
|
||||
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
|
||||
import { useFetch } from '@common/hooks/http'
|
||||
import { $t } from '@common/locales'
|
||||
import React from 'react'
|
||||
import { checkAccess } from '@common/utils/permission'
|
||||
import AIProviderSelect, { AIProvider } from '@core/components/AIProviderSelect'
|
||||
import { App, Divider, Space, Typography } from 'antd'
|
||||
import dayjs from 'dayjs'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import ApiKeyContent from './components/ApiKeyContent'
|
||||
import { APIKey, EditAPIKey } from './types'
|
||||
|
||||
const KeySettings: React.FC = () => {
|
||||
const pageListRef = useRef<ActionType>(null)
|
||||
const { modal, message } = App.useApp()
|
||||
const [selectedProvider, setSelectedProvider] = useState<string>()
|
||||
const [provider, setProvider] = useState<AIProvider | undefined>()
|
||||
const [apiKeys, setApiKeys] = useState<APIKey[]>([])
|
||||
const { fetchData } = useFetch()
|
||||
const [searchWord, setSearchWord] = useState<string>('')
|
||||
const [total, setTotal] = useState<number>(0)
|
||||
const modalRef = useRef<any>()
|
||||
const { accessData } = useGlobalContext()
|
||||
|
||||
useEffect(() => {
|
||||
pageListRef.current?.reload()
|
||||
}, [selectedProvider])
|
||||
|
||||
const handleEdit = (record: APIKey) => {
|
||||
openModal(record)
|
||||
}
|
||||
|
||||
const handleAdd = () => {
|
||||
openModal()
|
||||
}
|
||||
|
||||
const openModal = async (entity?: EditAPIKey) => {
|
||||
if (!provider) return
|
||||
const mode = entity ? 'edit' : 'add'
|
||||
if (mode === 'edit') {
|
||||
message.loading($t(RESPONSE_TIPS.loading))
|
||||
const { code, data, msg } = await fetchData<BasicResponse<{ info: EditAPIKey }>>('ai/resource/key', {
|
||||
method: 'GET',
|
||||
eoParams: { provider: selectedProvider, id: entity!.id }
|
||||
// eoApiPrefix: 'http://uat.apikit.com:11204/mockApi/aoplatform/api/v1/'
|
||||
})
|
||||
message.destroy()
|
||||
if (code !== STATUS_CODE.SUCCESS) {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
return
|
||||
}
|
||||
entity = data?.info
|
||||
} else {
|
||||
entity = {
|
||||
name: `key${total}`,
|
||||
config: provider.default_config,
|
||||
expire_time: 0
|
||||
} as EditAPIKey
|
||||
}
|
||||
const newEntity = entity as EditAPIKey
|
||||
|
||||
modal.confirm({
|
||||
title: mode === 'add' ? $t(`添加 ${provider?.name} APIKey`) : $t('编辑 APIKey'),
|
||||
content: <ApiKeyContent ref={modalRef} entity={newEntity} provider={provider} />,
|
||||
onOk: () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
modalRef.current?.handleOk().then((res: boolean) => {
|
||||
if (res === true) {
|
||||
pageListRef.current?.reload()
|
||||
resolve(res)
|
||||
return
|
||||
}
|
||||
reject()
|
||||
})
|
||||
})
|
||||
},
|
||||
width: 600,
|
||||
okText: $t('确认'),
|
||||
footer: (_, { OkBtn, CancelBtn }) => {
|
||||
return (
|
||||
<div className="flex justify-between items-center">
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={provider.getApikeyUrl}
|
||||
className="flex items-center gap-[8px]"
|
||||
>
|
||||
<span>{$t('从 (0) 获取 API KEY', [provider.name])}</span>
|
||||
<Icon icon="ic:baseline-open-in-new" width={16} height={16} />
|
||||
</a>
|
||||
<div>
|
||||
<CancelBtn />
|
||||
{checkAccess('system.settings.ai_key_resource.manager', accessData) ? <OkBtn /> : null}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
cancelText: $t('取消'),
|
||||
closable: true,
|
||||
icon: <></>
|
||||
})
|
||||
}
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
try {
|
||||
const response = await fetchData<BasicResponse<any>>('ai/resource/key', {
|
||||
method: 'DELETE',
|
||||
eoParams: {
|
||||
provider: selectedProvider,
|
||||
id: id,
|
||||
branchID: 0
|
||||
}
|
||||
// eoApiPrefix: 'http://uat.apikit.com:11204/mockApi/aoplatform/api/v1/'
|
||||
})
|
||||
|
||||
if (response.code === STATUS_CODE.SUCCESS) {
|
||||
message.success($t('删除成功'))
|
||||
pageListRef.current?.reload()
|
||||
} else {
|
||||
message.error(response.msg || RESPONSE_TIPS.error)
|
||||
}
|
||||
} catch (error) {
|
||||
message.error(RESPONSE_TIPS.error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleToggleStatus = async (id: string, currentStatus: string) => {
|
||||
try {
|
||||
const newStatus = currentStatus === 'normal' ? 'disable' : 'enable'
|
||||
const response = await fetchData<BasicResponse<any>>(`ai/resource/key/${newStatus}`, {
|
||||
method: 'PUT',
|
||||
eoParams: {
|
||||
provider: selectedProvider,
|
||||
id: id
|
||||
}
|
||||
// eoApiPrefix: 'http://uat.apikit.com:11204/mockApi/aoplatform/api/v1/'
|
||||
})
|
||||
|
||||
if (response.code === STATUS_CODE.SUCCESS) {
|
||||
message.success(newStatus === 'disable' ? $t('停用成功') : $t('启用成功'))
|
||||
pageListRef.current?.reload()
|
||||
} else {
|
||||
message.error(response.msg || RESPONSE_TIPS.error)
|
||||
}
|
||||
} catch (error) {
|
||||
message.error(RESPONSE_TIPS.error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDragSortEnd = async (beforeIndex: number, afterIndex: number, newDataSource: APIKey[]) => {
|
||||
console.log(beforeIndex, afterIndex, newDataSource)
|
||||
try {
|
||||
const response = await fetchData<BasicResponse<any>>('ai/resource/key/sort', {
|
||||
method: 'PUT',
|
||||
eoParams: {
|
||||
origin: newDataSource[beforeIndex].id,
|
||||
target: newDataSource[afterIndex].id,
|
||||
sort: afterIndex > beforeIndex ? 'before' : 'after'
|
||||
}
|
||||
// eoApiPrefix: 'http://uat.apikit.com:11204/mockApi/aoplatform/api/v1/'
|
||||
})
|
||||
|
||||
if (response.code === STATUS_CODE.SUCCESS) {
|
||||
message.success($t('排序成功'))
|
||||
pageListRef.current?.reload()
|
||||
} else {
|
||||
message.error(response.msg || RESPONSE_TIPS.error)
|
||||
}
|
||||
} catch (error) {
|
||||
message.error(RESPONSE_TIPS.error)
|
||||
}
|
||||
}
|
||||
|
||||
const requestApiKeys = async (params: any) => {
|
||||
if (!selectedProvider) return
|
||||
try {
|
||||
const response = await fetchData<BasicResponse<{ data: APIKey[] }>>('ai/resource/keys', {
|
||||
method: 'GET',
|
||||
eoParams: {
|
||||
provider: selectedProvider,
|
||||
page_size: params.pageSize,
|
||||
keyword: searchWord,
|
||||
page: params.current
|
||||
}
|
||||
// eoApiPrefix: 'http://uat.apikit.com:11204/mockApi/aoplatform/api/v1/'
|
||||
})
|
||||
|
||||
if (response.code === STATUS_CODE.SUCCESS) {
|
||||
setTotal(response.data.total)
|
||||
return {
|
||||
data: response.data.keys,
|
||||
success: true,
|
||||
total: response.data.total
|
||||
}
|
||||
} else {
|
||||
message.error(response.msg || $t(RESPONSE_TIPS.error))
|
||||
return {
|
||||
data: [],
|
||||
success: false,
|
||||
total: response.data.total
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
data: [],
|
||||
success: false,
|
||||
total: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
const statusEnum = {
|
||||
normal: { text: <Typography.Text type="success">{$t('正常')}</Typography.Text> },
|
||||
exceeded: { text: <Typography.Text type="warning">{$t('超额')}</Typography.Text> },
|
||||
expired: { text: <Typography.Text type="secondary">{$t('过期')}</Typography.Text> },
|
||||
disabled: { text: <Typography.Text type="warning">{$t('停用')}</Typography.Text> },
|
||||
error: { text: <Typography.Text type="danger">{$t('错误')}</Typography.Text> }
|
||||
}
|
||||
|
||||
const operation: PageProColumns<APIKey>[] = [
|
||||
{
|
||||
title: '',
|
||||
key: 'option',
|
||||
btnNums: 4,
|
||||
fixed: 'right',
|
||||
valueType: 'option',
|
||||
render: (_: React.ReactNode, entity: APIKey) => [
|
||||
<TableBtnWithPermission
|
||||
access="system.settings.ai_key_resource.manager"
|
||||
key="edit"
|
||||
btnType="edit"
|
||||
onClick={() => handleEdit(entity)}
|
||||
btnTitle={$t('编辑')}
|
||||
/>,
|
||||
<Divider type="vertical" className="mx-0" key="div1" />,
|
||||
entity.status !== 'expired' && entity.status !== 'error' && (
|
||||
<>
|
||||
<TableBtnWithPermission
|
||||
access="system.settings.ai_key_resource.manager"
|
||||
key="toggle"
|
||||
btnType={entity.status === 'normal' ? 'disable' : 'enable'}
|
||||
onClick={() => handleToggleStatus(entity.id, entity.status)}
|
||||
btnTitle={entity.status === 'normal' ? $t('停用') : $t('启用')}
|
||||
/>
|
||||
<Divider type="vertical" className="mx-0" key="div2" />
|
||||
</>
|
||||
),
|
||||
entity.can_delete !== false && (
|
||||
<TableBtnWithPermission
|
||||
access="system.settings.ai_key_resource.manager"
|
||||
key="delete"
|
||||
btnType="delete"
|
||||
onClick={() => handleDelete(entity.id as string)}
|
||||
btnTitle={$t('删除')}
|
||||
/>
|
||||
)
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const columns: PageProColumns<APIKey>[] = [
|
||||
{
|
||||
title: '',
|
||||
dataIndex: 'drag',
|
||||
width: '40px'
|
||||
},
|
||||
{
|
||||
title: $t('调用优先级'),
|
||||
dataIndex: 'priority',
|
||||
width: '100px'
|
||||
},
|
||||
{
|
||||
title: $t('名称'),
|
||||
dataIndex: 'name',
|
||||
render: (dom: React.ReactNode, entity: APIKey) => <Space>{entity.name}</Space>
|
||||
},
|
||||
{
|
||||
title: $t('状态'),
|
||||
dataIndex: 'status',
|
||||
ellipsis: true,
|
||||
valueType: 'select',
|
||||
filters: true,
|
||||
onFilter: true,
|
||||
valueEnum: statusEnum,
|
||||
render: (dom: React.ReactNode, entity: APIKey) => statusEnum[entity.status]?.text || entity.status
|
||||
},
|
||||
{
|
||||
title: $t('已用 Token'),
|
||||
dataIndex: 'use_token',
|
||||
render: (dom: React.ReactNode, entity: APIKey) => {
|
||||
const value = entity.use_token
|
||||
return value.toLocaleString()
|
||||
}
|
||||
},
|
||||
{
|
||||
title: $t('编辑时间'),
|
||||
dataIndex: 'update_time'
|
||||
},
|
||||
{
|
||||
title: $t('过期时间'),
|
||||
dataIndex: 'expire_time',
|
||||
render: (dom: React.ReactNode, entity: APIKey) => {
|
||||
return entity.expire_time === 0
|
||||
? $t('永不过期')
|
||||
: dayjs(Number(entity.expire_time)).format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
},
|
||||
...operation
|
||||
]
|
||||
|
||||
const AIApis: React.FC = () => {
|
||||
return (
|
||||
<InsidePage
|
||||
className="overflow-y-auto pb-PAGE_INSIDE_B"
|
||||
pageTitle={$t('AI API')}
|
||||
description={$t('配置好 AI 模型后,你可以使用对应的大模型来创建 AI 服务')}
|
||||
className="overflow-y-auto gap-4 pb-PAGE_INSIDE_B pr-PAGE_INSIDE_X"
|
||||
pageTitle={$t('AI API 列表')}
|
||||
description={
|
||||
<>
|
||||
{$t('支持查看调用某个 AI 供应商的所有 AI 服务 API 清单')}
|
||||
<div className="mt-4">
|
||||
<AIProviderSelect
|
||||
value={selectedProvider}
|
||||
onChange={(value: string, provider: AIProvider) => {
|
||||
setSelectedProvider(value)
|
||||
setProvider(provider)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
showBorder={false}
|
||||
scrollPage={false}
|
||||
></InsidePage>
|
||||
>
|
||||
<div className="h-[calc(100%-1rem-36px)]">
|
||||
<PageList
|
||||
ref={pageListRef}
|
||||
rowKey="id"
|
||||
request={requestApiKeys}
|
||||
onSearchWordChange={(e) => {
|
||||
setSearchWord(e.target.value)
|
||||
}}
|
||||
showPagination={true}
|
||||
searchPlaceholder={$t('请输入 APIURL 搜索')}
|
||||
columns={columns}
|
||||
dragSortKey="drag"
|
||||
onDragSortEnd={handleDragSortEnd}
|
||||
addNewBtnTitle={$t('添加 APIKey')}
|
||||
onAddNewBtnClick={handleAdd}
|
||||
/>
|
||||
</div>
|
||||
</InsidePage>
|
||||
)
|
||||
}
|
||||
|
||||
export default AIApis
|
||||
export default KeySettings
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
|
||||
|
||||
export interface APIKey extends EditAPIKey {
|
||||
status: 'normal' | 'exceeded' | 'expired' | 'disabled' | 'error'
|
||||
use_token: number
|
||||
update_time: string
|
||||
can_delete: boolean
|
||||
priority: number
|
||||
}
|
||||
|
||||
export interface EditAPIKey {
|
||||
id?: string
|
||||
name: string
|
||||
config: string
|
||||
expire_time: number
|
||||
}
|
||||
Reference in New Issue
Block a user