mirror of
https://github.com/APIParkLab/APIPark.git
synced 2026-06-14 20:41:15 +08:00
feat: add api keys
This commit is contained in:
@@ -4,12 +4,14 @@ import { Select, Space, message } from 'antd'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface AIProvider {
|
||||
export interface AIProvider {
|
||||
id: string
|
||||
name: string
|
||||
logo: string
|
||||
configured: boolean
|
||||
getApikeyUrl: string
|
||||
status: string
|
||||
config: string
|
||||
}
|
||||
|
||||
interface AIProviderResponse {
|
||||
@@ -24,7 +26,7 @@ interface AIProviderResponse {
|
||||
|
||||
interface AIProviderSelectProps {
|
||||
value?: string
|
||||
onChange?: (value: string) => void
|
||||
onChange?: (value: string, provider: AIProvider) => void
|
||||
style?: React.CSSProperties
|
||||
}
|
||||
|
||||
@@ -35,24 +37,33 @@ const AIProviderSelect: React.FC<AIProviderSelectProps> = ({ value, onChange, st
|
||||
const { fetchData } = useFetch()
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true
|
||||
const fetchProviders = async () => {
|
||||
setLoading(true)
|
||||
if (isMounted) setLoading(true)
|
||||
try {
|
||||
const response = await fetchData<AIProviderResponse>('simple/ai/providers', { method: 'GET' })
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
setProviders(data.providers)
|
||||
isMounted && setProviders(data.providers)
|
||||
if (!data.providers?.length) return
|
||||
|
||||
const selectedProvider: AIProvider = value ? providers.find((p) => p.id === value) : data.providers[0]
|
||||
onChange?.(selectedProvider.id, selectedProvider)
|
||||
} else {
|
||||
message.error(msg || t('Failed to fetch AI providers'))
|
||||
}
|
||||
} catch (error) {
|
||||
message.error(t('Failed to fetch AI providers'))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
isMounted && setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
fetchProviders()
|
||||
|
||||
return () => {
|
||||
isMounted = false
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
@@ -60,7 +71,10 @@ const AIProviderSelect: React.FC<AIProviderSelectProps> = ({ value, onChange, st
|
||||
<span>{t('AI 供应商')}:</span>
|
||||
<Select
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onChange={(selectedValue) => {
|
||||
const selectedProvider = providers.find((p) => p.id === selectedValue)
|
||||
onChange?.(selectedValue, selectedProvider as AIProvider)
|
||||
}}
|
||||
style={style}
|
||||
loading={loading}
|
||||
showSearch
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import Icon from '@ant-design/icons'
|
||||
import { Codebox } from '@common/components/postcat/api/Codebox'
|
||||
import { $t } from '@common/locales'
|
||||
import { AIProvider } from '@core/components/AIProviderSelect'
|
||||
import { DatePicker, Form, Input, Modal, Switch } from 'antd'
|
||||
import dayjs from 'dayjs'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
@@ -8,50 +11,32 @@ interface ApiKeyModalProps {
|
||||
visible: boolean
|
||||
onCancel: () => void
|
||||
onSave: (values: any) => void
|
||||
providerName: string
|
||||
provider?: AIProvider
|
||||
mode: 'add' | 'edit'
|
||||
initialValues?: Partial<APIKey>
|
||||
defaultKeyNumber?: number
|
||||
entity: APIKey | null
|
||||
}
|
||||
|
||||
const { TextArea } = Input
|
||||
|
||||
const ApiKeyModal: React.FC<ApiKeyModalProps> = ({
|
||||
visible,
|
||||
onCancel,
|
||||
onSave,
|
||||
providerName,
|
||||
mode,
|
||||
initialValues,
|
||||
defaultKeyNumber = 1
|
||||
}) => {
|
||||
const ApiKeyModal: React.FC<ApiKeyModalProps> = ({ visible, onCancel, onSave, provider, mode, entity }) => {
|
||||
const [form] = Form.useForm()
|
||||
const [neverExpire, setNeverExpire] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
if (mode === 'add') {
|
||||
form.setFieldsValue({
|
||||
id: `KEY${defaultKeyNumber}`,
|
||||
neverExpire: true,
|
||||
expire_time: dayjs().add(7, 'days'),
|
||||
name: {
|
||||
openai_api_base: 'API Base',
|
||||
openai_api_key: 'API Key'
|
||||
}
|
||||
})
|
||||
} else if (initialValues) {
|
||||
form.setFieldsValue({
|
||||
id: initialValues.id,
|
||||
name: initialValues.name,
|
||||
expire_time: initialValues.expire_time ? dayjs(initialValues.expire_time) : undefined,
|
||||
enabled: initialValues.enabled,
|
||||
neverExpire: !initialValues.expire_time
|
||||
})
|
||||
setNeverExpire(!initialValues.expire_time)
|
||||
}
|
||||
try {
|
||||
form.setFieldsValue({
|
||||
name: entity?.name,
|
||||
expire_time: entity?.expire_time ? dayjs(entity.expire_time) : undefined,
|
||||
config: entity?.config ? JSON.stringify(JSON.parse(entity?.config), null, 2) : JSON.parse(provider?.config)
|
||||
})
|
||||
setNeverExpire(!entity?.expire_time)
|
||||
} catch (e) {
|
||||
console.error('Error setting form values:', e)
|
||||
form.setFieldsValue({
|
||||
name: entity?.name,
|
||||
expire_time: undefined,
|
||||
config: ''
|
||||
})
|
||||
}
|
||||
}, [visible, mode, initialValues, defaultKeyNumber, form])
|
||||
}, [])
|
||||
|
||||
const handleOk = async () => {
|
||||
try {
|
||||
@@ -74,57 +59,65 @@ const ApiKeyModal: React.FC<ApiKeyModalProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
const getProviderKeyUrl = (provider: string): string => {
|
||||
const urls: Record<string, string> = {
|
||||
openai: 'https://platform.openai.com/api-keys',
|
||||
anthropic: 'https://console.anthropic.com/account/keys',
|
||||
google: 'https://console.cloud.google.com/apis/credentials',
|
||||
azure: 'https://portal.azure.com/#blade/Microsoft_Azure_ProjectOxford/CognitiveServicesHub/OpenAI',
|
||||
stability: 'https://platform.stability.ai/account/keys'
|
||||
}
|
||||
return urls[provider.toLowerCase()] || '#'
|
||||
}
|
||||
|
||||
console.log(provider)
|
||||
return (
|
||||
<Modal
|
||||
title={mode === 'add' ? $t(`添加 ${providerName} APIKey`) : $t('编辑 APIKey')}
|
||||
title={mode === 'add' ? $t(`添加 ${provider?.name} APIKey`) : $t('编辑 APIKey')}
|
||||
open={visible}
|
||||
onCancel={onCancel}
|
||||
onOk={handleOk}
|
||||
destroyOnClose
|
||||
width={600}
|
||||
maskClosable={false}
|
||||
footer={(_, { OkBtn, CancelBtn }) => (
|
||||
<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 />
|
||||
<OkBtn />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item name="id" label={$t('APIKey 名称')} rules={[{ required: true, message: $t('请输入 APIKey') }]}>
|
||||
<Input disabled={mode === 'edit'} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="name"
|
||||
label={
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', width: '100%' }}>
|
||||
<span>{$t('API Key')}</span>
|
||||
{mode === 'add' && (
|
||||
<a href="https://platform.openai.com/api-keys" target="_blank" rel="noopener noreferrer">
|
||||
{$t('从 OpenAI 获取 API Key')}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
rules={[{ required: true, message: $t('请输入 API Key') }]}
|
||||
>
|
||||
{mode === 'add' ? (
|
||||
<TextArea
|
||||
rows={4}
|
||||
placeholder={JSON.stringify(
|
||||
{
|
||||
openai_api_base: 'API Base',
|
||||
openai_api_key: 'API Key'
|
||||
},
|
||||
null,
|
||||
2
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<Input.Password />
|
||||
)}
|
||||
<Form.Item label={$t('API Key')} name="config">
|
||||
<Codebox
|
||||
editorTheme="vs-dark"
|
||||
readOnly={false}
|
||||
width="100%"
|
||||
height="300px"
|
||||
language="json"
|
||||
enableToolbar={false}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="neverExpire" valuePropName="checked">
|
||||
<Switch
|
||||
checkedChildren={$t('永不过期')}
|
||||
unCheckedChildren={$t('设置过期时间')}
|
||||
onChange={handleNeverExpireChange}
|
||||
/>
|
||||
<Form.Item label={$t('过期时间')} name="neverExpire" valuePropName="checked">
|
||||
<div className="flex items-center">
|
||||
<Switch onChange={handleNeverExpireChange} />
|
||||
<span className="ml-2">{neverExpire ? $t('永不过期') : $t('设置过期时间')}</span>
|
||||
</div>
|
||||
</Form.Item>
|
||||
|
||||
{!neverExpire && (
|
||||
|
||||
@@ -5,7 +5,7 @@ import TableBtnWithPermission from '@common/components/aoplatform/TableBtnWithPe
|
||||
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
|
||||
import { useFetch } from '@common/hooks/http'
|
||||
import { $t } from '@common/locales'
|
||||
import AIProviderSelect from '@core/components/AIProviderSelect'
|
||||
import AIProviderSelect, { AIProvider } from '@core/components/AIProviderSelect'
|
||||
import { Divider, message, Space, Typography } from 'antd'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import ApiKeyModal from './components/ApiKeyModal'
|
||||
@@ -13,8 +13,8 @@ import { APIKey } from './types'
|
||||
|
||||
const KeySettings: React.FC = () => {
|
||||
const pageListRef = useRef<ActionType>(null)
|
||||
const [selectedProvider, setSelectedProvider] = useState<string>('openai')
|
||||
const [providerName, setProviderName] = useState<string>('')
|
||||
const [selectedProvider, setSelectedProvider] = useState<string>()
|
||||
const [provider, setProvider] = useState<AIProvider | undefined>()
|
||||
const [modalVisible, setModalVisible] = useState(false)
|
||||
const [modalMode, setModalMode] = useState<'add' | 'edit'>('add')
|
||||
const [editingKey, setEditingKey] = useState<APIKey | null>(null)
|
||||
@@ -24,28 +24,8 @@ const KeySettings: React.FC = () => {
|
||||
|
||||
useEffect(() => {
|
||||
pageListRef.current?.reload()
|
||||
fetchProviderName()
|
||||
}, [selectedProvider])
|
||||
|
||||
const fetchProviderName = async () => {
|
||||
try {
|
||||
const response = await fetchData<{ code: number; data: { providers: { id: string; name: string }[] } }>(
|
||||
'simple/ai/providers',
|
||||
{ method: 'GET' }
|
||||
)
|
||||
if (response.code === STATUS_CODE.SUCCESS) {
|
||||
const provider = response.data.providers.find((p) => p.id === selectedProvider)
|
||||
if (provider) {
|
||||
setProviderName(provider.name)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch provider name:', error)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {}, [])
|
||||
|
||||
const handleEdit = (record: APIKey) => {
|
||||
setEditingKey(record)
|
||||
setModalMode('edit')
|
||||
@@ -172,6 +152,7 @@ const KeySettings: React.FC = () => {
|
||||
}
|
||||
|
||||
const requestApiKeys = async (params: any) => {
|
||||
if (!selectedProvider) return
|
||||
try {
|
||||
const response = await fetchData<BasicResponse<{ data: APIKey[] }>>('ai/resource/keys', {
|
||||
method: 'GET',
|
||||
@@ -310,7 +291,13 @@ const KeySettings: React.FC = () => {
|
||||
<>
|
||||
{$t('支持单个 API 模型供应商下创建多个 APIKey APIKey 进行智能负载均衡')}
|
||||
<div className="mt-4">
|
||||
<AIProviderSelect value={selectedProvider} onChange={setSelectedProvider} />
|
||||
<AIProviderSelect
|
||||
value={selectedProvider}
|
||||
onChange={(value: string, provider: AIProvider) => {
|
||||
setSelectedProvider(value)
|
||||
setProvider(provider)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
@@ -338,16 +325,8 @@ const KeySettings: React.FC = () => {
|
||||
mode={modalMode}
|
||||
onCancel={handleModalCancel}
|
||||
onSave={handleSave}
|
||||
providerName={providerName}
|
||||
initialValues={
|
||||
editingKey
|
||||
? {
|
||||
id: editingKey.id,
|
||||
name: editingKey.name,
|
||||
expire_time: editingKey.expire_time
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
provider={provider}
|
||||
entity={editingKey}
|
||||
defaultKeyNumber={apiKeys.length + 1}
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user