mirror of
https://github.com/APIParkLab/APIPark.git
synced 2026-06-14 20:41:15 +08:00
Merge remote-tracking branch 'github-pro/main' into main-github-pro
# Conflicts: # frontend/packages/common/src/components/aoplatform/LanguageSetting.tsx # frontend/packages/common/src/components/aoplatform/TableBtnWithPermission.tsx # frontend/packages/common/src/components/aoplatform/intelligent-plugin/IntelligentPluginList.tsx # frontend/packages/common/src/components/aoplatform/prompt-editor/PromptEditorResizable.tsx # frontend/packages/common/src/const/permissions.ts # frontend/packages/common/src/contexts/GlobalStateContext.tsx # frontend/packages/common/src/hooks/pluginLoader.ts # frontend/packages/common/src/utils/systemRunning.ts # frontend/packages/core/src/components/AIProviderSelect/index.tsx # frontend/packages/core/src/const/const.tsx # frontend/packages/core/src/const/system/const.tsx # frontend/packages/core/src/pages/aiApis/index.tsx # frontend/packages/core/src/pages/aiSetting/AIFlowChart.tsx # frontend/packages/core/src/pages/aiSetting/AiSettingList.tsx # frontend/packages/core/src/pages/aiSetting/AiSettingModal.tsx # frontend/packages/core/src/pages/aiSetting/components/CustomEdge.tsx # frontend/packages/core/src/pages/aiSetting/components/KeyStatusNode.tsx # frontend/packages/core/src/pages/aiSetting/components/ModelCardNode.tsx # frontend/packages/core/src/pages/aiSetting/components/ServiceCardNode.tsx # frontend/packages/core/src/pages/aiSetting/constants.ts # frontend/packages/core/src/pages/aiSetting/styles.css # frontend/packages/core/src/pages/aiSetting/types.ts # frontend/packages/core/src/pages/keySettings/components/ApiKeyContent.tsx # frontend/packages/core/src/pages/keySettings/components/StatusFilter.tsx # frontend/packages/core/src/pages/keySettings/index.tsx # frontend/packages/dashboard/src/component/MonitorApiPage.tsx
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
import { Dropdown, Button } from 'antd'
|
||||
import i18n from '@common/locales'
|
||||
import { memo, useEffect, useMemo } from 'react'
|
||||
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
|
||||
import i18n from '@common/locales'
|
||||
import { Icon } from '@iconify/react/dist/iconify.js'
|
||||
import { Button, Dropdown } from 'antd'
|
||||
import { memo, useEffect, useMemo } from 'react'
|
||||
|
||||
const LanguageSetting = ({ mode = 'light' }: { mode?: 'dark' | 'light' }) => {
|
||||
const { dispatch, state } = useGlobalContext()
|
||||
@@ -48,12 +48,17 @@ const LanguageSetting = ({ mode = 'light' }: { mode?: 'dark' | 'light' }) => {
|
||||
const langLabel = useMemo(() => items.find((item) => item?.key === state.language)?.title, [state.language])
|
||||
|
||||
useEffect(() => {
|
||||
const savedLang = sessionStorage.getItem('i18nextLng')
|
||||
const browserLang = navigator.language || navigator.userLanguage
|
||||
if (savedLang) return
|
||||
|
||||
dispatch({ type: 'UPDATE_LANGUAGE', language: browserLang })
|
||||
const savedLang = i18n.language || sessionStorage.getItem('i18nextLng')
|
||||
if (savedLang && state.language !== savedLang) {
|
||||
dispatch({ type: 'UPDATE_LANGUAGE', language: savedLang })
|
||||
} else if (!savedLang) {
|
||||
const browserLang = navigator.language
|
||||
const supportedLang = items.find((item) => item.key === browserLang) ? browserLang : 'zh-CN'
|
||||
dispatch({ type: 'UPDATE_LANGUAGE', language: supportedLang })
|
||||
i18n.changeLanguage(supportedLang)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
trigger={['hover']}
|
||||
@@ -64,6 +69,7 @@ const LanguageSetting = ({ mode = 'light' }: { mode?: 'dark' | 'light' }) => {
|
||||
const { key } = e
|
||||
dispatch({ type: 'UPDATE_LANGUAGE', language: key })
|
||||
i18n.changeLanguage(key)
|
||||
sessionStorage.setItem('i18nextLng', key)
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Button, Tooltip } from 'antd'
|
||||
import { useState, useMemo, useEffect, useCallback } from 'react'
|
||||
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { PERMISSION_DEFINITION } from '@common/const/permissions'
|
||||
import { Icon } from '@iconify/react/dist/iconify.js'
|
||||
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
|
||||
import { $t } from '@common/locales'
|
||||
import { Icon } from '@iconify/react/dist/iconify.js'
|
||||
import { Button, Tooltip } from 'antd'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
type TableBtnWithPermissionProps = {
|
||||
btnTitle: string
|
||||
@@ -25,6 +25,7 @@ const TableIconName = {
|
||||
copy: 'ic:baseline-file-copy',
|
||||
view: 'ic:baseline-remove-red-eye',
|
||||
publish: 'ic:baseline-publish',
|
||||
offline: 'ic:baseline-file-download-off',
|
||||
approval: 'ic:baseline-approval',
|
||||
stop: 'ic:baseline-stop-circle',
|
||||
online: 'ic:baseline-check-circle',
|
||||
@@ -86,7 +87,7 @@ const TableBtnWithPermission = ({
|
||||
<Button
|
||||
type="text"
|
||||
disabled={true}
|
||||
className={`h-[22px] border-none p-0 flex items-center bg-transparent ${className}`}
|
||||
className={`flex items-center p-0 bg-transparent border-none h-[22px] ${className}`}
|
||||
key={btnType}
|
||||
icon={<Icon icon={TableIconName[btnType as keyof typeof TableIconName]} width="18" height="18" />}
|
||||
>
|
||||
@@ -104,7 +105,7 @@ const TableBtnWithPermission = ({
|
||||
<Button
|
||||
type="text"
|
||||
disabled={disabled}
|
||||
className={`h-[22px] border-none p-0 flex items-center bg-transparent ${className} `}
|
||||
className={`flex items-center p-0 bg-transparent border-none h-[22px] ${className}`}
|
||||
key={btnType}
|
||||
icon={<Icon icon={TableIconName[btnType as keyof typeof TableIconName]} width="18" height="18" />}
|
||||
onClick={handleClick}
|
||||
|
||||
+17
-17
@@ -1,21 +1,21 @@
|
||||
import { LoadingOutlined } from '@ant-design/icons'
|
||||
import { ActionType, ParamsType } from '@ant-design/pro-components'
|
||||
import { DrawerWithFooter } from '@common/components/aoplatform/DrawerWithFooter.tsx'
|
||||
import PageList, { PageProColumns } from '@common/components/aoplatform/PageList.tsx'
|
||||
import TableBtnWithPermission from '@common/components/aoplatform/TableBtnWithPermission.tsx'
|
||||
import WithPermission from '@common/components/aoplatform/WithPermission.tsx'
|
||||
import { BasicResponse, COLUMNS_TITLE, DELETE_TIPS, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const.tsx'
|
||||
import { EntityItem } from '@common/const/type.ts'
|
||||
import { useBreadcrumb } from '@common/contexts/BreadcrumbContext.tsx'
|
||||
import { useGlobalContext } from '@common/contexts/GlobalStateContext.tsx'
|
||||
import { useFetch } from '@common/hooks/http.ts'
|
||||
import { $t } from '@common/locales/index.ts'
|
||||
import { RouterParams } from '@core/components/aoplatform/RenderRoutes.tsx'
|
||||
import { App, Divider, Spin } from 'antd'
|
||||
import { DefaultOptionType } from 'antd/es/cascader'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useLocation, useOutletContext, useParams } from 'react-router-dom'
|
||||
import { useBreadcrumb } from '@common/contexts/BreadcrumbContext.tsx'
|
||||
import { ActionType, ParamsType } from '@ant-design/pro-components'
|
||||
import { RouterParams } from '@core/components/aoplatform/RenderRoutes.tsx'
|
||||
import { DefaultOptionType } from 'antd/es/cascader'
|
||||
import { IntelligentPluginConfig, IntelligentPluginConfigHandle } from './IntelligentPluginConfig.tsx'
|
||||
import { BasicResponse, COLUMNS_TITLE, DELETE_TIPS, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const.tsx'
|
||||
import { useFetch } from '@common/hooks/http.ts'
|
||||
import { EntityItem } from '@common/const/type.ts'
|
||||
import WithPermission from '@common/components/aoplatform/WithPermission.tsx'
|
||||
import TableBtnWithPermission from '@common/components/aoplatform/TableBtnWithPermission.tsx'
|
||||
import { DrawerWithFooter } from '@common/components/aoplatform/DrawerWithFooter.tsx'
|
||||
import { LoadingOutlined } from '@ant-design/icons'
|
||||
import { $t } from '@common/locales/index.ts'
|
||||
import { useGlobalContext } from '@common/contexts/GlobalStateContext.tsx'
|
||||
|
||||
type DynamicTableField = {
|
||||
name: string
|
||||
@@ -218,8 +218,8 @@ export default function IntelligentPluginList() {
|
||||
render: (_: React.ReactNode, entity: DynamicTableItem) => [
|
||||
<TableBtnWithPermission
|
||||
access={`${accessPrefix}.publish`}
|
||||
key="publish"
|
||||
btnType="publish"
|
||||
key={entity.status === $t('已发布') ? 'offline' : 'publish'}
|
||||
btnType={entity.status === $t('已发布') ? 'offline' : 'publish'}
|
||||
onClick={() => {
|
||||
openModal('publish', entity)
|
||||
}}
|
||||
@@ -233,7 +233,7 @@ export default function IntelligentPluginList() {
|
||||
onClick={() => {
|
||||
openDrawer('edit', entity)
|
||||
}}
|
||||
btnTitle={$t('查看')}
|
||||
btnTitle={$t('查看 ')}
|
||||
/>,
|
||||
<Divider type="vertical" className="mx-0" key="div2" />,
|
||||
<TableBtnWithPermission
|
||||
@@ -322,6 +322,7 @@ export default function IntelligentPluginList() {
|
||||
const { code, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
message.success(msg || $t(RESPONSE_TIPS.success))
|
||||
manualReloadTable()
|
||||
return Promise.resolve(true)
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
@@ -329,7 +330,6 @@ export default function IntelligentPluginList() {
|
||||
}
|
||||
})
|
||||
.catch((errorInfo) => Promise.reject(errorInfo))
|
||||
message.destroy()
|
||||
return
|
||||
}
|
||||
case 'delete':
|
||||
|
||||
+3
-2
@@ -9,8 +9,9 @@ const PromptEditorResizable = (props: {
|
||||
onChange?: (value: string) => void
|
||||
variablesChange?: (keys: string[]) => void
|
||||
promptVariables: VariableItems[]
|
||||
disabled?: boolean
|
||||
}) => {
|
||||
const { value, onChange, variablesChange, promptVariables } = props
|
||||
const { value, onChange, variablesChange, promptVariables, disabled } = props
|
||||
const minHeight = 68
|
||||
const [editorHeight, setEditorHeight] = useState(minHeight)
|
||||
const [previousKeys, setPreviousKeys] = useState<string[]>([])
|
||||
@@ -82,7 +83,7 @@ const PromptEditorResizable = (props: {
|
||||
setPreviousKeys(keys)
|
||||
}
|
||||
}}
|
||||
editable={true}
|
||||
editable={disabled ? false : true}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -219,22 +219,22 @@ export const PERMISSION_DEFINITION = [
|
||||
anyOf: [{ backend: ['system.settings.log_configuration.view'] }]
|
||||
}
|
||||
},
|
||||
'system.devops.log_configuration.add': {
|
||||
'system.settings.log_configuration.add': {
|
||||
granted: {
|
||||
anyOf: [{ backend: ['system.settings.log_configuration.manager'] }]
|
||||
}
|
||||
},
|
||||
'system.devops.log_configuration.edit': {
|
||||
'system.settings.log_configuration.edit': {
|
||||
granted: {
|
||||
anyOf: [{ backend: ['system.settings.log_configuration.manager'] }]
|
||||
}
|
||||
},
|
||||
'system.devops.log_configuration.publish': {
|
||||
'system.settings.log_configuration.publish': {
|
||||
granted: {
|
||||
anyOf: [{ backend: ['system.settings.log_configuration.manager'] }]
|
||||
}
|
||||
},
|
||||
'system.devops.log_configuration.delete': {
|
||||
'system.settings.log_configuration.delete': {
|
||||
granted: {
|
||||
anyOf: [{ backend: ['system.settings.log_configuration.manager'] }]
|
||||
}
|
||||
@@ -674,10 +674,21 @@ export const PERMISSION_DEFINITION = [
|
||||
anyOf: [{ backend: ['project.permission_manager'] }]
|
||||
}
|
||||
},
|
||||
'system.settings.ai_key_resource.view': {
|
||||
granted: {
|
||||
anyOf: [{ backend: ['system.settings.ai_key_resource.view'] }]
|
||||
}
|
||||
},
|
||||
'system.settings.ai_key_resource.manager': {
|
||||
granted: {
|
||||
anyOf: [{ backend: ['system.settings.ai_key_resource.manager'] }]
|
||||
}
|
||||
},
|
||||
|
||||
'system.settings.ai_api.view': {
|
||||
granted: {
|
||||
anyOf: [{ backend: ['system.settings.ai_api.view'] }]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -151,15 +151,15 @@ const mockData = [
|
||||
name: 'APIKey 资源池',
|
||||
key: 'aiKeys',
|
||||
path: '/keysetting',
|
||||
icon: 'ic:baseline-key'
|
||||
// access: 'system.settings.ai_key_resource.view'
|
||||
icon: 'ic:baseline-key',
|
||||
access: 'system.settings.ai_key_resource.view'
|
||||
},
|
||||
{
|
||||
name: 'AI API',
|
||||
key: 'aiApiList',
|
||||
path: '/aiapis',
|
||||
icon: 'ic:baseline-api'
|
||||
// access: 'system.settings.ai_api.view'
|
||||
path: '/aiApis',
|
||||
icon: 'ic:baseline-api',
|
||||
access: 'system.settings.ai_api.view'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -92,10 +92,10 @@ const mockData = {
|
||||
},
|
||||
{
|
||||
driver: 'apipark.builtIn.component',
|
||||
name: 'aiapis',
|
||||
name: 'aiApis',
|
||||
router: [
|
||||
{
|
||||
path: 'aiapis',
|
||||
path: 'aiApis',
|
||||
type: 'normal'
|
||||
}
|
||||
]
|
||||
|
||||
@@ -45,8 +45,6 @@
|
||||
"成功": "K43fcaf94",
|
||||
"上线失败": "Kc71c6a9",
|
||||
"失败": "K56c686f8",
|
||||
"正常": "Ke039b9b5",
|
||||
"无效": "K1da86266",
|
||||
"申请系统": "K1ff96ff",
|
||||
"所属团队": "K9bf855d6",
|
||||
"申请人": "K11b994ed",
|
||||
@@ -239,6 +237,10 @@
|
||||
"请输入IP地址或CIDR范围,每条以换行分割": "K49731763",
|
||||
"待更新": "K3a34d49b",
|
||||
"待删除": "Kd2850420",
|
||||
"内容": "K3e7aa0ad",
|
||||
"调用地址": "K2f5fdf5e",
|
||||
"消费者 IP": "K1bc5e0a3",
|
||||
"鉴权名称": "K6f39ea21",
|
||||
"暂无操作权限,请联系管理员分配。": "K23fda291",
|
||||
"微信小程序": "K4618cb0a",
|
||||
"获取文件,需填路径": "Ka854f511",
|
||||
@@ -324,12 +326,25 @@
|
||||
"响应 Body": "Kd2be51d1",
|
||||
"默认工作表": "K2a3f24ac",
|
||||
"至": "K7e1ab4b0",
|
||||
"详情": "Kf1b166e7",
|
||||
"暂不支持带有双斜杠//的url": "K28555332",
|
||||
"输入的IP或CIDR不符合格式": "K83237c89",
|
||||
"请正确输入路径,如/usr/*或*/usr/*": "K5ae2c87a",
|
||||
"必填项": "K71661ee8",
|
||||
"不是有效邮箱地址": "Kcbee3f8",
|
||||
"获取 AI providers 失败": "K94b48734",
|
||||
"AI 供应商": "Kf23a8988",
|
||||
"AI 服务": "Kd2c34e2c",
|
||||
"模型": "Kfede1c7c",
|
||||
"已用 Token": "K89f135a7",
|
||||
"拦截": "Kb7df6ac1",
|
||||
"放行": "K5c1722fe",
|
||||
"编辑时间": "K1acc30b2",
|
||||
"预览": "K4d81a657",
|
||||
"查看详情": "K35f990b0",
|
||||
"AI API 列表": "K91144ebd",
|
||||
"重置": "K50d471b2",
|
||||
"查询": "Kee8ae330",
|
||||
"请输入 APIURL 搜索": "Kf8187c33",
|
||||
"最近一次更新者": "K617f34f1",
|
||||
"最近一次更新时间": "K6ebca204",
|
||||
"保存": "Kabfe9512",
|
||||
@@ -357,18 +372,18 @@
|
||||
"变量": "K13ffbe88",
|
||||
"输入这个接口的描述": "K79c8cfaf",
|
||||
"重试次数": "K469e475a",
|
||||
"拦截接口": "Kee4139c2",
|
||||
"开启拦截后,网关会拦截所有该路径的请求。": "K3e38ea",
|
||||
"模型配置": "K8a35059b",
|
||||
"路由": "Kf9dcef3a",
|
||||
"添加路由": "K6134bbe8",
|
||||
"输入 URL 查找路由": "Kf85b83a0",
|
||||
"模型供应商": "Kcf9f90b8",
|
||||
"模型": "Kfede1c7c",
|
||||
"参数": "Ke99513a0",
|
||||
"审核": "Kb595f40",
|
||||
"通过": "K54e27f57",
|
||||
"拒绝": "K8582af3f",
|
||||
"发布结果": "Kd568e15c",
|
||||
"查看详情": "K35f990b0",
|
||||
"申请发布": "Kdbc1f6cb",
|
||||
"回滚": "Kb6860a3f",
|
||||
"请确认是否回滚?": "Ka3494f4b",
|
||||
@@ -377,13 +392,31 @@
|
||||
"终止发布": "Ke1b79b93",
|
||||
"请确认是否终止发布?": "Ka2449180",
|
||||
"新建版本": "K2cb02f38",
|
||||
"从 (0) 获取 API KEY": "Kb3e34847",
|
||||
"未配置 AI 模型": "Kd752a3a8",
|
||||
"前往设置": "K8b7ac871",
|
||||
"AI 模型": "K99935e6f",
|
||||
"配置好 AI 模型后,你可以使用对应的大模型来创建 AI 服务": "K2260837a",
|
||||
"已设置": "Kf97448b3",
|
||||
"未设置": "K30d4d8df",
|
||||
"保存后供应商状态变为【停用】,使用本供应商的 API 将临时使用负载优先级最高的正常供应商。": "Ke32702ac",
|
||||
"保存后供应商状态变为【正常】,恢复调用本供应商的 AI 能力。": "Ka08c28d4",
|
||||
"默认模型": "Kc2ee5223",
|
||||
"负载优先级": "K608af899",
|
||||
"负载优先级决定在原供应商异常或停用后,优先使用哪一个供应商。优先级数字越小,优先级越高。": "K65b7a96",
|
||||
"优先级必须大于 0": "K9eccff16",
|
||||
"请输入优先级": "Kfcf02780",
|
||||
"API Key(默认 Key)": "K5c6dcf58",
|
||||
"LLM 状态管理": "K59bf8ed9",
|
||||
"当前调用状态:": "Kab8fe398",
|
||||
"正常": "Ke039b9b5",
|
||||
"停用": "Kedd64e4d",
|
||||
"异常": "K23a3bd72",
|
||||
"启用": "K52c8a730",
|
||||
"已配置": "K66a7d24c",
|
||||
"未配置": "Kaf074220",
|
||||
"默认": "Kd9a46c29",
|
||||
"AI 模型管理": "K7ac2be34",
|
||||
"配置好 AI 模型后,你可以使用对应的大模型来创建 AI 服务": "K2260837a",
|
||||
"同步最新模型": "K18dccc1a",
|
||||
"默认:": "K2a3aeb8d",
|
||||
"从 (0) 获取 API KEY": "Kb3e34847",
|
||||
"待审核": "K35612f29",
|
||||
"已审核": "K47eaafde",
|
||||
"发布申请": "K56b4254f",
|
||||
@@ -437,6 +470,28 @@
|
||||
"了解 APIPark 如何更好地管理 API 和 AI": "K1afaf20e",
|
||||
"了解更多功能": "K48f7e21f",
|
||||
"隐藏该教程": "K698296e2",
|
||||
"请输入 APIKey": "K8b88ef63",
|
||||
"API Key": "Kcbd30819",
|
||||
"请填写 APIKey": "Kcb6e2d3e",
|
||||
"永不过期": "K9dfa2c97",
|
||||
"设置过期时间": "Ke13e332a",
|
||||
"请选择过期时间": "K409aa8ba",
|
||||
"超额": "Kba69594c",
|
||||
"过期": "Kb9e7ceda",
|
||||
"错误": "Kac405b50",
|
||||
"请选择状态": "K3fde5b49",
|
||||
"添加 (0) APIKey": "K4880fd04",
|
||||
"编辑 APIKey": "K434b7e76",
|
||||
"删除成功": "K28190dbc",
|
||||
"停用成功": "Kb5fcf5b8",
|
||||
"启用成功": "K5940d788",
|
||||
"排序成功": "K8743bccd",
|
||||
"编辑": "Kad207008",
|
||||
"调用优先级": "K19590c2c",
|
||||
"APIKey 资源池": "Kefb03657",
|
||||
"支持单个 API 模型供应商下创建多个 APIKey APIKey 进行智能负载均衡": "Kc0352e64",
|
||||
"请输入名称搜索": "Kd25acba1",
|
||||
"添加 APIKey": "K6d0388a0",
|
||||
"请输入账号": "Kf076f63c",
|
||||
"账号": "K80a560a1",
|
||||
"请输入密码": "K25c895d5",
|
||||
@@ -460,7 +515,6 @@
|
||||
"加入部门": "Ke6f00b44",
|
||||
"确定删除成员?此操作无法恢复,确认操作?": "K501cb1e7",
|
||||
"成员与部门": "Kf20863b5",
|
||||
"启用": "K52c8a730",
|
||||
"禁用": "K718c9310",
|
||||
"输入用户名、邮箱查找成员": "K5f27a546",
|
||||
"移出当前部门": "K7c97c5df",
|
||||
@@ -473,10 +527,13 @@
|
||||
"成员": "K74aef1ad",
|
||||
"设置成员和对应的角色,成员只能够看到权限范围内的功能和数据。": "K3f1077c9",
|
||||
"搜索部门": "Kdce62a6",
|
||||
"操作成功,即将刷新页面": "K9ada4366",
|
||||
"数据源类型": "Ka46b9b24",
|
||||
"数据源地址": "Kbb0cdcd0",
|
||||
"Organization": "Kd9dfb884",
|
||||
"鉴权 Token": "K3e770a75",
|
||||
"请求前缀": "K9b332ab1",
|
||||
"HTTP 头部": "K3d78d483",
|
||||
"密钥": "K8ef69ee2",
|
||||
"上传密钥": "Kba3507d6",
|
||||
"密钥文件的后缀名一般为 .key 的文件内容": "K93ac0f23",
|
||||
@@ -490,7 +547,6 @@
|
||||
"集群": "Ke93d36ed",
|
||||
"修改配置": "K877985b7",
|
||||
"设置访问 API 的集群,让 API 在分布式环境中稳定运行,并且能够根据业务需求进行灵活扩展和优化。": "Kdf66a675",
|
||||
"异常": "K23a3bd72",
|
||||
"私有网络": "Ke1b1865",
|
||||
"公共网络": "K4786c57c",
|
||||
"管理地址": "Kf12b3034",
|
||||
@@ -501,14 +557,19 @@
|
||||
"数据源": "K8fa58214",
|
||||
"设置监控报表的数据来源,设置完成之后即可获得详细的API调用统计图表。": "Kdbafd6f9",
|
||||
"统计图表": "K1358acf",
|
||||
"数据日志": "K17dc3a62",
|
||||
"地址(IP:端口)": "K62dabdf6",
|
||||
"组织(Organization)": "K2db12335",
|
||||
"添加策略": "K34d0d409",
|
||||
"输入名称、筛选条件查找": "Kbb4298ac",
|
||||
"处理日志": "Ke429194e",
|
||||
"脱敏前": "K8c34c02f",
|
||||
"脱敏后": "K8e3d388d",
|
||||
"编辑策略": "Kc82b8374",
|
||||
"策略类型": "K4b34a5e5",
|
||||
"匹配条件": "K57f0fee8",
|
||||
"数据脱敏规则": "K10650c58",
|
||||
"输入调用地址、消费者IP和消费者条件查找": "K84ffb1dd",
|
||||
"配置脱敏规则": "K1b34a9ab",
|
||||
"匹配值": "K26d22405",
|
||||
"脱敏类型": "K1546e1fe",
|
||||
@@ -520,7 +581,6 @@
|
||||
"脱敏规则": "K4cd91d61",
|
||||
"自定义字符串; 值:": "K8dcad979",
|
||||
"起始位置:(0)位;长度:(1)位": "K82e3f7b7",
|
||||
"编辑": "Kad207008",
|
||||
"已选择(0)项(1)数据": "K49dfc123",
|
||||
"所有(0)": "K8457ea34",
|
||||
"属性名称": "K7ca9a795",
|
||||
@@ -543,13 +603,9 @@
|
||||
"请求协议": "K6bc47edb",
|
||||
"请求方式": "K1365fe45",
|
||||
"转发规则设置": "K90f3c02f",
|
||||
"拦截": "Kb7df6ac1",
|
||||
"放行": "K5c1722fe",
|
||||
"路由详情": "K28435c5c",
|
||||
"只允许上传PNG、JPG或SVG格式的图片": "Ka9c08390",
|
||||
"服务名称": "K413b9869",
|
||||
"服务类型": "K9919285b",
|
||||
"AI 服务": "Kd2c34e2c",
|
||||
"REST 服务": "K62840d62",
|
||||
"默认 AI 供应商": "Kcef64f4d",
|
||||
"创建 API 时会默认选择该供应商,修改默认供应商不会影响现有 API": "K300c89d4",
|
||||
@@ -604,8 +660,6 @@
|
||||
"选择API": "Kcc8265e1",
|
||||
"路径": "Kc380335f",
|
||||
"请输入请求路径进行搜索": "K8aefc1e4",
|
||||
"重置": "K50d471b2",
|
||||
"查询": "Kee8ae330",
|
||||
"导出": "Ka2c794a2",
|
||||
"退出全屏": "Kaf70c3b",
|
||||
"(0)调用详情": "Kd22841a4",
|
||||
@@ -695,7 +749,6 @@
|
||||
"修改": "K9cbe1e0",
|
||||
"访问授权": "Kb6e9328f",
|
||||
"添加授权": "Kd23d1716",
|
||||
"永不过期": "K9dfa2c97",
|
||||
"到期时间": "Kfa920c0",
|
||||
"订阅的服务": "Kcce1af60",
|
||||
"审核详情": "Kfefa9b58",
|
||||
@@ -710,7 +763,6 @@
|
||||
"创建并管理自己的消费者实体,每个消费者可以订阅多个API服务,确保在调用之前已获得相应权限。你可以为消费者生成 API 密钥等鉴权方式,用于安全地调用 API 服务": "K5c4e2865",
|
||||
"订阅的服务数量:已通过 (0) 个,申请中 (1) 个": "K3c7b175f",
|
||||
"输入名称、ID 查找消费者": "K3a6f905d",
|
||||
"退出测试": "Kbe3e9335",
|
||||
"服务市场": "K370a3eb2",
|
||||
"服务详情": "Kf7ec36d",
|
||||
"申请服务": "K58ca9485",
|
||||
|
||||
@@ -704,64 +704,109 @@
|
||||
"Kbdec9fa": "Filter Criteria",
|
||||
"Kbcbb7391": "Processed Count",
|
||||
"Kad207008": "Edit",
|
||||
"K630c9e6d": "APIPark",
|
||||
"Ka3e9f580": "Release Name",
|
||||
"Kb2480682": "Policy List",
|
||||
"K118d8d74": "Data Format",
|
||||
"Kfe7c7d2d": "Keyword",
|
||||
"K2f57a694": "Regular Expression",
|
||||
"K8953e0a6": "Mobile Number",
|
||||
"K6f86a038": "ID Card Number",
|
||||
"K7954e7c8": "Bank Card Number",
|
||||
"K320fdb17": "Amount",
|
||||
"K7867acda": "Date",
|
||||
"K7d327ae8": "Partial Display",
|
||||
"Kfbf38e3c": "Partial Masking",
|
||||
"Kd8c1fbb0": "Truncate",
|
||||
"K89829921": "Replace",
|
||||
"K480a7165": "Shuffle",
|
||||
"Kea0d69df": "Random String",
|
||||
"Ke7c84d1d": "Custom String",
|
||||
"K49731763": "Please enter IP address or CIDR range, each separated by a newline.",
|
||||
"K83237c89": "The entered IP or CIDR does not meet the format.",
|
||||
"K5ae2c87a": "Please enter the correct path, such as /usr/* or */usr/*.",
|
||||
"Kc82b8374": "Edit Policy",
|
||||
"K4b34a5e5": "Policy Type",
|
||||
"K57f0fee8": "Match Conditions",
|
||||
"K10650c58": "Data Masking Rules",
|
||||
"K1b34a9ab": "Configure Masking Rules",
|
||||
"K26d22405": "Match Value",
|
||||
"K1546e1fe": "Masking Type",
|
||||
"K9b9b0629": "Starting Position",
|
||||
"K52c84fe1": "Length",
|
||||
"Kde84409c": "Replace Type",
|
||||
"K338653b4": "Replace Value",
|
||||
"Kbaeed3b7": "JSON Path",
|
||||
"K4cd91d61": "Masking Rules",
|
||||
"K8dcad979": "Custom String; Value:",
|
||||
"K82e3f7b7": "Starting Position: (0); Length: (1)",
|
||||
"K49dfc123": "Selected (0) items (1) data",
|
||||
"K8457ea34": "All (0)",
|
||||
"K7ca9a795": "Attribute Name",
|
||||
"Kc4391744": "Attribute Value",
|
||||
"K678e13fc": "Configure (0)",
|
||||
"K508d8bf4": "Integration Address",
|
||||
"K67f4e9bb": "The domain name for obtaining API market documentation information when integrating with external platforms",
|
||||
"K1da86266": "Invalid",
|
||||
"K3a34d49b": "Pending Update",
|
||||
"Kd2850420": "Pending Deletion",
|
||||
"K9ada4366": "Operation successful, the page will refresh shortly",
|
||||
"K9b332ab1": "Request prefix",
|
||||
"K3d78d483": "HTTP headers",
|
||||
"K17dc3a62": "Data logs",
|
||||
"Ke429194e": "Processing logs",
|
||||
"K84ffb1dd": "Enter the invocation address, consumer IP, and consumer condition to search",
|
||||
"Kb147fabc": "Create",
|
||||
"K40ca4f2": "Update",
|
||||
"K3e7aa0ad": "Content",
|
||||
"K2f5fdf5e": "Call Address",
|
||||
"K1bc5e0a3": "Consumer IP",
|
||||
"K6f39ea21": "Authentication Name",
|
||||
"K8c34c02f": "Before Masking",
|
||||
"K8e3d388d": "After Masking"
|
||||
"K630c9e6d": "APIPark",
|
||||
"Ka3e9f580": "Release Name",
|
||||
"Kb2480682": "Policy List",
|
||||
"K118d8d74": "Data Format",
|
||||
"Kfe7c7d2d": "Keyword",
|
||||
"K2f57a694": "Regular Expression",
|
||||
"K8953e0a6": "Mobile Number",
|
||||
"K6f86a038": "ID Card Number",
|
||||
"K7954e7c8": "Bank Card Number",
|
||||
"K320fdb17": "Amount",
|
||||
"K7867acda": "Date",
|
||||
"K7d327ae8": "Partial Display",
|
||||
"Kfbf38e3c": "Partial Masking",
|
||||
"Kd8c1fbb0": "Truncate",
|
||||
"K89829921": "Replace",
|
||||
"K480a7165": "Shuffle",
|
||||
"Kea0d69df": "Random String",
|
||||
"Ke7c84d1d": "Custom String",
|
||||
"K49731763": "Please enter IP address or CIDR range, each separated by a newline.",
|
||||
"K83237c89": "The entered IP or CIDR does not meet the format.",
|
||||
"K5ae2c87a": "Please enter the correct path, such as /usr/* or */usr/*.",
|
||||
"Kc82b8374": "Edit Policy",
|
||||
"K4b34a5e5": "Policy Type",
|
||||
"K57f0fee8": "Match Conditions",
|
||||
"K10650c58": "Data Masking Rules",
|
||||
"K1b34a9ab": "Configure Masking Rules",
|
||||
"K26d22405": "Match Value",
|
||||
"K1546e1fe": "Masking Type",
|
||||
"K9b9b0629": "Starting Position",
|
||||
"K52c84fe1": "Length",
|
||||
"Kde84409c": "Replace Type",
|
||||
"K338653b4": "Replace Value",
|
||||
"Kbaeed3b7": "JSON Path",
|
||||
"K4cd91d61": "Masking Rules",
|
||||
"K8dcad979": "Custom String; Value:",
|
||||
"K82e3f7b7": "Starting Position: (0); Length: (1)",
|
||||
"K49dfc123": "Selected (0) items (1) data",
|
||||
"K8457ea34": "All (0)",
|
||||
"K7ca9a795": "Attribute Name",
|
||||
"Kc4391744": "Attribute Value",
|
||||
"K678e13fc": "Configure (0)",
|
||||
"K508d8bf4": "Integration Address",
|
||||
"K67f4e9bb": "The domain name for obtaining API market documentation information when integrating with external platforms",
|
||||
"K1da86266": "Invalid",
|
||||
"K3a34d49b": "Pending Update",
|
||||
"Kd2850420": "Pending Deletion",
|
||||
"K9ada4366": "Operation successful, the page will refresh shortly",
|
||||
"K9b332ab1": "Request prefix",
|
||||
"K3d78d483": "HTTP headers",
|
||||
"K17dc3a62": "Data logs",
|
||||
"Ke429194e": "Processing logs",
|
||||
"K84ffb1dd": "Enter the invocation address, consumer IP, and consumer condition to search",
|
||||
"Kb147fabc": "Create",
|
||||
"K40ca4f2": "Update",
|
||||
"K3e7aa0ad": "Content",
|
||||
"K2f5fdf5e": "Call Address",
|
||||
"K1bc5e0a3": "Consumer IP",
|
||||
"K6f39ea21": "Authentication Name",
|
||||
"K8c34c02f": "Before Masking",
|
||||
"K8e3d388d": "After Masking",
|
||||
"K94b48734": "Failed to get AI providers",
|
||||
"Kf23a8988": "AI Provider",
|
||||
"K4d81a657": "Preview",
|
||||
"K91144ebd": "AI API List",
|
||||
"Kf8187c33": "Please enter API URL to search",
|
||||
"Kee4139c2": "Intercept Interface",
|
||||
"K3e38ea": "After enabling interception, the gateway will intercept all requests on this path.",
|
||||
"Kd752a3a8": "AI Model Not Configured",
|
||||
"K8b7ac871": "Go to Settings",
|
||||
"Kf97448b3": "Configured",
|
||||
"K30d4d8df": "Not Configured",
|
||||
"Kc2ee5223": "Default Model",
|
||||
"K608af899": "Load Priority",
|
||||
"K65b7a96": "Load priority determines which provider to use first when the original provider is abnormal or disabled. The smaller the priority number, the higher the priority.",
|
||||
"K9eccff16": "Priority must be greater than 0",
|
||||
"Kfcf02780": "Please enter priority",
|
||||
"K5c6dcf58": "API Key (Default Key)",
|
||||
"K59bf8ed9": "LLM Status Management",
|
||||
"Kedd64e4d": "Disable",
|
||||
"K2a3aeb8d": "Default:",
|
||||
"K8b88ef63": "Please enter APIKey",
|
||||
"Kcbd30819": "API Key",
|
||||
"Kcb6e2d3e": "Please fill in APIKey",
|
||||
"Ke13e332a": "Set expiration time",
|
||||
"K409aa8ba": "Please select expiration time",
|
||||
"Kba69594c": "Overage",
|
||||
"Kb9e7ceda": "Expired",
|
||||
"Kac405b50": "Error",
|
||||
"K3fde5b49": "Please select status",
|
||||
"K434b7e76": "Edit APIKey",
|
||||
"K28190dbc": "Deleted successfully",
|
||||
"Kb5fcf5b8": "Disabled successfully",
|
||||
"K5940d788": "Enabled successfully",
|
||||
"K8743bccd": "Sorted successfully",
|
||||
"K19590c2c": "Call Priority",
|
||||
"K89f135a7": "Used Token",
|
||||
"K1acc30b2": "Edit Time",
|
||||
"Kefb03657": "APIKey Resource Pool",
|
||||
"Kc0352e64": "Supports creating multiple APIKeys under a single API model provider for intelligent load balancing",
|
||||
"Kd25acba1": "Please enter name to search",
|
||||
"K6d0388a0": "Add APIKey",
|
||||
"Ke32702ac": "After saving, the supplier status will become [Disabled]. APIs using this supplier will temporarily use the normal supplier with the highest load priority.",
|
||||
"Ka08c28d4": "After saving, the supplier status will become [Normal], restoring the AI capabilities of this supplier.",
|
||||
"Kab8fe398": "Current Call Status:",
|
||||
"K4880fd04": "Add (0) APIKey"
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1 +1,6 @@
|
||||
{}
|
||||
{
|
||||
"Ke32702ac": "保存后供应商状态变为【停用】,使用本供应商的 API 将临时使用负载优先级最高的正常供应商。",
|
||||
"Ka08c28d4": "保存后供应商状态变为【正常】,恢复调用本供应商的 AI 能力。",
|
||||
"Kab8fe398": "当前调用状态:",
|
||||
"K4880fd04": "添加 (0) APIKey"
|
||||
}
|
||||
@@ -1 +1,6 @@
|
||||
{}
|
||||
{
|
||||
"Ke32702ac": "保存后供应商状态变为【停用】,使用本供应商的 API 将临时使用负载优先级最高的正常供应商。",
|
||||
"Ka08c28d4": "保存后供应商状态变为【正常】,恢复调用本供应商的 AI 能力。",
|
||||
"Kab8fe398": "当前调用状态:",
|
||||
"K4880fd04": "添加 (0) APIKey"
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
"K630c9e6d": "APIPark",
|
||||
"Ka3e9f580": "发布名称",
|
||||
"Kb2480682": "策略列表",
|
||||
"K1da86266": "无效",
|
||||
"K76036e25": "HTTP 请求头",
|
||||
"K44607e3f": "全等匹配",
|
||||
"Kc287500a": "前缀匹配",
|
||||
@@ -45,8 +44,12 @@
|
||||
"Kd2850420": "待删除",
|
||||
"K83237c89": "输入的IP或CIDR不符合格式",
|
||||
"K5ae2c87a": "请正确输入路径,如/usr/*或*/usr/*",
|
||||
"Ke32702ac": "保存后供应商状态变为【停用】,使用本供应商的 API 将临时使用负载优先级最高的正常供应商。",
|
||||
"Ka08c28d4": "保存后供应商状态变为【正常】,恢复调用本供应商的 AI 能力。",
|
||||
"Kab8fe398": "当前调用状态:",
|
||||
"K508d8bf4": "集成地址",
|
||||
"K67f4e9bb": "与外部平台集成时,获取 API 市场中文档信息的域名",
|
||||
"K4880fd04": "添加 (0) APIKey",
|
||||
"Kc82b8374": "编辑策略",
|
||||
"K4b34a5e5": "策略类型",
|
||||
"K57f0fee8": "匹配条件",
|
||||
|
||||
@@ -1 +1,6 @@
|
||||
{}
|
||||
{
|
||||
"Ke32702ac": "保存后供应商状态变为【停用】,使用本供应商的 API 将临时使用负载优先级最高的正常供应商。",
|
||||
"Ka08c28d4": "保存后供应商状态变为【正常】,恢复调用本供应商的 AI 能力。",
|
||||
"Kab8fe398": "当前调用状态:",
|
||||
"K4880fd04": "添加 (0) APIKey"
|
||||
}
|
||||
@@ -6,11 +6,18 @@
|
||||
"Kecbb0e45": "System",
|
||||
"Ka358e23d": "General",
|
||||
"K449058e9": "API Gateway",
|
||||
"K99935e6f": "AI Model",
|
||||
"K1deaa2dd": "User",
|
||||
"K631d646f": "Open API",
|
||||
"K1196b104": "APIPark",
|
||||
"Kf1b166e7": "Details",
|
||||
"K7ac2be34": "AI Model Management",
|
||||
"K18dccc1a": "Sync Latest Model",
|
||||
"K5cfdd950": "This data cannot be recovered after deletion. Are you sure you want to delete?",
|
||||
"K28435c5c": "API Details",
|
||||
"Kb9052305": "Search Username, Email",
|
||||
"K40a89bd8": "Enter Name, ID to Search Member"
|
||||
"Kbe3e9335": "Exit Test",
|
||||
"K40a89bd8": "Enter Name, ID to Search Member",
|
||||
"K1da86266": "Invalid",
|
||||
"Kb147fabc": "Create",
|
||||
"K40ca4f2": "Update"
|
||||
}
|
||||
@@ -1,22 +1,25 @@
|
||||
{
|
||||
"Kc0e5ef9f": "Workspace",
|
||||
"K4de11e23": "Home",
|
||||
"Kfe93ef35": "Application",
|
||||
"K61c89f5f": "API Portal",
|
||||
"Kc0e5ef9f": "ワークスペース",
|
||||
"K4de11e23": "ホーム",
|
||||
"Kfe93ef35": "アプリケーション",
|
||||
"K61c89f5f": "API ポータル",
|
||||
"K3fe97dcc": "設定",
|
||||
"Kecbb0e45": "システム",
|
||||
"Ka358e23d": "一般",
|
||||
"K449058e9": "API ゲートウェイ",
|
||||
"K99935e6f": "AI モデル",
|
||||
"K1deaa2dd": "ユーザー",
|
||||
"K631d646f": "Open API",
|
||||
"K1196b104": "APIPark",
|
||||
"Kffd7e274": "無審査:すべてのアプリケーションがこのサービスにサブスクライブできます",
|
||||
"K8a8b13e4": "手動審査:承認されたアプリケーションのみがこのサービスにサブスクライブできます",
|
||||
"Kf1b166e7": "詳細",
|
||||
"K7ac2be34": "AI モデル管理",
|
||||
"K18dccc1a": "最新モデルを同期",
|
||||
"K9bdd8403": "API を安全に呼び出すためには、アプリケーションとトークンを作成する必要があります。",
|
||||
"Kc8239422": "チームにはユーザー、アプリケーション、サービスが含まれ、異なるチームのアプリケーションとサービスのデータは分離されています。企業内の部門/プロジェクトグループ/チームの管理に使用できます。",
|
||||
"Ka0a8840a": "他のアプリケーションのサブスクリプション申請をレビューし、承認後に API リクエストが発行できます。",
|
||||
"K5cfdd950": "このデータを削除すると、復元できません。削除しますか?",
|
||||
"K28435c5c": "API 詳細",
|
||||
"Kb9052305": "ユーザー名またはメールを検索",
|
||||
"K5ece3bac": "チームとメンバーを設定してから、チーム内でサービスとアプリケーションを作成し、API をサブスクライブできます。メンバーは所属チーム内のサービスとアプリケーションのみを表示できます。",
|
||||
"K1512e983": "アプリケーション呼び出し統計",
|
||||
@@ -28,11 +31,15 @@
|
||||
"K546e46f": "Application ID",
|
||||
"K95764d1d": "Application を削除",
|
||||
"K667bbbe7": "Application を追加",
|
||||
"Kbe3e9335": "テスト終了",
|
||||
"K8723422e": "アプリケーションを接続",
|
||||
"K93d5a66e": "接続アプリケーション数",
|
||||
"K216a1ac7": "アプリケーション開発者",
|
||||
"K27924db": "アプリケーション管理者",
|
||||
"Kd55c6887": "レビュー",
|
||||
"K831aa6c0": "申請元-アプリケーション",
|
||||
"K40a89bd8": "名前または ID を入力してサービスを検索"
|
||||
"K40a89bd8": "名前または ID を入力してサービスを検索",
|
||||
"K1da86266": "無効",
|
||||
"Kb147fabc": "新規作成",
|
||||
"K40ca4f2": "更新"
|
||||
}
|
||||
@@ -7,16 +7,19 @@
|
||||
"Kecbb0e45": "系统",
|
||||
"Ka358e23d": "常规",
|
||||
"K449058e9": "API 网关",
|
||||
"K99935e6f": "AI 模型",
|
||||
"K1deaa2dd": "用户",
|
||||
"K631d646f": "Open API",
|
||||
"K1196b104": "APIPark",
|
||||
"Kffd7e274": "无审核:允许所有消费者订阅该服务",
|
||||
"K8a8b13e4": "人工审核:仅允许审核通过的消费者订阅该服务",
|
||||
"Kf1b166e7": "详情",
|
||||
"K7ac2be34": "AI 模型管理",
|
||||
"K18dccc1a": "同步最新模型",
|
||||
"K9bdd8403": "为了安全地调用 API,你需要创建一个消费者以及Token。",
|
||||
"Kc8239422": "团队中包含了人员、消费者和服务,不同团队之间的消费者和服务数据是隔离的,可用于管理企业内部不同的部门/项目组/团队。",
|
||||
"Ka0a8840a": "审核其他消费者的订阅申请,审核通过后的才可发起 API 请求。",
|
||||
"K5cfdd950": "该数据删除后将无法找回,是否删除?",
|
||||
"K28435c5c": "API 详情",
|
||||
"Kb9052305": "搜索用户名、邮箱",
|
||||
"K5ece3bac": "设置团队和成员,然后你可以在团队内创建服务和消费者、订阅API,成员只能看到所属团队内的服务和消费者。",
|
||||
"K1512e983": "消费者调用统计",
|
||||
@@ -28,11 +31,14 @@
|
||||
"K546e46f": "消费者 ID",
|
||||
"K95764d1d": "删除消费者",
|
||||
"K667bbbe7": "添加消费者",
|
||||
"Kbe3e9335": "退出测试",
|
||||
"K8723422e": "接入消费者",
|
||||
"K93d5a66e": "接入消费者数量",
|
||||
"K216a1ac7": "消费者开发者",
|
||||
"K27924db": "消费者管理员",
|
||||
"Kd55c6887": "审核",
|
||||
"K831aa6c0": "申请方-消费者",
|
||||
"K40a89bd8": "输入名称、ID 查找服务"
|
||||
"K40a89bd8": "输入名称、ID 查找服务",
|
||||
"Kb147fabc": "新建",
|
||||
"K40ca4f2": "更新"
|
||||
}
|
||||
@@ -7,16 +7,19 @@
|
||||
"Kecbb0e45": "系統",
|
||||
"Ka358e23d": "常規",
|
||||
"K449058e9": "API 網關",
|
||||
"K99935e6f": "AI 模型",
|
||||
"K1deaa2dd": "用戶",
|
||||
"K631d646f": "Open API",
|
||||
"K1196b104": "APIPark",
|
||||
"Kffd7e274": "無審核:允許所有應用程式訂閱該服務",
|
||||
"K8a8b13e4": "人工審核:僅允許審核通過的應用程式訂閱該服務",
|
||||
"Kf1b166e7": "詳情",
|
||||
"K7ac2be34": "AI 模型管理",
|
||||
"K18dccc1a": "同步最新模型",
|
||||
"K9bdd8403": "為了安全地調用 API,你需要創建一個應用以及Token。",
|
||||
"Kc8239422": "團隊中包含了人員、應用程式和服務,不同團隊之間的應用程式和服務數據是隔離的,可用於管理企業內部不同的部門/項目組/團隊。",
|
||||
"Ka0a8840a": "審核其他應用程式的訂閱申請,審核通過後的才可發起 API 請求。",
|
||||
"K5cfdd950": "該數據刪除後將無法找回,是否刪除?",
|
||||
"K28435c5c": "API 詳情",
|
||||
"Kb9052305": "搜索用戶名、電郵",
|
||||
"K5ece3bac": "設置團隊和成員,然後你可以在團隊內創建服務和應用程式、訂閱API,成員只能看到所屬團隊內的服務和應用程式。",
|
||||
"K1512e983": "應用程式調用統計",
|
||||
@@ -28,11 +31,15 @@
|
||||
"K546e46f": "應用程式 ID",
|
||||
"K95764d1d": "刪除應用程式",
|
||||
"K667bbbe7": "添加應用程式",
|
||||
"Kbe3e9335": "退出測試",
|
||||
"K8723422e": "接入應用程式",
|
||||
"K93d5a66e": "接入應用程式數量",
|
||||
"K216a1ac7": "應用程式開發者",
|
||||
"K27924db": "應用程式管理員",
|
||||
"Kd55c6887": "審核",
|
||||
"K831aa6c0": "申請方-應用程式",
|
||||
"K40a89bd8": "輸入名稱、ID 查找服務"
|
||||
"K40a89bd8": "輸入名稱、ID 查找服務",
|
||||
"K1da86266": "無效",
|
||||
"Kb147fabc": "新建",
|
||||
"K40ca4f2": "更新"
|
||||
}
|
||||
@@ -716,5 +716,50 @@
|
||||
"K1bc5e0a3": "消费者 IP",
|
||||
"K6f39ea21": "鉴权名称",
|
||||
"K8c34c02f": "脱敏前",
|
||||
"K8e3d388d": "脱敏后"
|
||||
}
|
||||
"K8e3d388d": "脱敏后",
|
||||
"K94b48734": "获取 AI providers 失败",
|
||||
"Kf23a8988": "AI 供应商",
|
||||
"K4d81a657": "预览",
|
||||
"K91144ebd": "AI API 列表",
|
||||
"Kf8187c33": "请输入 APIURL 搜索",
|
||||
"Kee4139c2": "拦截接口",
|
||||
"K3e38ea": "开启拦截后,网关会拦截所有该路径的请求。",
|
||||
"Kd752a3a8": "未配置 AI 模型",
|
||||
"K8b7ac871": "前往设置",
|
||||
"Kf97448b3": "已设置",
|
||||
"K30d4d8df": "未设置",
|
||||
"Kc2ee5223": "默认模型",
|
||||
"K608af899": "负载优先级",
|
||||
"K65b7a96": "负载优先级决定在原供应商异常或停用后,优先使用哪一个供应商。优先级数字越小,优先级越高。",
|
||||
"K9eccff16": "优先级必须大于 0",
|
||||
"Kfcf02780": "请输入优先级",
|
||||
"K5c6dcf58": "API Key(默认 Key)",
|
||||
"K59bf8ed9": "LLM 状态管理",
|
||||
"Kedd64e4d": "停用",
|
||||
"K2a3aeb8d": "默认:",
|
||||
"K8b88ef63": "请输入 APIKey",
|
||||
"Kcbd30819": "API Key",
|
||||
"Kcb6e2d3e": "请填写 APIKey",
|
||||
"Ke13e332a": "设置过期时间",
|
||||
"K409aa8ba": "请选择过期时间",
|
||||
"Kba69594c": "超额",
|
||||
"Kb9e7ceda": "过期",
|
||||
"Kac405b50": "错误",
|
||||
"K3fde5b49": "请选择状态",
|
||||
"K434b7e76": "编辑 APIKey",
|
||||
"K28190dbc": "删除成功",
|
||||
"Kb5fcf5b8": "停用成功",
|
||||
"K5940d788": "启用成功",
|
||||
"K8743bccd": "排序成功",
|
||||
"K19590c2c": "调用优先级",
|
||||
"K89f135a7": "已用 Token",
|
||||
"K1acc30b2": "编辑时间",
|
||||
"Kefb03657": "APIKey 资源池",
|
||||
"Kc0352e64": "支持单个 API 模型供应商下创建多个 APIKey APIKey 进行智能负载均衡",
|
||||
"Kd25acba1": "请输入名称搜索",
|
||||
"K6d0388a0": "添加 APIKey",
|
||||
"Ke32702ac": "保存后供应商状态变为【停用】,使用本供应商的 API 将临时使用负载优先级最高的正常供应商。",
|
||||
"Ka08c28d4": "保存后供应商状态变为【正常】,恢复调用本供应商的 AI 能力。",
|
||||
"Kab8fe398": "当前调用状态:",
|
||||
"K4880fd04": "添加 (0) APIKey"
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -17,11 +17,11 @@
|
||||
|
||||
import G6, { EdgeConfig } from '@antv/g6'
|
||||
import {
|
||||
SELF_SPACE_CONTENT_COLOR,
|
||||
OUT_SPACE_CONTENT_COLOR,
|
||||
OUT_SPACE_THEME,
|
||||
RELATIVE_PICTURE_NODE_FONTSIZE,
|
||||
SELF_SPACE_THEME,
|
||||
OUT_SPACE_THEME
|
||||
SELF_SPACE_CONTENT_COLOR,
|
||||
SELF_SPACE_THEME
|
||||
} from '@core/const/system-running/const'
|
||||
import { NodeData } from '@core/const/system-running/type'
|
||||
import { TopologyProjectItem, TopologyServiceItem } from '@core/pages/systemRunning/SystemRunning'
|
||||
@@ -144,131 +144,6 @@ export const getNodeSpacing = (num: number, nodes?: unknown) => {
|
||||
return result
|
||||
}
|
||||
|
||||
// const registerEdge = () => {
|
||||
// G6.registerEdge(
|
||||
// 'line-running',
|
||||
// {
|
||||
// options: {
|
||||
// style: EDGE_STYLE
|
||||
// },
|
||||
|
||||
// afterDraw: (cfg, group) => {
|
||||
// const lineDash = [4, 2, 1, 2]
|
||||
// if (!group) return
|
||||
// const shape = group.get('children')[0]
|
||||
// let index = 0
|
||||
// // Define the animation
|
||||
// shape.animate(
|
||||
// () => {
|
||||
// index = index + 0.4
|
||||
// if (index > 1000) {
|
||||
// index = 0
|
||||
// }
|
||||
// const res = {
|
||||
// lineDash,
|
||||
// lineDashOffset: -index
|
||||
// }
|
||||
// return res
|
||||
// },
|
||||
// {
|
||||
// repeat: true,
|
||||
// duration: 5000
|
||||
// }
|
||||
// )
|
||||
// },
|
||||
|
||||
// setState: (name, value, item) => {
|
||||
// if (!item || !name) return
|
||||
// const shape = item.get('keyShape')
|
||||
// const itemStatus = item.getStates()
|
||||
|
||||
// if (
|
||||
// !['edge-success', 'edge-error', 'edge-transparent'].includes(name) &&
|
||||
// itemStatus.some((state) => ['edge-error', 'edge-success', 'edge-transparent'].includes(state))
|
||||
// )
|
||||
// return
|
||||
// const theme = item?._cfg?.model?.style?.stroke || SELF_SPACE_THEME
|
||||
// if (name === 'running') {
|
||||
// if (value) {
|
||||
// shape.attr({
|
||||
// lineWidth: 4,
|
||||
// shadowColor: theme,
|
||||
// shadowBlur: 2
|
||||
// })
|
||||
// } else {
|
||||
// shape.attr(EDGE_STYLE)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// 'quadratic'
|
||||
// )
|
||||
// }
|
||||
|
||||
// private initGraph = (container: ElementRef) => {
|
||||
// const element = container.nativeElement
|
||||
// this.graph = new G6.Graph({
|
||||
// container: container.nativeElement,
|
||||
// plugins: [this.tooltip],
|
||||
// layout: {
|
||||
// type: 'force',
|
||||
// // 稳定系数,初始动画的加载时长(稳定性)=节点数量/稳定系数
|
||||
// alphaDecay: 0.08,
|
||||
// // 因为有分组的存在,整体布局需要往左偏移一点
|
||||
// center: [element.scrollWidth / 2 - 150, element.scrollHeight / 2],
|
||||
// preventOverlap: true,
|
||||
// linkDistance: (d: nodeAny) => {
|
||||
// if (d.source.id === 'node0') {
|
||||
// return 100
|
||||
// }
|
||||
// return 30
|
||||
// },
|
||||
// nodeStrength: (d: nodeAny) => {
|
||||
// if (d.isLeaf) {
|
||||
// return -50
|
||||
// }
|
||||
// return -10
|
||||
// },
|
||||
// edgeStrength: (d: nodeAny) => {
|
||||
// if (d.source.id === 'node1' || d.source.id === 'node2' || d.source.id === 'node3') {
|
||||
// return 0.7
|
||||
// }
|
||||
// return 0.1
|
||||
// }
|
||||
// },
|
||||
// modes: {
|
||||
// default: ['drag-canvas', 'drag-node', 'zoom-canvas']
|
||||
// },
|
||||
// defaultNode: {
|
||||
// size: [24, 24],
|
||||
// style: {
|
||||
// radius: 5,
|
||||
// stroke: '#69c0ff',
|
||||
// lineWidth: 1,
|
||||
// fillOpacity: 1
|
||||
// },
|
||||
// labelCfg: {
|
||||
// style: {
|
||||
// fontSize: RELATIVE_PICTURE_NODE_FONTSIZE,
|
||||
// fill: this.textColor
|
||||
// },
|
||||
// position: 'bottom',
|
||||
// offset: 12
|
||||
// }
|
||||
// },
|
||||
// defaultEdge: {
|
||||
// type: 'line-running',
|
||||
// label: $t('详情',
|
||||
// labelCfg: {
|
||||
// style: {
|
||||
// fill: '5B8FF9',
|
||||
// opacity: 0
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
|
||||
export class UnionFind {
|
||||
private parent: Record<string, string>
|
||||
private rank: Record<string, number>
|
||||
|
||||
+128
-130
@@ -1,47 +1,45 @@
|
||||
|
||||
import { StyleProvider } from '@ant-design/cssinjs'
|
||||
import { BreadcrumbProvider } from '@common/contexts/BreadcrumbContext.tsx'
|
||||
import { GlobalProvider } from '@common/contexts/GlobalStateContext'
|
||||
import { useLocaleContext } from '@common/contexts/LocaleContext'
|
||||
import { PluginEventHubProvider } from '@common/contexts/PluginEventHubContext'
|
||||
import { PluginSlotHubProvider } from '@common/contexts/PluginSlotHubContext'
|
||||
import useInitializeMonaco from '@common/hooks/useInitializeMonaco'
|
||||
import { $t } from '@common/locales'
|
||||
import RenderRoutes from '@core/components/aoplatform/RenderRoutes'
|
||||
import { App as AppAntd, ConfigProvider } from 'antd'
|
||||
import { useMemo } from 'react'
|
||||
import './App.css'
|
||||
import { ConfigProvider, App as AppAntd } from 'antd';
|
||||
import RenderRoutes from '@core/components/aoplatform/RenderRoutes';
|
||||
import {BreadcrumbProvider} from "@common/contexts/BreadcrumbContext.tsx";
|
||||
import useInitializeMonaco from "@common/hooks/useInitializeMonaco";
|
||||
import { useMemo } from 'react';
|
||||
import { GlobalProvider } from '@common/contexts/GlobalStateContext';
|
||||
import { $t } from '@common/locales';
|
||||
import { PluginEventHubProvider } from '@common/contexts/PluginEventHubContext';
|
||||
import { PluginSlotHubProvider } from '@common/contexts/PluginSlotHubContext';
|
||||
import { useLocaleContext } from '@common/contexts/LocaleContext';
|
||||
import { StyleProvider } from '@ant-design/cssinjs';
|
||||
|
||||
|
||||
const antdComponentThemeToken = {
|
||||
token: {
|
||||
// Seed Token,影响范围大
|
||||
colorPrimary: '#3D46F2',
|
||||
colorLink:'#3D46F2',
|
||||
colorBorder:'#ededed',
|
||||
colorText:'#333',
|
||||
colorLink: '#3D46F2',
|
||||
colorBorder: '#ededed',
|
||||
colorText: '#333',
|
||||
borderRadius: 4,
|
||||
// 派生变量,影响范围小
|
||||
colorBgContainer: '#fff',
|
||||
colorPrimaryBg:'#EBEEF2',
|
||||
colorTextQuaternary:'#BBB',
|
||||
colorTextTertiary:'#999'
|
||||
colorPrimaryBg: '#EBEEF2',
|
||||
colorTextQuaternary: '#BBB',
|
||||
colorTextTertiary: '#999'
|
||||
},
|
||||
components:{
|
||||
components: {
|
||||
// 派生变量,影响范围小
|
||||
Input:{
|
||||
activeShadow:'none'
|
||||
Input: {
|
||||
activeShadow: 'none'
|
||||
},
|
||||
Select:{
|
||||
activeShadow:'none'
|
||||
Select: {
|
||||
activeShadow: 'none'
|
||||
},
|
||||
Checkbox:{
|
||||
activeShadow:'none'
|
||||
Checkbox: {
|
||||
activeShadow: 'none'
|
||||
},
|
||||
Cascader:{
|
||||
activeShadow:'none',
|
||||
optionSelectedBg:'#EBEEF2',
|
||||
optionHoverBg:'#EBEEF2'
|
||||
Cascader: {
|
||||
activeShadow: 'none',
|
||||
optionSelectedBg: '#EBEEF2',
|
||||
optionHoverBg: '#EBEEF2'
|
||||
},
|
||||
Layout: {
|
||||
bodyBg: '#fff',
|
||||
@@ -50,122 +48,122 @@ const antdComponentThemeToken = {
|
||||
headerHeight: 50,
|
||||
headerPadding: '10 20px',
|
||||
lightSiderBg: '#fff',
|
||||
siderBg: '#fff',
|
||||
siderBg: '#fff'
|
||||
},
|
||||
Breadcrumb:{
|
||||
itemColor:'#666',
|
||||
linkColor:'#666',
|
||||
lastItemColor:'#333',
|
||||
Breadcrumb: {
|
||||
itemColor: '#666',
|
||||
linkColor: '#666',
|
||||
lastItemColor: '#333'
|
||||
},
|
||||
Table:{
|
||||
headerBorderRadius:0,
|
||||
headerSplitColor:'#ededed',
|
||||
borderColor:'#ededed',
|
||||
cellPaddingBlockMD:'15px',
|
||||
cellPaddingInlineMD:'12px',
|
||||
cellPaddingBlockSM:'8px',
|
||||
cellPaddingInlineSM:'12px',
|
||||
headerFilterHoverBg:'#EBEEF2',
|
||||
headerSortActiveBg:'#F7F8FA',
|
||||
headerSortHoverBg:'#F7F8FA',
|
||||
fixedHeaderSortActiveBg:'#F7F8FA',
|
||||
headerBg:'#FAFAFA',
|
||||
rowHoverBg:'#EBEEF2'
|
||||
|
||||
Table: {
|
||||
headerBorderRadius: 0,
|
||||
headerSplitColor: '#ededed',
|
||||
borderColor: '#ededed',
|
||||
cellPaddingBlockMD: '15px',
|
||||
cellPaddingInlineMD: '12px',
|
||||
cellPaddingBlockSM: '8px',
|
||||
cellPaddingInlineSM: '12px',
|
||||
headerFilterHoverBg: '#EBEEF2',
|
||||
headerSortActiveBg: '#F7F8FA',
|
||||
headerSortHoverBg: '#F7F8FA',
|
||||
fixedHeaderSortActiveBg: '#F7F8FA',
|
||||
headerBg: '#FAFAFA',
|
||||
rowHoverBg: '#EBEEF2'
|
||||
},
|
||||
Segmented:{
|
||||
itemColor:'#333',
|
||||
itemSelectedColor:'#333',
|
||||
trackBg:'#f7f8fa',
|
||||
trackPadding:0,
|
||||
// itemHoverColor:'#EBEEF2',
|
||||
itemActiveBg:'#EBEEF2',
|
||||
itemHoverBg:'#EBEEF2',
|
||||
itemSelectedBg:'#EBEEF2',
|
||||
Segmented: {
|
||||
itemColor: '#333',
|
||||
itemSelectedColor: '#333',
|
||||
trackBg: '#f7f8fa',
|
||||
trackPadding: 0,
|
||||
// itemHoverColor:'#EBEEF2',
|
||||
itemActiveBg: '#EBEEF2',
|
||||
itemHoverBg: '#EBEEF2',
|
||||
itemSelectedBg: '#EBEEF2'
|
||||
},
|
||||
Tree:{
|
||||
// titleHeight:30,
|
||||
// fontSize:12,
|
||||
directoryNodeSelectedBg:'#EBEEF2',
|
||||
directoryNodeSelectedColor:'#333',
|
||||
nodeSelectedBg:'#EBEEF2',
|
||||
nodeHoverBg:'#EBEEF2'
|
||||
},
|
||||
Collapse:{
|
||||
headerBg:'#f7f8fa',
|
||||
headerPadding:"12px",
|
||||
contentPadding:"0 10px 12px 10px"
|
||||
},
|
||||
Button:{
|
||||
// paddingInline:8,
|
||||
dangerShadow:'none',
|
||||
defaultShadow:'none',
|
||||
primaryShadow:'none'
|
||||
},
|
||||
Tabs:{
|
||||
cardBg:'#EBEEF2',
|
||||
cardHeight:42,
|
||||
horizontalItemGutter:8,
|
||||
horizontalItemPaddingSM:'12px 8px 8px 8px',
|
||||
horizontalItemPadding:'12px 8px 8px 8px',
|
||||
},
|
||||
Menu:{
|
||||
// itemBg:'#F7F8FA',
|
||||
// subMenuItemBg:'#F7F8FA',
|
||||
// itemMarginBlock:0,
|
||||
// activeBarBorderWidth:0,
|
||||
// itemSelectedColor:'#333',
|
||||
// itemSelectedBg:'#EBEEF2',
|
||||
// itemHoverBg:'#EBEEF2'
|
||||
},
|
||||
List:{
|
||||
itemPadding:'8px 0'
|
||||
},
|
||||
Form:{
|
||||
itemMarginBottom:10,
|
||||
|
||||
},
|
||||
Alert:{
|
||||
defaultPadding:'12px 16px'
|
||||
},
|
||||
Tag:{
|
||||
defaultBg:"#f7f8fa"
|
||||
},
|
||||
Tree: {
|
||||
// titleHeight:30,
|
||||
// fontSize:12,
|
||||
directoryNodeSelectedBg: '#EBEEF2',
|
||||
directoryNodeSelectedColor: '#333',
|
||||
nodeSelectedBg: '#EBEEF2',
|
||||
nodeHoverBg: '#EBEEF2'
|
||||
},
|
||||
Collapse: {
|
||||
headerBg: '#f7f8fa',
|
||||
headerPadding: '12px',
|
||||
contentPadding: '0 10px 12px 10px'
|
||||
},
|
||||
Button: {
|
||||
// paddingInline:8,
|
||||
dangerShadow: 'none',
|
||||
defaultShadow: 'none',
|
||||
primaryShadow: 'none'
|
||||
},
|
||||
Tabs: {
|
||||
cardBg: '#EBEEF2',
|
||||
cardHeight: 42,
|
||||
horizontalItemGutter: 8,
|
||||
horizontalItemPaddingSM: '12px 8px 8px 8px',
|
||||
horizontalItemPadding: '12px 8px 8px 8px'
|
||||
},
|
||||
Menu: {
|
||||
// itemBg:'#F7F8FA',
|
||||
// subMenuItemBg:'#F7F8FA',
|
||||
// itemMarginBlock:0,
|
||||
// activeBarBorderWidth:0,
|
||||
// itemSelectedColor:'#333',
|
||||
// itemSelectedBg:'#EBEEF2',
|
||||
// itemHoverBg:'#EBEEF2'
|
||||
},
|
||||
List: {
|
||||
itemPadding: '8px 0'
|
||||
},
|
||||
Form: {
|
||||
itemMarginBottom: 10
|
||||
},
|
||||
Alert: {
|
||||
defaultPadding: '8px 12px'
|
||||
},
|
||||
Tag: {
|
||||
defaultBg: '#f7f8fa'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function App() {
|
||||
const { locale } = useLocaleContext();
|
||||
const { locale } = useLocaleContext()
|
||||
useInitializeMonaco()
|
||||
|
||||
|
||||
const validateMessages = useMemo(()=>({
|
||||
required: $t('必填项'),
|
||||
email:$t('不是有效邮箱地址')}
|
||||
),[locale])
|
||||
|
||||
|
||||
const validateMessages = useMemo(
|
||||
() => ({
|
||||
required: $t('必填项'),
|
||||
email: $t('不是有效邮箱地址')
|
||||
}),
|
||||
[locale]
|
||||
)
|
||||
|
||||
return (
|
||||
<StyleProvider hashPriority={"high"}>
|
||||
<ConfigProvider
|
||||
<StyleProvider hashPriority={'high'}>
|
||||
<ConfigProvider
|
||||
locale={locale}
|
||||
wave={{disabled:true}}
|
||||
wave={{ disabled: true }}
|
||||
theme={antdComponentThemeToken}
|
||||
form={{validateMessages }}>
|
||||
<PluginEventHubProvider>
|
||||
<AppAntd className="h-full" message={{ maxCount: 1 }}>
|
||||
<PluginSlotHubProvider>
|
||||
<GlobalProvider>
|
||||
<BreadcrumbProvider>
|
||||
<RenderRoutes />
|
||||
</BreadcrumbProvider>
|
||||
</GlobalProvider>
|
||||
</PluginSlotHubProvider>
|
||||
form={{ validateMessages }}
|
||||
>
|
||||
<PluginEventHubProvider>
|
||||
<AppAntd className="h-full" message={{ maxCount: 1 }}>
|
||||
<PluginSlotHubProvider>
|
||||
<GlobalProvider>
|
||||
<BreadcrumbProvider>
|
||||
<RenderRoutes />
|
||||
</BreadcrumbProvider>
|
||||
</GlobalProvider>
|
||||
</PluginSlotHubProvider>
|
||||
</AppAntd>
|
||||
</PluginEventHubProvider>
|
||||
</ConfigProvider>
|
||||
</StyleProvider>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import { STATUS_CODE } from '@common/const/const'
|
||||
import { useFetch } from '@common/hooks/http'
|
||||
import { $t } from '@common/locales'
|
||||
import { ModelDetailData } from '@core/pages/aiSetting/types'
|
||||
import { Select, Space, message } from 'antd'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export interface AIProvider {
|
||||
id: string
|
||||
name: string
|
||||
logo: string
|
||||
configured: boolean
|
||||
getApikeyUrl: string
|
||||
status: string
|
||||
export interface AIProvider extends ModelDetailData {
|
||||
default_config: string
|
||||
backupName: string
|
||||
backupModel: string
|
||||
}
|
||||
|
||||
interface AIProviderResponse {
|
||||
@@ -41,19 +39,24 @@ const AIProviderSelect: React.FC<AIProviderSelectProps> = ({ value, onChange, st
|
||||
const fetchProviders = async () => {
|
||||
if (isMounted) setLoading(true)
|
||||
try {
|
||||
const response = await fetchData<AIProviderResponse>('simple/ai/providers/configured', { method: 'GET' })
|
||||
const endpoint = 'simple/ai/providers/configured'
|
||||
const response = await fetchData<AIProviderResponse>(endpoint, { method: 'GET' })
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
isMounted && setProviders(data.providers)
|
||||
const providers = data.providers.map((val) => ({
|
||||
...val,
|
||||
backupName: data.backup?.name,
|
||||
backupModel: data.backup?.model?.name
|
||||
}))
|
||||
isMounted && setProviders(providers)
|
||||
if (!data.providers?.length) return
|
||||
|
||||
const selectedProvider: AIProvider = value ? providers.find((p) => p.id === value) : data.providers[0]
|
||||
const selectedProvider: AIProvider = value ? providers.find((p) => p.id === value) : providers[0]
|
||||
onChange?.(selectedProvider.id, selectedProvider)
|
||||
} else {
|
||||
message.error(msg || t('Failed to fetch AI providers'))
|
||||
message.error(msg || t('获取 AI providers 失败'))
|
||||
}
|
||||
} catch (error) {
|
||||
message.error(t('Failed to fetch AI providers'))
|
||||
message.error(t('获取 AI providers 失败'))
|
||||
} finally {
|
||||
isMounted && setLoading(false)
|
||||
}
|
||||
@@ -68,7 +71,7 @@ const AIProviderSelect: React.FC<AIProviderSelectProps> = ({ value, onChange, st
|
||||
|
||||
return (
|
||||
<Space className="flex items-center">
|
||||
<span>{t('AI 供应商')}:</span>
|
||||
<span>{$t('AI 供应商')}:</span>
|
||||
<Select
|
||||
value={value}
|
||||
onChange={(selectedValue) => {
|
||||
|
||||
@@ -1,127 +1,135 @@
|
||||
import { AiServiceRouterTableListItem, VariableItems } from "./type";
|
||||
import { TabsProps } from "antd";
|
||||
import { frontendTimeSorter } from "@common/utils/dataTransfer";
|
||||
import { COLUMNS_TITLE, PLACEHOLDER } from "@common/const/const";
|
||||
|
||||
import { PageProColumns } from "@common/components/aoplatform/PageList";
|
||||
|
||||
import { COLUMNS_TITLE, PLACEHOLDER } from '@common/const/const'
|
||||
import { frontendTimeSorter } from '@common/utils/dataTransfer'
|
||||
import { TabsProps } from 'antd'
|
||||
import { AiServiceRouterTableListItem, VariableItems } from './type'
|
||||
|
||||
import { PageProColumns } from '@common/components/aoplatform/PageList'
|
||||
|
||||
export const AI_SERVICE_ROUTER_TABLE_COLUMNS: PageProColumns<AiServiceRouterTableListItem>[] = [
|
||||
{
|
||||
title:('URL'),
|
||||
dataIndex: 'requestPath',
|
||||
ellipsis:true
|
||||
},
|
||||
{
|
||||
title:('名称'),
|
||||
dataIndex: 'name',
|
||||
ellipsis:true,
|
||||
},
|
||||
{
|
||||
title:('模型'),
|
||||
dataIndex: ['model','logo'],
|
||||
ellipsis:true,
|
||||
render: (_: React.ReactNode, entity: AiServiceRouterTableListItem) =><div className="flex items-center gap-[2px] " ><span>{entity.model.id}</span></div>
|
||||
},
|
||||
{
|
||||
title:('描述'),
|
||||
dataIndex: 'description',
|
||||
ellipsis:true
|
||||
},
|
||||
{
|
||||
title:('创建者'),
|
||||
dataIndex: ['creator','name'],
|
||||
ellipsis: true,
|
||||
filters: true,
|
||||
onFilter: true,
|
||||
valueType: 'select',
|
||||
filterSearch: true,
|
||||
},
|
||||
{
|
||||
title:('更新时间'),
|
||||
dataIndex: 'updateTime',
|
||||
ellipsis:true,
|
||||
hideInSearch: true,
|
||||
width:182,
|
||||
sorter: (a,b)=>frontendTimeSorter(a,b,'updateTime')
|
||||
},
|
||||
];
|
||||
{
|
||||
title: 'URL',
|
||||
dataIndex: 'requestPath',
|
||||
ellipsis: true,
|
||||
width: 200
|
||||
},
|
||||
{
|
||||
title: '名称',
|
||||
dataIndex: 'name',
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '模型',
|
||||
dataIndex: ['model', 'logo'],
|
||||
ellipsis: true,
|
||||
render: (_: React.ReactNode, entity: AiServiceRouterTableListItem) => (
|
||||
<div className="flex items-center gap-[2px] ">
|
||||
<span>{entity.model.id}</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '是否放行',
|
||||
dataIndex: 'disabled',
|
||||
ellipsis: true,
|
||||
filters: true,
|
||||
onFilter: true,
|
||||
valueType: 'select'
|
||||
},
|
||||
{
|
||||
title: '描述',
|
||||
dataIndex: 'description',
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '创建者',
|
||||
dataIndex: ['creator', 'name'],
|
||||
ellipsis: true,
|
||||
filters: true,
|
||||
onFilter: true,
|
||||
valueType: 'select',
|
||||
filterSearch: true
|
||||
},
|
||||
{
|
||||
title: '更新时间',
|
||||
dataIndex: 'updateTime',
|
||||
ellipsis: true,
|
||||
hideInSearch: true,
|
||||
width: 182,
|
||||
sorter: (a, b) => frontendTimeSorter(a, b, 'updateTime')
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
export const AI_SERVICE_VARIABLES_TABLE_COLUMNS: PageProColumns<VariableItems & {_id:string}>[] = [
|
||||
{
|
||||
title:('Key'),
|
||||
dataIndex: 'key',
|
||||
key:'key',
|
||||
width: '30%',
|
||||
formItemProps: {
|
||||
className:'p-0 bg-transparent border-none',
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
whitespace: true
|
||||
},
|
||||
{
|
||||
pattern:/^[a-zA-Z][a-zA-Z0-9-_]*$/,
|
||||
message: PLACEHOLDER.onlyAlphabet
|
||||
}
|
||||
],
|
||||
},
|
||||
ellipsis:true,
|
||||
fieldProps:{
|
||||
allowClear:false
|
||||
}
|
||||
},
|
||||
{
|
||||
title:('描述'),
|
||||
dataIndex: 'description',
|
||||
key:'description',
|
||||
formItemProps: {
|
||||
className:'p-0 bg-transparent border-none'
|
||||
export const AI_SERVICE_VARIABLES_TABLE_COLUMNS: PageProColumns<VariableItems & { _id: string }>[] = [
|
||||
{
|
||||
title: 'Key',
|
||||
dataIndex: 'key',
|
||||
key: 'key',
|
||||
width: '30%',
|
||||
formItemProps: {
|
||||
className: 'p-0 bg-transparent border-none',
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
whitespace: true
|
||||
},
|
||||
fieldProps:{
|
||||
allowClear:false
|
||||
{
|
||||
pattern: /^[a-zA-Z][a-zA-Z0-9-_]*$/,
|
||||
message: PLACEHOLDER.onlyAlphabet
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title:('必填'),
|
||||
dataIndex: 'require',
|
||||
key:'require',
|
||||
valueType:'switch',
|
||||
width:64,
|
||||
formItemProps: {
|
||||
className:'p-0 bg-transparent border-none'}
|
||||
},
|
||||
{
|
||||
title: COLUMNS_TITLE.operate,
|
||||
valueType: 'option',
|
||||
width:34,
|
||||
render: ()=>null
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
export const AiService_INSIDE_APPROVAL_TAB_ITEMS: TabsProps['items'] = [
|
||||
{
|
||||
key: '0',
|
||||
label:('待审核'),
|
||||
},
|
||||
{
|
||||
key: '1',
|
||||
label: ('已审核'),
|
||||
ellipsis: true,
|
||||
fieldProps: {
|
||||
allowClear: false
|
||||
}
|
||||
];
|
||||
|
||||
},
|
||||
{
|
||||
title: '描述',
|
||||
dataIndex: 'description',
|
||||
key: 'description',
|
||||
formItemProps: {
|
||||
className: 'p-0 bg-transparent border-none'
|
||||
},
|
||||
fieldProps: {
|
||||
allowClear: false
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '必填',
|
||||
dataIndex: 'require',
|
||||
key: 'require',
|
||||
valueType: 'switch',
|
||||
width: 64,
|
||||
formItemProps: {
|
||||
className: 'p-0 bg-transparent border-none'
|
||||
}
|
||||
},
|
||||
{
|
||||
title: COLUMNS_TITLE.operate,
|
||||
valueType: 'option',
|
||||
width: 34,
|
||||
render: () => null
|
||||
}
|
||||
]
|
||||
|
||||
export const AiService_INSIDE_APPROVAL_TAB_ITEMS: TabsProps['items'] = [
|
||||
{
|
||||
key: '0',
|
||||
label: '待审核'
|
||||
},
|
||||
{
|
||||
key: '1',
|
||||
label: '已审核'
|
||||
}
|
||||
]
|
||||
|
||||
export const AiService_PUBLISH_TAB_ITEMS: TabsProps['items'] = [
|
||||
{
|
||||
key: '0',
|
||||
label: ('发布版本'),
|
||||
},
|
||||
{
|
||||
key: '1',
|
||||
label: ('发布申请记录'),
|
||||
}
|
||||
];
|
||||
{
|
||||
key: '0',
|
||||
label: '发布版本'
|
||||
},
|
||||
{
|
||||
key: '1',
|
||||
label: '发布申请记录'
|
||||
}
|
||||
]
|
||||
|
||||
@@ -468,10 +468,34 @@ export const routerMap: Map<string, RouterMapConfig> = new Map([
|
||||
}
|
||||
],
|
||||
[
|
||||
'aiapis',
|
||||
'aiApis',
|
||||
{
|
||||
type: 'component',
|
||||
lazy: lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/aiApis/index.tsx'))
|
||||
lazy: lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/aiApis/aiApisLayout.tsx')),
|
||||
key: 'aiApis',
|
||||
provider: AiServiceProvider,
|
||||
children: [
|
||||
{
|
||||
path: 'list',
|
||||
key: 'apiList',
|
||||
lazy: lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/aiApis/index.tsx'))
|
||||
},
|
||||
{
|
||||
path: 'service/:teamId/aiInside/:serviceId',
|
||||
key: 'aiApisServiceInside',
|
||||
lazy: lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/aiService/AiServiceInsidePage.tsx')),
|
||||
children: [
|
||||
{
|
||||
path: 'route/:routeId/:type',
|
||||
key: 'aiApisServiceInsideRouteDetail',
|
||||
lazy: lazy(
|
||||
() =>
|
||||
import(/* webpackChunkName: "[request]" */ '@core/pages/aiService/api/AiServiceInsideRouterCreate')
|
||||
)
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
[
|
||||
|
||||
@@ -258,7 +258,7 @@ export const SYSTEM_API_TABLE_COLUMNS: PageProColumns<SystemApiTableListItem>[]
|
||||
},
|
||||
{
|
||||
title: '是否放行',
|
||||
dataIndex: 'disable',
|
||||
dataIndex: 'disabled',
|
||||
ellipsis: true,
|
||||
filters: true,
|
||||
onFilter: true,
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { useEffect } from 'react'
|
||||
import { Outlet, useLocation, useNavigate } from 'react-router-dom'
|
||||
|
||||
export default function GlobalPolicyLayout() {
|
||||
const location = useLocation()
|
||||
const pathName = location.pathname
|
||||
const navigator = useNavigate()
|
||||
useEffect(() => {
|
||||
if (pathName === '/aiApis') {
|
||||
const queryParams = new URLSearchParams(location.search).toString()
|
||||
navigator(`/aiApis/list${queryParams ? `?${queryParams}` : ''}`)
|
||||
}
|
||||
}, [pathName])
|
||||
return <Outlet></Outlet>
|
||||
}
|
||||
@@ -1,17 +1,322 @@
|
||||
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 TimeRangeSelector, { TimeRangeButton } from '@common/components/aoplatform/TimeRangeSelector'
|
||||
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
|
||||
import { useFetch } from '@common/hooks/http'
|
||||
import { $t } from '@common/locales'
|
||||
import React from 'react'
|
||||
import AIProviderSelect, { AIProvider } from '@core/components/AIProviderSelect'
|
||||
import { getTime } from '@dashboard/utils/dashboard'
|
||||
import { Alert, App, Button, Typography } from 'antd'
|
||||
import dayjs from 'dayjs'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom'
|
||||
import { APIs } from './types'
|
||||
|
||||
const ApiSettings: React.FC = () => {
|
||||
const pageListRef = useRef<ActionType>(null)
|
||||
const { modal, message } = App.useApp()
|
||||
const [searchParams] = useSearchParams()
|
||||
const [selectedProvider, setSelectedProvider] = useState<string>(searchParams.get('modelId') || '')
|
||||
const [provider, setProvider] = useState<AIProvider | undefined>()
|
||||
const { fetchData } = useFetch()
|
||||
const [searchWord, setSearchWord] = useState<string>('')
|
||||
const [columns, setColumns] = useState<PageProColumns<APIs>[]>([])
|
||||
const [total, setTotal] = useState<number>(0)
|
||||
const [timeButton, setTimeButton] = useState<TimeRangeButton>('day')
|
||||
const navigate = useNavigate()
|
||||
const [timeRange, setTimeRange] = useState<{ start: number | null; end: number | null }>({
|
||||
start: null,
|
||||
end: null
|
||||
})
|
||||
const [queryBtnLoading, setQueryBtnLoading] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
pageListRef.current?.reload()
|
||||
}, [selectedProvider])
|
||||
|
||||
const handlePreview = (record: APIs) => {
|
||||
navigate(`../service/${record.team.id}/aiInside/${record.service.id}/route/${record.id}/apiDetail`)
|
||||
}
|
||||
const requestApis = async (
|
||||
params: any & {
|
||||
pageSize: number
|
||||
current: number
|
||||
},
|
||||
sort: Record<string, string>,
|
||||
filter: Record<string, string>
|
||||
) => {
|
||||
if (!selectedProvider) return
|
||||
setQueryBtnLoading(true)
|
||||
try {
|
||||
const eoParams = {
|
||||
provider: selectedProvider,
|
||||
page_size: params.pageSize,
|
||||
keyword: searchWord,
|
||||
sort: Object.keys(sort)?.length > 0 ? 'use_token' : undefined,
|
||||
asc: Object.keys(sort)?.length > 0 ? Object.values(sort)?.[0] === 'ascend' : undefined,
|
||||
models: filter?.model && filter?.model?.length ? JSON.stringify(filter.model) : undefined,
|
||||
services: filter?.name && filter?.name?.length ? JSON.stringify(filter.name) : undefined,
|
||||
disabled: filter?.disable && filter?.disable?.length ? filter.disable[0] : undefined,
|
||||
page: params.current,
|
||||
start: timeRange.start,
|
||||
end: timeRange.end
|
||||
}
|
||||
if (!timeRange || !timeRange.start) {
|
||||
const { startTime, endTime } = getTime(timeButton, [])
|
||||
eoParams.start = startTime
|
||||
eoParams.end = endTime
|
||||
}
|
||||
const response = await fetchData<BasicResponse<{ data: APIs[] }>>('ai/apis', {
|
||||
method: 'GET',
|
||||
eoParams
|
||||
})
|
||||
setQueryBtnLoading(false)
|
||||
if (response.code === STATUS_CODE.SUCCESS) {
|
||||
setTotal(response.data.total)
|
||||
const modalMap: {
|
||||
[key: string]: string
|
||||
} = response.data?.condition?.models.reduce(
|
||||
(acc: { [key: string]: string }, item: { id: string; name: string }) => {
|
||||
acc[item.id] = $t(item.name)
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
const serviceMap: {
|
||||
[key: string]: string
|
||||
} = response.data?.condition?.services.reduce(
|
||||
(acc: { [key: string]: string }, item: { id: string; name: string }) => {
|
||||
acc[item.id] = $t(item.name)
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
setTableColumns(modalMap, serviceMap)
|
||||
return {
|
||||
data: response.data.apis || [],
|
||||
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 setTableColumns = (
|
||||
modalMap: {
|
||||
[key: string]: string
|
||||
},
|
||||
serviceMap: {
|
||||
[key: string]: string
|
||||
}
|
||||
) => {
|
||||
setColumns([
|
||||
{
|
||||
title: $t('AI 服务'),
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 180,
|
||||
filters: true,
|
||||
valueEnum: serviceMap || {}
|
||||
},
|
||||
{
|
||||
title: 'API URL',
|
||||
dataIndex: 'request_path',
|
||||
key: 'request_path',
|
||||
ellipsis: true,
|
||||
render: (text: string, record: APIs) => (
|
||||
<p>
|
||||
<Typography.Text type="success">{record.method}</Typography.Text>
|
||||
<span className="ml-1">{text}</span>
|
||||
</p>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: $t('模型'),
|
||||
dataIndex: ['model', 'name'],
|
||||
key: 'model',
|
||||
width: 150,
|
||||
filters: true,
|
||||
onFilter: true,
|
||||
valueType: 'select',
|
||||
valueEnum: modalMap || {}
|
||||
},
|
||||
{
|
||||
title: $t('已用 Token'),
|
||||
dataIndex: 'use_token',
|
||||
key: 'use_token',
|
||||
width: 120,
|
||||
sorter: (a: any, b: any) => {
|
||||
return (a.priority as number) - (b.priority as number)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: $t('是否放行'),
|
||||
dataIndex: 'disable',
|
||||
ellipsis: true,
|
||||
width: 120,
|
||||
filters: true,
|
||||
onFilter: true,
|
||||
valueType: 'select',
|
||||
valueEnum: {
|
||||
true: { text: <Typography.Text type="danger">{$t('拦截')}</Typography.Text> },
|
||||
false: { text: <Typography.Text type="success">{$t('放行')}</Typography.Text> }
|
||||
}
|
||||
},
|
||||
{
|
||||
title: $t('编辑时间'),
|
||||
dataIndex: 'update_time',
|
||||
key: 'update_time',
|
||||
width: 200,
|
||||
render: (time: string) => <Typography.Text>{dayjs(time).format('YYYY-MM-DD HH:mm:ss')}</Typography.Text>
|
||||
},
|
||||
...operation
|
||||
])
|
||||
}
|
||||
const operation: PageProColumns<APIs>[] = [
|
||||
{
|
||||
title: '',
|
||||
key: 'option',
|
||||
btnNums: 4,
|
||||
fixed: 'right',
|
||||
valueType: 'option',
|
||||
render: (_: React.ReactNode, entity: APIs) => [
|
||||
<TableBtnWithPermission
|
||||
access="team.service.router.view"
|
||||
key="preview"
|
||||
btnType="logs"
|
||||
onClick={() => handlePreview(entity)}
|
||||
btnTitle={$t('预览')}
|
||||
/>
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const resetQuery = () => {
|
||||
setTimeButton('day')
|
||||
setTimeRange({ start: null, end: null })
|
||||
setSearchWord('')
|
||||
}
|
||||
|
||||
const getData = () => {
|
||||
pageListRef.current?.reload()
|
||||
}
|
||||
|
||||
const renderProviderBanner = () => {
|
||||
if (!provider) return null
|
||||
if (provider.status === 'disabled' || provider.status === 'abnormal') {
|
||||
const message =
|
||||
provider.status === 'disabled'
|
||||
? $t(`当前供应商异常,以下API均临时调用 ${provider.backupName} 下的 ${provider.backupModel} 模型能力。`)
|
||||
: $t(`当前供应商异常,以下API均临时调用 ${provider.backupName} 下的 ${provider.backupModel} 模型能力。`)
|
||||
const type = provider.status === 'disabled' ? 'warning' : 'error'
|
||||
return (
|
||||
<Alert
|
||||
message={message}
|
||||
type={type}
|
||||
className="mt-[30px]"
|
||||
showIcon
|
||||
action={
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
onClick={() => {
|
||||
navigate('/aisetting')
|
||||
}}
|
||||
>
|
||||
{$t('查看详情')}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const AIApis: React.FC = () => {
|
||||
return (
|
||||
<InsidePage
|
||||
className="overflow-y-auto pb-PAGE_INSIDE_B"
|
||||
pageTitle={$t('AI API')}
|
||||
description={$t('配置好 AI 模型后,你可以使用对应的大模型来创建 AI 服务')}
|
||||
pageTitle={$t('AI API 列表')}
|
||||
description={
|
||||
<>
|
||||
<div className="flex gap-2 items-center">
|
||||
<AIProviderSelect
|
||||
value={selectedProvider}
|
||||
onChange={(value, option) => {
|
||||
setSelectedProvider(value)
|
||||
setProvider(option)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{renderProviderBanner()}
|
||||
</>
|
||||
}
|
||||
showBorder={false}
|
||||
scrollPage={false}
|
||||
></InsidePage>
|
||||
>
|
||||
<div className="h-[calc(100%-1rem-36px)] pr-PAGE_INSIDE_X">
|
||||
<PageList
|
||||
ref={pageListRef}
|
||||
rowKey="id"
|
||||
afterNewBtn={
|
||||
<div className="flex items-center flex-wrap pb-[10px] px-btnbase content-before bg-MAIN_BG pr-PAGE_INSIDE_X">
|
||||
<TimeRangeSelector
|
||||
labelSize="small"
|
||||
hideBtns={['hour']}
|
||||
initialTimeButton={timeButton}
|
||||
onTimeButtonChange={setTimeButton}
|
||||
onTimeRangeChange={($event) => {
|
||||
setTimeRange($event)
|
||||
}}
|
||||
/>
|
||||
<div className="flex flex-nowrap items-center pt-btnybase">
|
||||
<Button onClick={resetQuery}>{$t('重置')}</Button>
|
||||
<Button
|
||||
className="ml-btnybase"
|
||||
type="primary"
|
||||
loading={queryBtnLoading}
|
||||
onClick={() => {
|
||||
setQueryBtnLoading(true)
|
||||
getData()
|
||||
}}
|
||||
>
|
||||
{$t('查询')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
request={async (
|
||||
params: any & {
|
||||
pageSize: number
|
||||
current: number
|
||||
},
|
||||
sort: Record<string, string>,
|
||||
filter: Record<string, string>
|
||||
) => requestApis(params, sort, filter)}
|
||||
onSearchWordChange={(e) => {
|
||||
setSearchWord(e.target.value)
|
||||
}}
|
||||
onRowClick={(row: APIs) => handlePreview(row)}
|
||||
showPagination={true}
|
||||
searchPlaceholder={$t('请输入 APIURL 搜索')}
|
||||
columns={columns}
|
||||
/>
|
||||
</div>
|
||||
</InsidePage>
|
||||
)
|
||||
}
|
||||
|
||||
export default AIApis
|
||||
export default ApiSettings
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
export interface APIs {
|
||||
id: string;
|
||||
name: string;
|
||||
service: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
team:{
|
||||
id: string;
|
||||
name: string;
|
||||
},
|
||||
method: string;
|
||||
request_path: string;
|
||||
model: string;
|
||||
disabled: boolean;
|
||||
update_time: string;
|
||||
use_token: number;
|
||||
}
|
||||
@@ -1,166 +1,262 @@
|
||||
import InsidePage from '@common/components/aoplatform/InsidePage.tsx'
|
||||
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const.tsx'
|
||||
import { PERMISSION_DEFINITION } from '@common/const/permissions.ts'
|
||||
import { useGlobalContext } from '@common/contexts/GlobalStateContext.tsx'
|
||||
import { useFetch } from '@common/hooks/http.ts'
|
||||
import { $t } from '@common/locales/index.ts'
|
||||
import { getItem } from '@common/utils/navigation.tsx'
|
||||
import { RouterParams } from '@core/components/aoplatform/RenderRoutes.tsx'
|
||||
import { AiServiceConfigFieldType } from '@core/const/ai-service/type.ts'
|
||||
import { App, Menu, MenuProps } from 'antd'
|
||||
import { ItemType, MenuItemGroupType, MenuItemType } from 'antd/es/menu/interface'
|
||||
import Paragraph from 'antd/es/typography/Paragraph'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import { FC, useEffect, useMemo, useState } from 'react'
|
||||
import { Link, Outlet, useLocation, useNavigate, useParams } from 'react-router-dom'
|
||||
import { useAiServiceContext } from '../../contexts/AiServiceContext.tsx'
|
||||
const APP_MODE = import.meta.env.VITE_APP_MODE
|
||||
|
||||
import {FC, useEffect, useMemo, useState} from "react";
|
||||
import {Link, Outlet, useLocation, useNavigate, useParams} from "react-router-dom";
|
||||
import {RouterParams} from "@core/components/aoplatform/RenderRoutes.tsx";
|
||||
import {App, Menu, MenuProps} from "antd";
|
||||
import {BasicResponse, RESPONSE_TIPS, STATUS_CODE} from "@common/const/const.tsx";
|
||||
import {useFetch} from "@common/hooks/http.ts";
|
||||
import { useAiServiceContext} from "../../contexts/AiServiceContext.tsx";
|
||||
import { useGlobalContext } from "@common/contexts/GlobalStateContext.tsx";
|
||||
import { PERMISSION_DEFINITION } from "@common/const/permissions.ts";
|
||||
import InsidePage from "@common/components/aoplatform/InsidePage.tsx";
|
||||
import Paragraph from "antd/es/typography/Paragraph";
|
||||
import { cloneDeep } from "lodash-es";
|
||||
import { $t } from "@common/locales/index.ts";
|
||||
import { getItem } from "@common/utils/navigation.tsx";
|
||||
import { AiServiceConfigFieldType } from "@core/const/ai-service/type.ts";
|
||||
import { MenuItemGroupType, MenuItemType, ItemType } from "antd/es/menu/interface";
|
||||
const APP_MODE = import.meta.env.VITE_APP_MODE;
|
||||
const AiServiceInsidePage: FC = () => {
|
||||
const { message } = App.useApp()
|
||||
const { teamId, serviceId, apiId, routeId, policyId } = useParams<RouterParams>()
|
||||
const location = useLocation()
|
||||
const currentUrl = location.pathname
|
||||
const { fetchData } = useFetch()
|
||||
const { setPrefixForce, setApiPrefix, aiServiceInfo, setAiServiceInfo } = useAiServiceContext()
|
||||
const { accessData, checkPermission, accessInit, state } = useGlobalContext()
|
||||
const [activeMenu, setActiveMenu] = useState<string>()
|
||||
const navigateTo = useNavigate()
|
||||
const [showMenu, setShowMenu] = useState<boolean>(false)
|
||||
|
||||
const AiServiceInsidePage:FC = ()=> {
|
||||
const { message } = App.useApp()
|
||||
const { teamId,serviceId,apiId, routeId,policyId } = useParams<RouterParams>();
|
||||
const location = useLocation()
|
||||
const currentUrl = location.pathname
|
||||
const {fetchData} = useFetch()
|
||||
const { setPrefixForce,setApiPrefix ,aiServiceInfo ,setAiServiceInfo} = useAiServiceContext()
|
||||
const { accessData,checkPermission,accessInit,state} = useGlobalContext()
|
||||
const [activeMenu, setActiveMenu] = useState<string>()
|
||||
const navigateTo = useNavigate()
|
||||
const [showMenu, setShowMenu] = useState<boolean>(false)
|
||||
const getAiServiceInfo = () => {
|
||||
fetchData<BasicResponse<{ service: AiServiceConfigFieldType }>>('service/info', {
|
||||
method: 'GET',
|
||||
eoParams: { team: teamId, service: serviceId }
|
||||
}).then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
setAiServiceInfo(data.service)
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const getAiServiceInfo = ()=>{
|
||||
fetchData<BasicResponse<{ service:AiServiceConfigFieldType }>>('service/info',{method:'GET',eoParams:{team:teamId, service:serviceId}}).then(response=>{
|
||||
const {code,data,msg} = response
|
||||
if(code === STATUS_CODE.SUCCESS){
|
||||
setAiServiceInfo(data.service)
|
||||
}else{
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
}
|
||||
const getApiDefine = () => {
|
||||
setApiPrefix('')
|
||||
setPrefixForce(false)
|
||||
fetchData<BasicResponse<{ prefix: string; force: boolean }>>('service/router/define', {
|
||||
method: 'GET',
|
||||
eoParams: { service: serviceId, team: teamId }
|
||||
}).then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
setApiPrefix(data.prefix)
|
||||
setPrefixForce(data.force)
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
const getApiDefine = ()=>{
|
||||
setApiPrefix('')
|
||||
setPrefixForce(false)
|
||||
fetchData<BasicResponse<{ prefix:string, force:boolean }>>('service/router/define',{method:'GET',eoParams:{service:serviceId,team:teamId}}).then(response=>{
|
||||
const {code,data,msg} = response
|
||||
if(code === STATUS_CODE.SUCCESS){
|
||||
setApiPrefix(data.prefix)
|
||||
setPrefixForce(data.force)
|
||||
}else{
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
const SYSTEM_PAGE_MENU_ITEMS = useMemo(()=>[
|
||||
getItem($t('服务'), 'assets', null,
|
||||
const SYSTEM_PAGE_MENU_ITEMS = useMemo(
|
||||
() => [
|
||||
getItem(
|
||||
$t('服务'),
|
||||
'assets',
|
||||
null,
|
||||
[
|
||||
getItem(<Link to="./route">{$t('API 路由')}</Link>, 'route',undefined,undefined,undefined,'team.service.router.view'),
|
||||
getItem(<Link to="./api">{$t('API 文档')}</Link>, 'api',undefined,undefined,undefined,'team.service.api_doc.view'),
|
||||
getItem(<Link to="./document">{$t('使用说明')}</Link>, 'document',undefined,undefined,undefined,'team.service.service_intro.view'),
|
||||
getItem(<Link to="./servicepolicy">{$t('服务策略')}</Link>, 'servicepolicy', undefined, undefined, undefined, 'team.service.policy.view'),
|
||||
getItem(<Link to="./publish">{$t('发布')}</Link>, 'publish',undefined,undefined,undefined,'team.service.release.view'),
|
||||
],
|
||||
'group'),
|
||||
getItem($t('订阅管理'), 'provideSer', null,
|
||||
[
|
||||
getItem(<Link to="./approval">{$t('订阅审核')}</Link>, 'approval',undefined,undefined,undefined,'team.service.subscription.view'),
|
||||
getItem(<Link to="./subscriber">{$t('订阅方管理')}</Link>, 'subscriber',undefined,undefined,undefined,'team.service.subscription.view'),
|
||||
getItem(
|
||||
<Link to="./route">{$t('API 路由')}</Link>,
|
||||
'route',
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
'team.service.router.view'
|
||||
),
|
||||
getItem(
|
||||
<Link to="./api">{$t('API 文档')}</Link>,
|
||||
'api',
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
'team.service.api_doc.view'
|
||||
),
|
||||
getItem(
|
||||
<Link to="./document">{$t('使用说明')}</Link>,
|
||||
'document',
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
'team.service.service_intro.view'
|
||||
),
|
||||
getItem(
|
||||
<Link to="./servicepolicy">{$t('服务策略')}</Link>,
|
||||
'servicepolicy',
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
'team.service.policy.view'
|
||||
),
|
||||
getItem(
|
||||
<Link to="./publish">{$t('发布')}</Link>,
|
||||
'publish',
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
'team.service.release.view'
|
||||
)
|
||||
],
|
||||
'group'),
|
||||
getItem($t('管理'), 'mng', null,
|
||||
'group'
|
||||
),
|
||||
getItem(
|
||||
$t('订阅管理'),
|
||||
'provideSer',
|
||||
null,
|
||||
[
|
||||
APP_MODE === 'pro' ? getItem(<Link to="./topology">{$t('调用拓扑图')}</Link>, 'topology',undefined,undefined,undefined,'project.myAiService.topology.view'):null,
|
||||
getItem(<Link to="./setting">{$t('设置')}</Link>, 'setting',undefined,undefined,undefined,'')],
|
||||
'group'),
|
||||
],[state.language])
|
||||
getItem(
|
||||
<Link to="./approval">{$t('订阅审核')}</Link>,
|
||||
'approval',
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
'team.service.subscription.view'
|
||||
),
|
||||
getItem(
|
||||
<Link to="./subscriber">{$t('订阅方管理')}</Link>,
|
||||
'subscriber',
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
'team.service.subscription.view'
|
||||
)
|
||||
],
|
||||
'group'
|
||||
),
|
||||
getItem(
|
||||
$t('管理'),
|
||||
'mng',
|
||||
null,
|
||||
[
|
||||
APP_MODE === 'pro'
|
||||
? getItem(
|
||||
<Link to="./topology">{$t('调用拓扑图')}</Link>,
|
||||
'topology',
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
'project.myAiService.topology.view'
|
||||
)
|
||||
: null,
|
||||
getItem(<Link to="./setting">{$t('设置')}</Link>, 'setting', undefined, undefined, undefined, '')
|
||||
],
|
||||
'group'
|
||||
)
|
||||
],
|
||||
[state.language]
|
||||
)
|
||||
|
||||
|
||||
const menuData = useMemo(()=>{
|
||||
const filterMenu = (menu:MenuItemGroupType<MenuItemType>[])=>{
|
||||
const newMenu = cloneDeep(menu)
|
||||
return newMenu!.filter((m:MenuItemGroupType )=>{
|
||||
if(m&&m.children && m.children.length > 0){
|
||||
m.children = m.children.filter(
|
||||
(c)=>{
|
||||
if(!c) return false
|
||||
return (((c as MenuItemType&{access:string} ).access ?
|
||||
checkPermission((c as MenuItemType&{access:string} ).access as keyof typeof PERMISSION_DEFINITION[0]):
|
||||
true))})
|
||||
}
|
||||
return m.children && m.children.length > 0
|
||||
})
|
||||
const menuData = useMemo(() => {
|
||||
const filterMenu = (menu: MenuItemGroupType<MenuItemType>[]) => {
|
||||
const newMenu = cloneDeep(menu)
|
||||
return newMenu!.filter((m: MenuItemGroupType) => {
|
||||
if (m && m.children && m.children.length > 0) {
|
||||
m.children = m.children.filter((c) => {
|
||||
if (!c) return false
|
||||
return (c as MenuItemType & { access: string }).access
|
||||
? checkPermission(
|
||||
(c as MenuItemType & { access: string }).access as keyof (typeof PERMISSION_DEFINITION)[0]
|
||||
)
|
||||
: true
|
||||
})
|
||||
}
|
||||
const filteredMenu = filterMenu(SYSTEM_PAGE_MENU_ITEMS as MenuItemGroupType<MenuItemType>[])
|
||||
const menu = activeMenu ?? filteredMenu[0]?.children ? filteredMenu[0]?.children?.[0]?.key : filteredMenu[0]?.key
|
||||
if(menu && currentUrl.split('/')[-1] !== menu){
|
||||
navigateTo(`/service/${teamId}/aiInside/${serviceId}/${menu}`)
|
||||
}
|
||||
return filteredMenu || []
|
||||
},[accessData,accessInit, SYSTEM_PAGE_MENU_ITEMS])
|
||||
|
||||
const onMenuClick: MenuProps['onClick'] = ({key}) => {
|
||||
setActiveMenu(key)
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// route edit and policy edit page don't need to show menu
|
||||
setShowMenu(!routeId && !currentUrl.includes('route/create') && !policyId &&!currentUrl.includes('servicepolicy/datamasking/create'))
|
||||
return m.children && m.children.length > 0
|
||||
})
|
||||
}
|
||||
const filteredMenu = filterMenu(SYSTEM_PAGE_MENU_ITEMS as MenuItemGroupType<MenuItemType>[])
|
||||
const menu = (activeMenu ?? filteredMenu[0]?.children) ? filteredMenu[0]?.children?.[0]?.key : filteredMenu[0]?.key
|
||||
if (menu && currentUrl.split('/')[-1] !== menu) {
|
||||
navigateTo(`/service/${teamId}/aiInside/${serviceId}/${menu}`)
|
||||
}
|
||||
return filteredMenu || []
|
||||
}, [accessData, accessInit, SYSTEM_PAGE_MENU_ITEMS])
|
||||
|
||||
if(apiId !== undefined){
|
||||
setActiveMenu('api')
|
||||
} else if(currentUrl.includes('servicepolicy')){
|
||||
setActiveMenu('servicepolicy')
|
||||
} else if(serviceId !== currentUrl.split('/')[currentUrl.split('/').length - 1]){
|
||||
setActiveMenu(currentUrl.split('/')[currentUrl.split('/').length - 1])
|
||||
}else{
|
||||
setActiveMenu('route')
|
||||
}
|
||||
}, [currentUrl]);
|
||||
const onMenuClick: MenuProps['onClick'] = ({ key }) => {
|
||||
setActiveMenu(key)
|
||||
}
|
||||
|
||||
useEffect(()=>{
|
||||
if(accessData && checkPermission('team.service.router.view')){
|
||||
getApiDefine()
|
||||
}
|
||||
},[accessData])
|
||||
|
||||
useEffect(()=>{
|
||||
if( activeMenu && serviceId === currentUrl.split('/')[currentUrl.split('/').length - 1]){
|
||||
navigateTo(`/service/${teamId}/aiInside/${serviceId}/${activeMenu}`)
|
||||
}
|
||||
},[activeMenu])
|
||||
|
||||
useEffect(() => {
|
||||
serviceId && getAiServiceInfo()
|
||||
}, [serviceId]);
|
||||
|
||||
return (
|
||||
<>{showMenu ?
|
||||
<InsidePage pageTitle={aiServiceInfo?.name || '-'}
|
||||
tagList={[{label:
|
||||
<Paragraph className="mb-0" copyable={serviceId ? { text: serviceId } : false}>{$t('服务 ID')}:{serviceId || '-'}</Paragraph>
|
||||
}]}
|
||||
backUrl="/service/list">
|
||||
<div className="flex flex-1 h-full">
|
||||
<Menu
|
||||
onClick={onMenuClick}
|
||||
className="h-full overflow-y-auto"
|
||||
style={{ width: 220 }}
|
||||
selectedKeys={[activeMenu!]}
|
||||
mode="inline"
|
||||
items={menuData as unknown as ItemType<MenuItemType>[] }
|
||||
/>
|
||||
<div className={` ${['setting', 'upstream'].indexOf(activeMenu!) !== -1 ? '' :''} w-full h-full flex flex-1 flex-col overflow-auto bg-MAIN_BG pt-[20px] pl-[20px] pb-PAGE_INSIDE_B ` }>
|
||||
<Outlet/>
|
||||
</div>
|
||||
</div>
|
||||
</InsidePage>: <Outlet/> }
|
||||
</>
|
||||
useEffect(() => {
|
||||
// route edit and policy edit page don't need to show menu
|
||||
setShowMenu(
|
||||
!routeId &&
|
||||
!currentUrl.includes('route/create') &&
|
||||
!policyId &&
|
||||
!currentUrl.includes('servicepolicy/datamasking/create')
|
||||
)
|
||||
|
||||
if (apiId !== undefined) {
|
||||
setActiveMenu('api')
|
||||
} else if (currentUrl.includes('servicepolicy')) {
|
||||
setActiveMenu('servicepolicy')
|
||||
} else if (serviceId !== currentUrl.split('/')[currentUrl.split('/').length - 1]) {
|
||||
setActiveMenu(currentUrl.split('/')[currentUrl.split('/').length - 1])
|
||||
} else {
|
||||
setActiveMenu('route')
|
||||
}
|
||||
}, [currentUrl])
|
||||
|
||||
useEffect(() => {
|
||||
if (accessData && checkPermission('team.service.router.view')) {
|
||||
getApiDefine()
|
||||
}
|
||||
}, [accessData])
|
||||
|
||||
useEffect(() => {
|
||||
if (activeMenu && serviceId === currentUrl.split('/')[currentUrl.split('/').length - 1]) {
|
||||
navigateTo(`/service/${teamId}/aiInside/${serviceId}/${activeMenu}`)
|
||||
}
|
||||
}, [activeMenu])
|
||||
|
||||
useEffect(() => {
|
||||
serviceId && getAiServiceInfo()
|
||||
}, [serviceId])
|
||||
|
||||
return (
|
||||
<>
|
||||
{showMenu ? (
|
||||
<InsidePage
|
||||
pageTitle={aiServiceInfo?.name || '-'}
|
||||
tagList={[
|
||||
{
|
||||
label: (
|
||||
<Paragraph className="mb-0" copyable={serviceId ? { text: serviceId } : false}>
|
||||
{$t('服务 ID')}:{serviceId || '-'}
|
||||
</Paragraph>
|
||||
)
|
||||
}
|
||||
]}
|
||||
backUrl="/service/list"
|
||||
>
|
||||
<div className="flex flex-1 h-full">
|
||||
<Menu
|
||||
onClick={onMenuClick}
|
||||
className="overflow-y-auto h-full"
|
||||
style={{ width: 220 }}
|
||||
selectedKeys={[activeMenu!]}
|
||||
mode="inline"
|
||||
items={menuData as unknown as ItemType<MenuItemType>[]}
|
||||
/>
|
||||
<div
|
||||
className={` ${['setting', 'upstream'].indexOf(activeMenu!) !== -1 ? '' : ''} w-full h-full flex flex-1 flex-col overflow-auto bg-MAIN_BG pt-[20px] pl-[20px] pb-PAGE_INSIDE_B `}
|
||||
>
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
</InsidePage>
|
||||
) : (
|
||||
<Outlet />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default AiServiceInsidePage
|
||||
export default AiServiceInsidePage
|
||||
|
||||
@@ -1,354 +1,420 @@
|
||||
import {App, Button, Form, Input, InputNumber, Row, Select, Space, Spin, Tag} from "antd";
|
||||
import { MutableRefObject, useEffect, useMemo, useRef, useState} from "react";
|
||||
import {BasicResponse, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE} from "@common/const/const.tsx";
|
||||
import {useFetch} from "@common/hooks/http.ts";
|
||||
import { $t } from "@common/locales/index.ts";
|
||||
import { LoadingOutlined } from "@ant-design/icons";
|
||||
import InsidePage from "@common/components/aoplatform/InsidePage.tsx";
|
||||
import { Icon } from "@iconify/react/dist/iconify.js";
|
||||
import { RouterParams } from "@core/components/aoplatform/RenderRoutes.tsx";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { useAiServiceContext } from "@core/contexts/AiServiceContext.tsx";
|
||||
import EditableTableNotAutoGen from "@common/components/aoplatform/EditableTableNotAutoGen.tsx";
|
||||
import { AI_SERVICE_VARIABLES_TABLE_COLUMNS } from "@core/const/ai-service/const.tsx";
|
||||
import { VariableItems } from "@core/const/ai-service/type.ts";
|
||||
import PromptEditorResizable from '@common/components/aoplatform/prompt-editor/PromptEditorResizable.tsx';
|
||||
import { DrawerWithFooter } from "@common/components/aoplatform/DrawerWithFooter";
|
||||
import AiServiceRouterModelConfig, { AiServiceRouterModelConfigHandle } from "./AiServiceInsideRouterModelConfig";
|
||||
import { AiProviderDefaultConfig, AiProviderLlmsItems } from "@core/pages/aiSetting/AiSettingList";
|
||||
import { EditableFormInstance } from "@ant-design/pro-components";
|
||||
import { validateUrlSlash } from "@common/utils/validate";
|
||||
import { API_PATH_MATCH_RULES } from "@core/const/system/const";
|
||||
import { useGlobalContext } from "@common/contexts/GlobalStateContext";
|
||||
import { LoadingOutlined } from '@ant-design/icons'
|
||||
import { EditableFormInstance } from '@ant-design/pro-components'
|
||||
import { DrawerWithFooter } from '@common/components/aoplatform/DrawerWithFooter'
|
||||
import EditableTableNotAutoGen from '@common/components/aoplatform/EditableTableNotAutoGen.tsx'
|
||||
import InsidePage from '@common/components/aoplatform/InsidePage.tsx'
|
||||
import PromptEditorResizable from '@common/components/aoplatform/prompt-editor/PromptEditorResizable.tsx'
|
||||
import WithPermission from '@common/components/aoplatform/WithPermission'
|
||||
import { BasicResponse, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const.tsx'
|
||||
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
|
||||
import { useFetch } from '@common/hooks/http.ts'
|
||||
import { $t } from '@common/locales/index.ts'
|
||||
import { validateUrlSlash } from '@common/utils/validate'
|
||||
import { RouterParams } from '@core/components/aoplatform/RenderRoutes.tsx'
|
||||
import { AI_SERVICE_VARIABLES_TABLE_COLUMNS } from '@core/const/ai-service/const.tsx'
|
||||
import { VariableItems } from '@core/const/ai-service/type.ts'
|
||||
import { API_PATH_MATCH_RULES } from '@core/const/system/const'
|
||||
import { useAiServiceContext } from '@core/contexts/AiServiceContext.tsx'
|
||||
import { AiProviderDefaultConfig, AiProviderLlmsItems } from '@core/pages/aiSetting/AiSettingList'
|
||||
import { Icon } from '@iconify/react/dist/iconify.js'
|
||||
import { App, Button, Form, Input, InputNumber, Row, Space, Spin, Switch, Tag } from 'antd'
|
||||
import { MutableRefObject, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
import AiServiceRouterModelConfig, { AiServiceRouterModelConfigHandle } from './AiServiceInsideRouterModelConfig'
|
||||
|
||||
type AiServiceRouterField = {
|
||||
name:string
|
||||
path:string
|
||||
prompt:string
|
||||
variables:Array<{key:string, description:string, require:true}>
|
||||
description:string
|
||||
timeout:number
|
||||
retry:number
|
||||
name: string
|
||||
path: string
|
||||
prompt: string
|
||||
variables: Array<{ key: string; description: string; require: true }>
|
||||
description: string
|
||||
timeout: number
|
||||
retry: number
|
||||
disabled: boolean
|
||||
}
|
||||
|
||||
type AiServiceRouterConfig = {
|
||||
name:string
|
||||
path:string
|
||||
aiPrompt:{
|
||||
prompt:string
|
||||
variables:Array<{key:string, description:string, require:true}>
|
||||
}
|
||||
aiModel:{
|
||||
id:string
|
||||
config:string
|
||||
}
|
||||
description:string
|
||||
timeout:number
|
||||
retry:number
|
||||
name: string
|
||||
path: string
|
||||
aiPrompt: {
|
||||
prompt: string
|
||||
variables: Array<{ key: string; description: string; require: true }>
|
||||
}
|
||||
aiModel: {
|
||||
id: string
|
||||
config: string
|
||||
}
|
||||
description: string
|
||||
timeout: number
|
||||
retry: number
|
||||
}
|
||||
|
||||
const AiServiceInsideRouterCreate = () => {
|
||||
const navigator = useNavigate()
|
||||
const { message } = App.useApp()
|
||||
const {serviceId, teamId,routeId} = useParams<RouterParams>()
|
||||
const [form] = Form.useForm();
|
||||
const {fetchData} = useFetch()
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const {apiPrefix,prefixForce ,aiServiceInfo} = useAiServiceContext()
|
||||
const [variablesTable,setVariablesTable] = useState<VariableItems[]>([])
|
||||
const [drawerType,setDrawerType]= useState<'edit'|undefined>()
|
||||
const [open, setOpen] = useState(false);
|
||||
const drawerAddFormRef = useRef<AiServiceRouterModelConfigHandle>(null)
|
||||
const [defaultLlm, setDefaultLlm] = useState<AiProviderDefaultConfig & {config:string}>()
|
||||
const [llmList, setLlmList] = useState<AiProviderLlmsItems[]>([])
|
||||
const [variablesTableRef, setVariablesTableRef] = useState<MutableRefObject<EditableFormInstance<T> | undefined>>()
|
||||
const {state} = useGlobalContext()
|
||||
|
||||
const onFinish = ()=>{
|
||||
return variablesTableRef?.current?.validateFields().then(()=>{
|
||||
return form.validateFields().then((formValue)=>{
|
||||
const {name, path, description, variables, prompt, timeout, retry,pathMatch} = formValue
|
||||
const body = {
|
||||
name,
|
||||
path: `${prefixForce ? apiPrefix + '/' : ''}${path.trim()}${pathMatch === 'prefix' ? '/*' : ''}`,
|
||||
description,timeout, retry,aiPrompt:{variables:variables, prompt:prompt},aiModel:{id:defaultLlm?.id, provider:defaultLlm?.provider, config:defaultLlm?.config}}
|
||||
return fetchData<BasicResponse<null>>('service/ai-router',{method: routeId ? 'PUT' : 'POST',eoBody:(body), eoParams: {service:serviceId,team:teamId, ...(routeId ? {router:routeId}: {})},eoTransformKeys:['aiPrompt','aiModel']}).then(response=>{
|
||||
const {code,msg} = response
|
||||
if(code === STATUS_CODE.SUCCESS){
|
||||
message.success(msg || $t(RESPONSE_TIPS.success))
|
||||
navigator(`/service/${teamId}/aiInside/${serviceId}/route`)
|
||||
return Promise.resolve(true)
|
||||
}else{
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
return Promise.reject(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
}).catch(errInfo=>Promise.reject(errInfo))
|
||||
})
|
||||
})
|
||||
.catch(errInfo=>Promise.reject(errInfo))
|
||||
}
|
||||
const navigator = useNavigate()
|
||||
const { message } = App.useApp()
|
||||
const { serviceId, teamId, routeId, type } = useParams<RouterParams>()
|
||||
const [form] = Form.useForm()
|
||||
const { fetchData } = useFetch()
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const { apiPrefix, prefixForce, aiServiceInfo } = useAiServiceContext()
|
||||
const [variablesTable, setVariablesTable] = useState<VariableItems[]>([])
|
||||
const [drawerType, setDrawerType] = useState<'edit' | undefined>()
|
||||
const [open, setOpen] = useState(false)
|
||||
const drawerAddFormRef = useRef<AiServiceRouterModelConfigHandle>(null)
|
||||
const [defaultLlm, setDefaultLlm] = useState<AiProviderDefaultConfig & { config: string }>()
|
||||
const [llmList, setLlmList] = useState<AiProviderLlmsItems[]>([])
|
||||
const [variablesTableRef, setVariablesTableRef] = useState<MutableRefObject<EditableFormInstance<T> | undefined>>()
|
||||
const { state } = useGlobalContext()
|
||||
|
||||
const openDrawer = (type:'edit')=>{
|
||||
setDrawerType(type)
|
||||
}
|
||||
|
||||
useEffect(()=>{drawerType !== undefined ? setOpen(true):setOpen(false)},[drawerType])
|
||||
|
||||
const getRouterConfig = ()=>{
|
||||
setLoading(true)
|
||||
fetchData<BasicResponse<{api:AiServiceRouterConfig}>>('service/ai-router',{method:'GET',eoParams:{service:serviceId,team:teamId, router:routeId}, eoTransformKeys:['ai_model', 'ai_prompt']}).then(response=>{
|
||||
const {code,data,msg} = response
|
||||
if(code === STATUS_CODE.SUCCESS){
|
||||
const {path, aiPrompt,aiModel} = data.api
|
||||
let newPath = path
|
||||
let pathMatch = 'full'
|
||||
if(prefixForce && path?.startsWith(apiPrefix + '/')){
|
||||
newPath = path.slice((apiPrefix?.length || 0) + 1)
|
||||
}
|
||||
if(newPath.endsWith('/*')){
|
||||
newPath = newPath.slice(0,-2)
|
||||
pathMatch = 'prefix'
|
||||
}
|
||||
form.setFieldsValue({
|
||||
...data.api,
|
||||
...aiPrompt,
|
||||
path:newPath,
|
||||
pathMatch})
|
||||
setVariablesTable(aiPrompt.variables as VariableItems[])
|
||||
setDefaultLlm(prev => ({...prev, provider: aiModel?.provider, id:aiModel?.id, config:aiModel.config}) as (AiProviderDefaultConfig & { config: string; }))
|
||||
getDefaultModelConfig(aiModel?.provider)
|
||||
}else{
|
||||
const onFinish = () => {
|
||||
return variablesTableRef?.current
|
||||
?.validateFields()
|
||||
.then(() => {
|
||||
return form.validateFields().then((formValue) => {
|
||||
const { name, path, description, variables, prompt, timeout, retry, pathMatch, disabled } = formValue
|
||||
const body = {
|
||||
name,
|
||||
path: `${prefixForce ? apiPrefix + '/' : ''}${path.trim()}${pathMatch === 'prefix' ? '/*' : ''}`,
|
||||
description,
|
||||
timeout,
|
||||
retry,
|
||||
aiPrompt: { variables: variables, prompt: prompt },
|
||||
aiModel: { id: defaultLlm?.id, provider: defaultLlm?.provider, config: defaultLlm?.config },
|
||||
disabled
|
||||
}
|
||||
return fetchData<BasicResponse<null>>('service/ai-router', {
|
||||
method: routeId ? 'PUT' : 'POST',
|
||||
eoBody: body,
|
||||
eoParams: { service: serviceId, team: teamId, ...(routeId ? { router: routeId } : {}) },
|
||||
eoTransformKeys: ['aiPrompt', 'aiModel']
|
||||
})
|
||||
.then((response) => {
|
||||
const { code, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
message.success(msg || $t(RESPONSE_TIPS.success))
|
||||
navigator(`/service/${teamId}/aiInside/${serviceId}/route`)
|
||||
return Promise.resolve(true)
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
}).catch((errorInfo)=> console.error(errorInfo))
|
||||
.finally(()=>setLoading(false))
|
||||
}
|
||||
|
||||
const getDefaultModelConfig = (provider?:string)=>{
|
||||
fetchData<BasicResponse<{llms:AiProviderLlmsItems[],provider:AiProviderDefaultConfig}>>('ai/provider/llms',{method:'GET',eoParams:{provider:provider ?? aiServiceInfo?.provider?.id}, eoTransformKeys:['default_llm']}).then(response=>{
|
||||
const {code,data,msg} = response
|
||||
if(code === STATUS_CODE.SUCCESS){
|
||||
setLlmList(data.llms)
|
||||
setDefaultLlm(prev => {
|
||||
const llmSetting = data.llms?.find((x:AiProviderLlmsItems)=>x.id ===( prev?.id ?? data.provider.defaultLlm))
|
||||
return {...prev,
|
||||
defaultLlm:data.provider.defaultLlm,
|
||||
provider:data.provider.id,
|
||||
name:data.provider.name,
|
||||
config:llmSetting?.config || '',
|
||||
...(llmSetting ?? {})
|
||||
} as (AiProviderDefaultConfig & { config: string; })
|
||||
})
|
||||
}else{
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
}).catch((errorInfo)=> console.error(errorInfo))
|
||||
}
|
||||
|
||||
|
||||
useEffect(()=>{
|
||||
!routeId && aiServiceInfo?.provider && getDefaultModelConfig()
|
||||
},[
|
||||
aiServiceInfo
|
||||
])
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if(routeId){
|
||||
getRouterConfig()
|
||||
}else{
|
||||
form.setFieldsValue({
|
||||
prefix:apiPrefix,
|
||||
variables:[{key:'Query',value:'',require:true}],
|
||||
prompt:'{{Query}}',
|
||||
retry:0,
|
||||
timeout:300000,
|
||||
pathMatch:'prefix'
|
||||
return Promise.reject(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
}
|
||||
return (form.setFieldsValue({}))
|
||||
}, []);
|
||||
|
||||
const addVariable = ()=>{
|
||||
form.setFieldsValue({
|
||||
variables:[...form.getFieldValue('variables'),{key:'',value:'',require:true}]
|
||||
.catch((errInfo) => Promise.reject(errInfo))
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch((errInfo) => Promise.reject(errInfo))
|
||||
}
|
||||
const isAIApiPreview = type === 'apiDetail'
|
||||
const backUrl = isAIApiPreview ? `/aiApis/list` : `/service/${teamId}/aiInside/${serviceId}/route`
|
||||
const openDrawer = (type: 'edit') => {
|
||||
setDrawerType(type)
|
||||
}
|
||||
|
||||
const handleVariablesChange = (newKeys:string[])=>{
|
||||
const variables = form.getFieldValue('variables') || []
|
||||
const variablesKeys = variables?.map(({key}:{key:string})=>(key))
|
||||
for(const key of newKeys){
|
||||
if(!variablesKeys ||variablesKeys.indexOf(key) === -1){
|
||||
variables.push({key, value:'',require:true})
|
||||
}
|
||||
useEffect(() => {
|
||||
drawerType !== undefined ? setOpen(true) : setOpen(false)
|
||||
}, [drawerType])
|
||||
|
||||
const getRouterConfig = () => {
|
||||
setLoading(true)
|
||||
fetchData<BasicResponse<{ api: AiServiceRouterConfig }>>('service/ai-router', {
|
||||
method: 'GET',
|
||||
eoParams: { service: serviceId, team: teamId, router: routeId },
|
||||
eoTransformKeys: ['ai_model', 'ai_prompt']
|
||||
})
|
||||
.then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
const { path, aiPrompt, aiModel } = data.api
|
||||
let newPath = path
|
||||
let pathMatch = 'full'
|
||||
if (prefixForce && path?.startsWith(apiPrefix + '/')) {
|
||||
newPath = path.slice((apiPrefix?.length || 0) + 1)
|
||||
}
|
||||
if (newPath.endsWith('/*')) {
|
||||
newPath = newPath.slice(0, -2)
|
||||
pathMatch = 'prefix'
|
||||
}
|
||||
form.setFieldsValue({
|
||||
...data.api,
|
||||
...aiPrompt,
|
||||
path: newPath,
|
||||
pathMatch
|
||||
})
|
||||
setVariablesTable(aiPrompt.variables as VariableItems[])
|
||||
setDefaultLlm(
|
||||
(prev) =>
|
||||
({
|
||||
...prev,
|
||||
provider: aiModel?.provider,
|
||||
id: aiModel?.id,
|
||||
config: aiModel.config
|
||||
}) as AiProviderDefaultConfig & { config: string }
|
||||
)
|
||||
getDefaultModelConfig(aiModel?.provider)
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
form.setFieldsValue({
|
||||
variables:[...variables]
|
||||
})
|
||||
setVariablesTable(variables as VariableItems[])
|
||||
}
|
||||
})
|
||||
.catch((errorInfo) => console.error(errorInfo))
|
||||
.finally(() => setLoading(false))
|
||||
}
|
||||
|
||||
|
||||
const handleValuesChange = (changedValues:Record<string,unknown>) => {
|
||||
if(changedValues.variables){
|
||||
setVariablesTable(changedValues.variables as VariableItems[])
|
||||
const getDefaultModelConfig = (provider?: string) => {
|
||||
fetchData<BasicResponse<{ llms: AiProviderLlmsItems[]; provider: AiProviderDefaultConfig }>>('ai/provider/llms', {
|
||||
method: 'GET',
|
||||
eoParams: { provider: provider ?? aiServiceInfo?.provider?.id },
|
||||
eoTransformKeys: ['default_llm']
|
||||
})
|
||||
.then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
setLlmList(data.llms)
|
||||
setDefaultLlm((prev) => {
|
||||
const llmSetting = data.llms?.find(
|
||||
(x: AiProviderLlmsItems) => x.id === (prev?.id ?? data.provider.defaultLlm)
|
||||
)
|
||||
return {
|
||||
...prev,
|
||||
defaultLlm: data.provider.defaultLlm,
|
||||
provider: data.provider.id,
|
||||
name: data.provider.name,
|
||||
config: llmSetting?.config || '',
|
||||
...(llmSetting ?? {})
|
||||
} as AiProviderDefaultConfig & { config: string }
|
||||
})
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
};
|
||||
})
|
||||
.catch((errorInfo) => console.error(errorInfo))
|
||||
}
|
||||
|
||||
|
||||
const handlerSubmit:() => Promise<boolean>|undefined= ()=>{
|
||||
return drawerAddFormRef.current?.save()?.then((res:{id:string, config:string})=>{
|
||||
setDefaultLlm(prev => ({...prev, provider:res.provider, id:res.id, config:res.config, logo:llmList?.find((x:AiProviderLlmsItems)=>x.id === res.id)?.logo}) as (AiProviderDefaultConfig & { config: string; }))
|
||||
return true})
|
||||
useEffect(() => {
|
||||
!routeId && aiServiceInfo?.provider && getDefaultModelConfig()
|
||||
}, [aiServiceInfo])
|
||||
|
||||
useEffect(() => {
|
||||
if (routeId) {
|
||||
getRouterConfig()
|
||||
} else {
|
||||
form.setFieldsValue({
|
||||
prefix: apiPrefix,
|
||||
variables: [{ key: 'Query', value: '', require: true }],
|
||||
prompt: '{{Query}}',
|
||||
retry: 0,
|
||||
timeout: 300000,
|
||||
pathMatch: 'prefix'
|
||||
})
|
||||
}
|
||||
return form.setFieldsValue({})
|
||||
}, [])
|
||||
|
||||
const onClose = () => {
|
||||
setDrawerType(undefined);
|
||||
};
|
||||
const addVariable = () => {
|
||||
if (isAIApiPreview) return
|
||||
form.setFieldsValue({
|
||||
variables: [...form.getFieldValue('variables'), { key: '', value: '', require: true }]
|
||||
})
|
||||
}
|
||||
|
||||
const apiPathMatchRulesOptions = useMemo(()=>API_PATH_MATCH_RULES.map(
|
||||
x=>({label:$t(x.label), value:x.value})),[state.language])
|
||||
const handleVariablesChange = (newKeys: string[]) => {
|
||||
const variables = form.getFieldValue('variables') || []
|
||||
const variablesKeys = variables?.map(({ key }: { key: string }) => key)
|
||||
for (const key of newKeys) {
|
||||
if (!variablesKeys || variablesKeys.indexOf(key) === -1) {
|
||||
variables.push({ key, value: '', require: true })
|
||||
}
|
||||
}
|
||||
form.setFieldsValue({
|
||||
variables: [...variables]
|
||||
})
|
||||
setVariablesTable(variables as VariableItems[])
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
<InsidePage pageTitle={ $t('AI 路由设置')|| '-'}
|
||||
showBorder={false}
|
||||
scrollPage={false}
|
||||
className="overflow-y-auto"
|
||||
backUrl={`/service/${teamId}/aiInside/${serviceId}/route`}
|
||||
customBtn={
|
||||
<div className="flex items-center gap-btnbase">
|
||||
<Button icon={<Icon icon='ic:baseline-tune' height={18} width={18} />} iconPosition='end' onClick={()=>openDrawer('edit')}>
|
||||
<div className="flex items-center gap-[10px]">
|
||||
<span className="flex items-center h-[24px] ai-setting-svg-container " dangerouslySetInnerHTML={{__html: defaultLlm?.logo || ''}}></span>
|
||||
<span>{defaultLlm?.id || defaultLlm?.defaultLlm}</span>
|
||||
{defaultLlm?.scopes?.map(x=><Tag >{x?.toLocaleUpperCase()}</Tag>)}
|
||||
</div>
|
||||
</Button>
|
||||
|
||||
<Button type="primary" onClick={onFinish}>
|
||||
{$t('保存')}
|
||||
</Button>
|
||||
</div>
|
||||
}>
|
||||
<Spin indicator={<LoadingOutlined style={{ fontSize: 24 }} spin />} spinning={loading} wrapperClassName=' pb-PAGE_INSIDE_B pr-PAGE_INSIDE_X'>
|
||||
<Form
|
||||
layout='vertical'
|
||||
labelAlign='left'
|
||||
scrollToFirstError
|
||||
form={form}
|
||||
className="flex flex-col h-full mx-auto"
|
||||
name="AiServiceInsideRouterCreate"
|
||||
onValuesChange={handleValuesChange}
|
||||
onFinish={onFinish}
|
||||
autoComplete="off"
|
||||
const handleValuesChange = (changedValues: Record<string, unknown>) => {
|
||||
if (changedValues.variables) {
|
||||
setVariablesTable(changedValues.variables as VariableItems[])
|
||||
}
|
||||
}
|
||||
|
||||
const handlerSubmit: () => Promise<boolean> | undefined = () => {
|
||||
return drawerAddFormRef.current?.save()?.then((res: { id: string; config: string }) => {
|
||||
setDefaultLlm(
|
||||
(prev) =>
|
||||
({
|
||||
...prev,
|
||||
provider: res.provider,
|
||||
id: res.id,
|
||||
config: res.config,
|
||||
logo: llmList?.find((x: AiProviderLlmsItems) => x.id === res.id)?.logo
|
||||
}) as AiProviderDefaultConfig & { config: string }
|
||||
)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
const onClose = () => {
|
||||
setDrawerType(undefined)
|
||||
}
|
||||
|
||||
const apiPathMatchRulesOptions = useMemo(
|
||||
() => API_PATH_MATCH_RULES.map((x) => ({ label: $t(x.label), value: x.value })),
|
||||
[state.language]
|
||||
)
|
||||
|
||||
return (
|
||||
<InsidePage
|
||||
pageTitle={$t('AI 路由设置') || '-'}
|
||||
showBorder={false}
|
||||
scrollPage={false}
|
||||
className="overflow-y-auto"
|
||||
backUrl={backUrl}
|
||||
customBtn={
|
||||
<div className="flex items-center gap-btnbase">
|
||||
<Button
|
||||
icon={<Icon icon="ic:baseline-tune" height={18} width={18} />}
|
||||
iconPosition="end"
|
||||
disabled={isAIApiPreview}
|
||||
onClick={() => openDrawer('edit')}
|
||||
>
|
||||
<div className="flex items-center gap-[10px]">
|
||||
<span
|
||||
className="flex items-center h-[24px] ai-setting-svg-container "
|
||||
dangerouslySetInnerHTML={{ __html: defaultLlm?.logo || '' }}
|
||||
></span>
|
||||
<span>{defaultLlm?.id || defaultLlm?.defaultLlm}</span>
|
||||
{defaultLlm?.scopes?.map((x) => <Tag>{x?.toLocaleUpperCase()}</Tag>)}
|
||||
</div>
|
||||
</Button>
|
||||
{!isAIApiPreview && (
|
||||
<Button type="primary" onClick={onFinish}>
|
||||
{$t('保存')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Spin
|
||||
indicator={<LoadingOutlined style={{ fontSize: 24 }} spin />}
|
||||
spinning={loading}
|
||||
wrapperClassName=" pb-PAGE_INSIDE_B pr-PAGE_INSIDE_X"
|
||||
>
|
||||
<WithPermission disabled={isAIApiPreview}>
|
||||
<Form
|
||||
layout="vertical"
|
||||
labelAlign="left"
|
||||
scrollToFirstError
|
||||
form={form}
|
||||
className="flex flex-col mx-auto h-full"
|
||||
name="AiServiceInsideRouterCreate"
|
||||
onValuesChange={handleValuesChange}
|
||||
onFinish={onFinish}
|
||||
autoComplete="off"
|
||||
>
|
||||
<div className="">
|
||||
<Row className="flex justify-between items-center w-full gap-btnbase">
|
||||
<Form.Item<AiServiceRouterField>
|
||||
className="flex-1"
|
||||
label={$t('路由名称')}
|
||||
name="name"
|
||||
rules={[{ required: true, whitespace: true }]}
|
||||
>
|
||||
<div className="">
|
||||
<Row className="flex items-center justify-between w-full gap-btnbase">
|
||||
<Form.Item<AiServiceRouterField>
|
||||
className="flex-1"
|
||||
label={$t("路由名称")}
|
||||
name="name"
|
||||
rules={[{ required: true,whitespace:true }]}
|
||||
>
|
||||
<Input className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)}/>
|
||||
</Form.Item>
|
||||
|
||||
|
||||
<Form.Item className="flex-1" label={$t("请求路径")}>
|
||||
<Space.Compact block>
|
||||
<Form.Item
|
||||
name="pathMatch"
|
||||
rules={[{ required: true,whitespace:true },
|
||||
{
|
||||
validator: validateUrlSlash,
|
||||
}]}
|
||||
noStyle
|
||||
>
|
||||
<Select placeholder={$t(PLACEHOLDER.select)} options={apiPathMatchRulesOptions} className="w-[30%] min-w-[100px]"/>
|
||||
</Form.Item>
|
||||
<Form.Item<AiServiceRouterField>
|
||||
name="path"
|
||||
rules={[{ required: true,whitespace:true },
|
||||
{
|
||||
validator: validateUrlSlash,
|
||||
}]}
|
||||
noStyle
|
||||
>
|
||||
<Input prefix={(prefixForce ? `${apiPrefix}/` :"/")}
|
||||
placeholder={$t(PLACEHOLDER.input)} onChange={(e)=>{
|
||||
if((e.target.value as string).endsWith('/*')){
|
||||
form.setFieldValue('path',e.target.value.slice(0,-2))
|
||||
form.setFieldValue('pathMatch','prefix')
|
||||
}
|
||||
}}/>
|
||||
</Form.Item>
|
||||
</Space.Compact>
|
||||
</Form.Item>
|
||||
<Input className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)} />
|
||||
</Form.Item>
|
||||
|
||||
</Row>
|
||||
|
||||
<Form.Item<AiServiceRouterField>
|
||||
label={$t("提示词")}
|
||||
name="prompt"
|
||||
>
|
||||
<PromptEditorResizable variablesChange={handleVariablesChange} promptVariables={variablesTable}/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<AiServiceRouterField>
|
||||
label={<div className="flex items-center justify-between w-full"><span>{$t("变量")}</span><a className="flex items-center gap-[4px]" onClick={addVariable}><Icon icon="ic:baseline-add" width={16} height={16} />New</a></div>}
|
||||
name="variables"
|
||||
className="[&>.ant-row>.ant-col>label]:w-full"
|
||||
>
|
||||
<EditableTableNotAutoGen<VariableItems & {_id:string}>
|
||||
getFromRef={setVariablesTableRef}
|
||||
configFields={AI_SERVICE_VARIABLES_TABLE_COLUMNS}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<AiServiceRouterField>
|
||||
label={$t("描述")}
|
||||
name="description"
|
||||
>
|
||||
<Input.TextArea className="w-INPUT_NORMAL" placeholder={$t('输入这个接口的描述')}/>
|
||||
</Form.Item>
|
||||
|
||||
<Row className="flex items-center justify-between w-full gap-btnbase">
|
||||
<Form.Item<AiServiceRouterField>
|
||||
className="flex-1"
|
||||
label={$t("请求超时时间")}
|
||||
name={'timeout'}
|
||||
rules={[{required: true}]}
|
||||
>
|
||||
<InputNumber className="w-INPUT_NORMAL" suffix="ms" min={1} placeholder={$t(PLACEHOLDER.input)} />
|
||||
</Form.Item>
|
||||
<Form.Item<AiServiceRouterField>
|
||||
className="flex-1"
|
||||
label={$t("重试次数")}
|
||||
name={'retry'}
|
||||
rules={[{required: true}]}
|
||||
>
|
||||
<InputNumber className="w-INPUT_NORMAL" min={0} placeholder={$t(PLACEHOLDER.input)} />
|
||||
</Form.Item>
|
||||
</Row>
|
||||
|
||||
|
||||
</div>
|
||||
</Form>
|
||||
</Spin>
|
||||
<DrawerWithFooter
|
||||
title={ $t("模型配置")}
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
onSubmit={()=>handlerSubmit()}
|
||||
<Form.Item className="flex-1" label={$t('请求路径')}>
|
||||
<Space.Compact block>
|
||||
<Form.Item<AiServiceRouterField>
|
||||
name="path"
|
||||
rules={[
|
||||
{ required: true, whitespace: true },
|
||||
{
|
||||
validator: validateUrlSlash
|
||||
}
|
||||
]}
|
||||
noStyle
|
||||
>
|
||||
<AiServiceRouterModelConfig ref={drawerAddFormRef} llmList={llmList} entity={defaultLlm!} />
|
||||
</DrawerWithFooter>
|
||||
</InsidePage>
|
||||
)
|
||||
<Input
|
||||
prefix={prefixForce ? `${apiPrefix}/` : '/'}
|
||||
placeholder={$t(PLACEHOLDER.input)}
|
||||
onChange={(e) => {
|
||||
if ((e.target.value as string).endsWith('/*')) {
|
||||
form.setFieldValue('path', e.target.value.slice(0, -2))
|
||||
form.setFieldValue('pathMatch', 'prefix')
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Space.Compact>
|
||||
</Form.Item>
|
||||
</Row>
|
||||
|
||||
<Form.Item<AiServiceRouterField> label={$t('提示词')} name="prompt">
|
||||
<PromptEditorResizable
|
||||
disabled={isAIApiPreview}
|
||||
variablesChange={handleVariablesChange}
|
||||
promptVariables={variablesTable}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<AiServiceRouterField>
|
||||
label={
|
||||
<div className="flex justify-between items-center w-full">
|
||||
<span>{$t('变量')}</span>
|
||||
<a
|
||||
className={`flex items-center gap-[4px] ${isAIApiPreview ? 'cursor-not-allowed' : ''}`}
|
||||
onClick={addVariable}
|
||||
>
|
||||
<Icon icon="ic:baseline-add" width={16} height={16} />
|
||||
New
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
name="variables"
|
||||
className="[&>.ant-row>.ant-col>label]:w-full"
|
||||
>
|
||||
<EditableTableNotAutoGen<VariableItems & { _id: string }>
|
||||
getFromRef={setVariablesTableRef}
|
||||
configFields={AI_SERVICE_VARIABLES_TABLE_COLUMNS}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<AiServiceRouterField> label={$t('描述')} name="description">
|
||||
<Input.TextArea className="w-INPUT_NORMAL" placeholder={$t('输入这个接口的描述')} />
|
||||
</Form.Item>
|
||||
|
||||
<Row className="flex justify-between items-center w-full gap-btnbase">
|
||||
<Form.Item<AiServiceRouterField>
|
||||
className="flex-1"
|
||||
label={$t('请求超时时间')}
|
||||
name={'timeout'}
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<InputNumber className="w-INPUT_NORMAL" suffix="ms" min={1} placeholder={$t(PLACEHOLDER.input)} />
|
||||
</Form.Item>
|
||||
<Form.Item<AiServiceRouterField>
|
||||
className="flex-1"
|
||||
label={$t('重试次数')}
|
||||
name={'retry'}
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<InputNumber className="w-INPUT_NORMAL" min={0} placeholder={$t(PLACEHOLDER.input)} />
|
||||
</Form.Item>
|
||||
</Row>
|
||||
<Form.Item<AiServiceRouterField>
|
||||
label={$t('拦截接口')}
|
||||
name="disabled"
|
||||
extra={$t('开启拦截后,网关会拦截所有该路径的请求。')}
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</div>
|
||||
</Form>
|
||||
</WithPermission>
|
||||
</Spin>
|
||||
<DrawerWithFooter title={$t('模型配置')} open={open} onClose={onClose} onSubmit={() => handlerSubmit()}>
|
||||
<AiServiceRouterModelConfig ref={drawerAddFormRef} llmList={llmList} entity={defaultLlm!} />
|
||||
</DrawerWithFooter>
|
||||
</InsidePage>
|
||||
)
|
||||
}
|
||||
export default AiServiceInsideRouterCreate
|
||||
|
||||
|
||||
|
||||
@@ -1,182 +1,224 @@
|
||||
import PageList, { PageProColumns } from "@common/components/aoplatform/PageList.tsx"
|
||||
import {ActionType} from "@ant-design/pro-components";
|
||||
import {FC, useEffect, useMemo, useRef, useState} from "react";
|
||||
import {Link, useNavigate, useParams} from "react-router-dom";
|
||||
import {useBreadcrumb} from "@common/contexts/BreadcrumbContext.tsx";
|
||||
import {App, Divider} from "antd";
|
||||
import {BasicResponse, COLUMNS_TITLE, DELETE_TIPS, RESPONSE_TIPS, STATUS_CODE} from "@common/const/const.tsx";
|
||||
import { SimpleMemberItem} from '@common/const/type.ts'
|
||||
import {useFetch} from "@common/hooks/http.ts";
|
||||
import {RouterParams} from "@core/components/aoplatform/RenderRoutes.tsx";
|
||||
import TableBtnWithPermission from "@common/components/aoplatform/TableBtnWithPermission.tsx";
|
||||
import { useGlobalContext } from "@common/contexts/GlobalStateContext.tsx";
|
||||
import { checkAccess } from "@common/utils/permission.ts";
|
||||
import { $t } from "@common/locales/index.ts";
|
||||
import { AiServiceRouterTableListItem } from "@core/const/ai-service/type.ts";
|
||||
import { AI_SERVICE_ROUTER_TABLE_COLUMNS } from "@core/const/ai-service/const.tsx";
|
||||
import { ActionType } from '@ant-design/pro-components'
|
||||
import PageList, { PageProColumns } from '@common/components/aoplatform/PageList.tsx'
|
||||
import TableBtnWithPermission from '@common/components/aoplatform/TableBtnWithPermission.tsx'
|
||||
import { BasicResponse, COLUMNS_TITLE, DELETE_TIPS, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const.tsx'
|
||||
import { SimpleMemberItem } from '@common/const/type.ts'
|
||||
import { useBreadcrumb } from '@common/contexts/BreadcrumbContext.tsx'
|
||||
import { useGlobalContext } from '@common/contexts/GlobalStateContext.tsx'
|
||||
import { useFetch } from '@common/hooks/http.ts'
|
||||
import { $t } from '@common/locales/index.ts'
|
||||
import { checkAccess } from '@common/utils/permission.ts'
|
||||
import { RouterParams } from '@core/components/aoplatform/RenderRoutes.tsx'
|
||||
import { AI_SERVICE_ROUTER_TABLE_COLUMNS } from '@core/const/ai-service/const.tsx'
|
||||
import { AiServiceRouterTableListItem } from '@core/const/ai-service/type.ts'
|
||||
import { App, Divider, Typography } from 'antd'
|
||||
import { FC, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Link, useNavigate, useParams } from 'react-router-dom'
|
||||
|
||||
const AiServiceInsideRouterList:FC = ()=>{
|
||||
const [searchWord, setSearchWord] = useState<string>('')
|
||||
const { setBreadcrumb } = useBreadcrumb()
|
||||
const { modal,message } = App.useApp()
|
||||
const [tableListDataSource, setTableListDataSource] = useState<AiServiceRouterTableListItem[]>([]);
|
||||
const [tableHttpReload, setTableHttpReload] = useState(true);
|
||||
const {fetchData} = useFetch()
|
||||
const pageListRef = useRef<ActionType>(null);
|
||||
const [memberValueEnum, setMemberValueEnum] = useState<SimpleMemberItem[]>([])
|
||||
const {accessData,state} = useGlobalContext()
|
||||
const {serviceId, teamId} = useParams<RouterParams>()
|
||||
const navigator = useNavigate()
|
||||
const AiServiceInsideRouterList: FC = () => {
|
||||
const [searchWord, setSearchWord] = useState<string>('')
|
||||
const { setBreadcrumb } = useBreadcrumb()
|
||||
const { modal, message } = App.useApp()
|
||||
const [tableListDataSource, setTableListDataSource] = useState<AiServiceRouterTableListItem[]>([])
|
||||
const [tableHttpReload, setTableHttpReload] = useState(true)
|
||||
const { fetchData } = useFetch()
|
||||
const pageListRef = useRef<ActionType>(null)
|
||||
const [memberValueEnum, setMemberValueEnum] = useState<SimpleMemberItem[]>([])
|
||||
const { accessData, state } = useGlobalContext()
|
||||
const { serviceId, teamId } = useParams<RouterParams>()
|
||||
const navigator = useNavigate()
|
||||
|
||||
const getRoutesList = (): Promise<{ data: AiServiceRouterTableListItem[], success: boolean }>=> {
|
||||
if(!tableHttpReload){
|
||||
setTableHttpReload(true)
|
||||
return Promise.resolve({
|
||||
data: tableListDataSource,
|
||||
success: true,
|
||||
});
|
||||
}
|
||||
|
||||
return fetchData<BasicResponse<{apis:AiServiceRouterTableListItem}>>('service/ai-routers',{method:'GET',eoParams:{service:serviceId,team:teamId, keyword:searchWord},eoTransformKeys:['request_path','create_time','update_time','disable']}).then(response=>{
|
||||
const {code,data,msg} = response
|
||||
if(code === STATUS_CODE.SUCCESS){
|
||||
setTableListDataSource(data.apis)
|
||||
setTableHttpReload(false)
|
||||
return {data:data.apis, success: true}
|
||||
}else{
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
return {data:[], success:false}
|
||||
}
|
||||
}).catch(() => {
|
||||
return {data:[], success:false}
|
||||
})
|
||||
const getRoutesList = (): Promise<{ data: AiServiceRouterTableListItem[]; success: boolean }> => {
|
||||
if (!tableHttpReload) {
|
||||
setTableHttpReload(true)
|
||||
return Promise.resolve({
|
||||
data: tableListDataSource,
|
||||
success: true
|
||||
})
|
||||
}
|
||||
|
||||
const deleteRoute = (entity:AiServiceRouterTableListItem)=>{
|
||||
return new Promise((resolve, reject)=>{
|
||||
fetchData<BasicResponse<null>>('service/ai-router',{method:'DELETE',eoParams:{service:serviceId,team:teamId, router:entity!.id}}).then(response=>{
|
||||
const {code,msg} = response
|
||||
if(code === STATUS_CODE.SUCCESS){
|
||||
message.success(msg || $t(RESPONSE_TIPS.success))
|
||||
resolve(true)
|
||||
}else{
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
reject(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
}).catch((errorInfo)=> reject(errorInfo))
|
||||
})
|
||||
}
|
||||
|
||||
const openModal = async (type: 'delete',entity:AiServiceRouterTableListItem) =>{
|
||||
let title:string = ''
|
||||
let content:string|React.ReactNode = ''
|
||||
switch (type){
|
||||
case 'delete':
|
||||
title=$t('删除')
|
||||
content=$t(DELETE_TIPS.default)
|
||||
break;
|
||||
return fetchData<BasicResponse<{ apis: AiServiceRouterTableListItem }>>('service/ai-routers', {
|
||||
method: 'GET',
|
||||
eoParams: { service: serviceId, team: teamId, keyword: searchWord },
|
||||
eoTransformKeys: ['request_path', 'create_time', 'update_time', 'disable']
|
||||
})
|
||||
.then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
setTableListDataSource(data.apis)
|
||||
setTableHttpReload(false)
|
||||
return { data: data.apis, success: true }
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
return { data: [], success: false }
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
return { data: [], success: false }
|
||||
})
|
||||
}
|
||||
|
||||
modal.confirm({
|
||||
title,
|
||||
content,
|
||||
onOk:()=> {
|
||||
switch (type){
|
||||
case 'delete':
|
||||
return deleteRoute(entity).then((res)=>{if(res === true) manualReloadTable()})
|
||||
}
|
||||
},
|
||||
width:600,
|
||||
okText:$t('确认'),
|
||||
okButtonProps:{
|
||||
disabled : !checkAccess( `team.service.router.${type}`, accessData )
|
||||
},
|
||||
cancelText:$t('取消'),
|
||||
closable:true,
|
||||
icon:<></>,
|
||||
})
|
||||
}
|
||||
|
||||
const operation:PageProColumns<AiServiceRouterTableListItem>[] =[
|
||||
{
|
||||
title: COLUMNS_TITLE.operate,
|
||||
key: 'option',
|
||||
btnNums:2,
|
||||
fixed:'right',
|
||||
valueType: 'option',
|
||||
render: (_: React.ReactNode, entity: AiServiceRouterTableListItem) => [
|
||||
<TableBtnWithPermission access="team.service.router.edit" key="edit" btnType="edit" onClick={()=>{navigator(`/service/${teamId}/aiInside/${serviceId}/route/${entity.id}`)}} btnTitle="编辑"/>,
|
||||
<Divider type="vertical" className="mx-0" key="div3"/>,
|
||||
<TableBtnWithPermission access="team.service.router.delete" key="delete" btnType="delete" onClick={()=>{openModal('delete',entity)}} btnTitle="删除"/>,
|
||||
],
|
||||
}
|
||||
]
|
||||
|
||||
const manualReloadTable = () => {
|
||||
setTableHttpReload(true); // 表格数据需要从后端接口获取
|
||||
pageListRef.current?.reload()
|
||||
};
|
||||
|
||||
const getMemberList = async ()=>{
|
||||
setMemberValueEnum([])
|
||||
const {code,data,msg} = await fetchData<BasicResponse<{ members: SimpleMemberItem[] }>>('simple/member',{method:'GET'})
|
||||
if(code === STATUS_CODE.SUCCESS){
|
||||
setMemberValueEnum(data.members)
|
||||
}else{
|
||||
const deleteRoute = (entity: AiServiceRouterTableListItem) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
fetchData<BasicResponse<null>>('service/ai-router', {
|
||||
method: 'DELETE',
|
||||
eoParams: { service: serviceId, team: teamId, router: entity!.id }
|
||||
})
|
||||
.then((response) => {
|
||||
const { code, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
message.success(msg || $t(RESPONSE_TIPS.success))
|
||||
resolve(true)
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
reject(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
.catch((errorInfo) => reject(errorInfo))
|
||||
})
|
||||
}
|
||||
|
||||
const openModal = async (type: 'delete', entity: AiServiceRouterTableListItem) => {
|
||||
let title: string = ''
|
||||
let content: string | React.ReactNode = ''
|
||||
switch (type) {
|
||||
case 'delete':
|
||||
title = $t('删除')
|
||||
content = $t(DELETE_TIPS.default)
|
||||
break
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setBreadcrumb([
|
||||
{
|
||||
title:<Link to={`/service/list`}>{$t('服务')}</Link>
|
||||
},
|
||||
{
|
||||
title:$t('路由')
|
||||
}
|
||||
])
|
||||
getMemberList()
|
||||
manualReloadTable()
|
||||
}, [serviceId]);
|
||||
|
||||
const columns = useMemo(()=>{
|
||||
return [...AI_SERVICE_ROUTER_TABLE_COLUMNS].map(x=>{
|
||||
if(x.filters &&((x.dataIndex as string[])?.indexOf('creator') !== -1) ){
|
||||
const tmpValueEnum:{[k:string]:{text:string}} = {}
|
||||
memberValueEnum?.forEach((x:SimpleMemberItem)=>{
|
||||
tmpValueEnum[x.name] = {text:x.name}
|
||||
})
|
||||
x.valueEnum = tmpValueEnum
|
||||
}
|
||||
|
||||
return {...x,title:typeof x.title === 'string' ? $t(x.title as string) : x.title}})
|
||||
},[memberValueEnum,state.language])
|
||||
modal.confirm({
|
||||
title,
|
||||
content,
|
||||
onOk: () => {
|
||||
switch (type) {
|
||||
case 'delete':
|
||||
return deleteRoute(entity).then((res) => {
|
||||
if (res === true) manualReloadTable()
|
||||
})
|
||||
}
|
||||
},
|
||||
width: 600,
|
||||
okText: $t('确认'),
|
||||
okButtonProps: {
|
||||
disabled: !checkAccess(`team.service.router.${type}`, accessData)
|
||||
},
|
||||
cancelText: $t('取消'),
|
||||
closable: true,
|
||||
icon: <></>
|
||||
})
|
||||
}
|
||||
|
||||
const operation: PageProColumns<AiServiceRouterTableListItem>[] = [
|
||||
{
|
||||
title: COLUMNS_TITLE.operate,
|
||||
key: 'option',
|
||||
btnNums: 2,
|
||||
fixed: 'right',
|
||||
valueType: 'option',
|
||||
render: (_: React.ReactNode, entity: AiServiceRouterTableListItem) => [
|
||||
<TableBtnWithPermission
|
||||
access="team.service.router.edit"
|
||||
key="edit"
|
||||
btnType="edit"
|
||||
onClick={() => {
|
||||
navigator(`/service/${teamId}/aiInside/${serviceId}/route/${entity.id}`)
|
||||
}}
|
||||
btnTitle="编辑"
|
||||
/>,
|
||||
<Divider type="vertical" className="mx-0" key="div3" />,
|
||||
<TableBtnWithPermission
|
||||
access="team.service.router.delete"
|
||||
key="delete"
|
||||
btnType="delete"
|
||||
onClick={() => {
|
||||
openModal('delete', entity)
|
||||
}}
|
||||
btnTitle="删除"
|
||||
/>
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageList
|
||||
id="global_system_api"
|
||||
ref={pageListRef}
|
||||
columns = {[...columns,...operation]}
|
||||
request={()=>getRoutesList()}
|
||||
dataSource={tableListDataSource}
|
||||
addNewBtnTitle={$t('添加路由')}
|
||||
searchPlaceholder={$t('输入 URL 查找路由')}
|
||||
onAddNewBtnClick={()=>{navigator(`/service/${teamId}/aiInside/${serviceId}/route/create`)}}
|
||||
addNewBtnAccess="team.service.router.add"
|
||||
tableClickAccess="team.service.router.view"
|
||||
manualReloadTable={manualReloadTable}
|
||||
onSearchWordChange={(e)=>{setSearchWord(e.target.value)}}
|
||||
onChange={() => {
|
||||
setTableHttpReload(false)
|
||||
}}
|
||||
onRowClick={(row:AiServiceRouterTableListItem)=>navigator(`/service/${teamId}/aiInside/${serviceId}/route/${row.id}`)}
|
||||
tableClass="mr-PAGE_INSIDE_X "
|
||||
/>
|
||||
</>
|
||||
)
|
||||
const manualReloadTable = () => {
|
||||
setTableHttpReload(true) // 表格数据需要从后端接口获取
|
||||
pageListRef.current?.reload()
|
||||
}
|
||||
|
||||
const getMemberList = async () => {
|
||||
setMemberValueEnum([])
|
||||
const { code, data, msg } = await fetchData<BasicResponse<{ members: SimpleMemberItem[] }>>('simple/member', {
|
||||
method: 'GET'
|
||||
})
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
setMemberValueEnum(data.members)
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setBreadcrumb([
|
||||
{
|
||||
title: <Link to={`/service/list`}>{$t('服务')}</Link>
|
||||
},
|
||||
{
|
||||
title: $t('路由')
|
||||
}
|
||||
])
|
||||
getMemberList()
|
||||
manualReloadTable()
|
||||
}, [serviceId])
|
||||
|
||||
const columns = useMemo(() => {
|
||||
return [...AI_SERVICE_ROUTER_TABLE_COLUMNS].map((x) => {
|
||||
if (x.filters && (x.dataIndex as string[])?.indexOf('creator') !== -1) {
|
||||
const tmpValueEnum: { [k: string]: { text: string } } = {}
|
||||
memberValueEnum?.forEach((x: SimpleMemberItem) => {
|
||||
tmpValueEnum[x.name] = { text: x.name }
|
||||
})
|
||||
x.valueEnum = tmpValueEnum
|
||||
}
|
||||
if (x.filters && (x.dataIndex as string[])?.indexOf('disabled') !== -1) {
|
||||
x.valueEnum = {
|
||||
true: { text: <Typography.Text type="danger">{$t('拦截')}</Typography.Text> },
|
||||
false: { text: <Typography.Text type="success">{$t('放行')}</Typography.Text> }
|
||||
}
|
||||
}
|
||||
|
||||
return { ...x, title: typeof x.title === 'string' ? $t(x.title as string) : x.title }
|
||||
})
|
||||
}, [memberValueEnum, state.language])
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageList
|
||||
id="global_system_api"
|
||||
ref={pageListRef}
|
||||
columns={[...columns, ...operation]}
|
||||
request={() => getRoutesList()}
|
||||
dataSource={tableListDataSource}
|
||||
addNewBtnTitle={$t('添加路由')}
|
||||
searchPlaceholder={$t('输入 URL 查找路由')}
|
||||
onAddNewBtnClick={() => {
|
||||
navigator(`/service/${teamId}/aiInside/${serviceId}/route/create`)
|
||||
}}
|
||||
addNewBtnAccess="team.service.router.add"
|
||||
tableClickAccess="team.service.router.view"
|
||||
manualReloadTable={manualReloadTable}
|
||||
onSearchWordChange={(e) => {
|
||||
setSearchWord(e.target.value)
|
||||
}}
|
||||
onChange={() => {
|
||||
setTableHttpReload(false)
|
||||
}}
|
||||
onRowClick={(row: AiServiceRouterTableListItem) =>
|
||||
navigator(`/service/${teamId}/aiInside/${serviceId}/route/${row.id}`)
|
||||
}
|
||||
tableClass="mr-PAGE_INSIDE_X "
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default AiServiceInsideRouterList
|
||||
export default AiServiceInsideRouterList
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
'use client'
|
||||
|
||||
import { BasicResponse } from '@common/const/const'
|
||||
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
|
||||
import { useFetch } from '@common/hooks/http'
|
||||
import { $t } from '@common/locales'
|
||||
import {
|
||||
CoordinateExtent,
|
||||
Edge,
|
||||
@@ -13,28 +16,26 @@ import {
|
||||
useNodesState
|
||||
} from '@xyflow/react'
|
||||
import '@xyflow/react/dist/style.css'
|
||||
import { Button, Space, Spin } from 'antd'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import CustomEdge from './components/CustomEdge'
|
||||
import { KeyStatusNode } from './components/KeyStatusNode'
|
||||
import { ModelCardNode } from './components/ModelCardNode'
|
||||
import { ServiceCardNode } from './components/NodeComponents'
|
||||
import { LAYOUT } from './constants'
|
||||
import './styles.css'
|
||||
import { ModelData } from './types'
|
||||
import { ModelListData } from './types'
|
||||
|
||||
interface ApiResponse {
|
||||
data: {
|
||||
backup: {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
providers: ModelData[]
|
||||
export type ApiResponse = BasicResponse<{
|
||||
backup: {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
code: number
|
||||
success: string
|
||||
}
|
||||
providers: ModelListData[]
|
||||
}>
|
||||
|
||||
const calculateNodePositions = (models: ModelData[], startY = LAYOUT.NODE_START_Y, gap = LAYOUT.NODE_GAP) => {
|
||||
const calculateNodePositions = (models: ModelListData[], startY = LAYOUT.NODE_START_Y, gap = LAYOUT.NODE_GAP) => {
|
||||
return models.reduce(
|
||||
(acc, model, index) => {
|
||||
const y = startY + index * gap
|
||||
@@ -46,7 +47,7 @@ const calculateNodePositions = (models: ModelData[], startY = LAYOUT.NODE_START_
|
||||
},
|
||||
[`${model.id}-keys`]: {
|
||||
x: LAYOUT.KEY_NODE_X,
|
||||
y
|
||||
y: y + 16
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -65,36 +66,47 @@ const edgeTypes: EdgeTypes = {
|
||||
}
|
||||
|
||||
const AIFlowChart = () => {
|
||||
const [modelData, setModelData] = useState<ModelData[]>([])
|
||||
const [modelData, setModelData] = useState<ModelListData[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState<Node>([])
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([])
|
||||
const { fetchData } = useFetch()
|
||||
const { aiConfigFlushed } = useGlobalContext()
|
||||
const navigate = useNavigate()
|
||||
|
||||
useEffect(() => {
|
||||
// Mock API call - replace with actual API call
|
||||
setLoading(true)
|
||||
fetchData<ApiResponse>('ai/providers/configured', {
|
||||
method: 'GET'
|
||||
method: 'GET',
|
||||
eoTransformKeys: ['default_llm']
|
||||
// eoApiPrefix: 'http://uat.apikit.com:11204/mockApi/aoplatform/api/v1/'
|
||||
}).then((response) => {
|
||||
const mockApiResponse: ApiResponse = response as ApiResponse
|
||||
setModelData(mockApiResponse.data.providers)
|
||||
})
|
||||
}, [])
|
||||
.then((response) => {
|
||||
const mockApiResponse: ApiResponse = response as ApiResponse
|
||||
setModelData(mockApiResponse.data.providers)
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
}, [aiConfigFlushed])
|
||||
|
||||
useEffect(() => {
|
||||
if (!modelData.length) return
|
||||
|
||||
const positions = calculateNodePositions(modelData)
|
||||
const firstSuccessModel = modelData.find((model) => model.status === 'enabled')
|
||||
console.log(firstSuccessModel)
|
||||
|
||||
// subtract 5 to make sure the service node is aligned with the top model node
|
||||
const serviceY = positions[modelData[0].id].y - 5
|
||||
|
||||
const newNodes = [
|
||||
{
|
||||
id: 'apiService',
|
||||
type: 'serviceCard',
|
||||
position: { x: LAYOUT.SERVICE_NODE_X, y: serviceY },
|
||||
draggable: false,
|
||||
data: {
|
||||
title: 'API Service',
|
||||
title: 'API Services',
|
||||
count: modelData.length
|
||||
}
|
||||
},
|
||||
@@ -103,10 +115,12 @@ const AIFlowChart = () => {
|
||||
type: 'modelCard',
|
||||
position: positions[model.id],
|
||||
data: {
|
||||
title: model.name,
|
||||
name: model.name,
|
||||
status: model.status,
|
||||
defaultModel: model.default_llm,
|
||||
logo: model.logo
|
||||
defaultLlm: model.defaultLlm,
|
||||
logo: model.logo,
|
||||
id: model.id,
|
||||
alternativeModel: firstSuccessModel
|
||||
}
|
||||
})),
|
||||
...modelData.map((model) => ({
|
||||
@@ -114,7 +128,7 @@ const AIFlowChart = () => {
|
||||
type: 'keyCard',
|
||||
position: positions[`${model.id}-keys`],
|
||||
data: {
|
||||
title: 'API Keys',
|
||||
title: '',
|
||||
keys: (model.keys || []).map((key, index) => ({
|
||||
id: key.id,
|
||||
status: key.status,
|
||||
@@ -130,25 +144,29 @@ const AIFlowChart = () => {
|
||||
source: 'apiService',
|
||||
target: model.id,
|
||||
label: `${model.api_count} apis`,
|
||||
data: { id: model.id },
|
||||
animated: true
|
||||
data: {
|
||||
id: model.id,
|
||||
status: model.status
|
||||
},
|
||||
animated: true,
|
||||
style: { stroke: model.status === 'enabled' ? '#52c41a' : '#ff4d4f' }
|
||||
})),
|
||||
...modelData.map((model) => ({
|
||||
id: `${model.id}-keys-edge`,
|
||||
source: model.id,
|
||||
target: `${model.id}-keys`,
|
||||
label: `${model.key_count} keys`,
|
||||
data: { id: model.id },
|
||||
animated: true
|
||||
}))
|
||||
]
|
||||
|
||||
setNodes(newNodes)
|
||||
setEdges(newEdges)
|
||||
}, [modelData])
|
||||
|
||||
const calculateExtent = useCallback(() => {
|
||||
const left = LAYOUT.SERVICE_NODE_X - 100
|
||||
const right = LAYOUT.KEY_NODE_X + 100
|
||||
const left = LAYOUT.SERVICE_NODE_X
|
||||
const right = LAYOUT.KEY_NODE_X
|
||||
const top = 0 // Allow slight negative scroll to reduce top padding
|
||||
const bottom = LAYOUT.NODE_START_Y + modelData.length * LAYOUT.NODE_GAP
|
||||
return [
|
||||
@@ -157,91 +175,93 @@ const AIFlowChart = () => {
|
||||
] as CoordinateExtent
|
||||
}, [modelData.length])
|
||||
|
||||
const onNodeDrag: any = useCallback(
|
||||
(_: MouseEvent, node: Node<any>) => {
|
||||
if (node.type !== 'modelCard') return
|
||||
const updateProviderOrder = async (sortedProviderIds: string[]) => {
|
||||
await fetchData('ai/provider/sort', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({
|
||||
providers: sortedProviderIds
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
setNodes((nds) => {
|
||||
return nds.map((n) => {
|
||||
if (n.type === 'keyCard' && n.id === `${node.id}-keys`) {
|
||||
const onNodeDragStop: any = useCallback((_: any, node: Node<any>) => {
|
||||
if (node.type !== 'modelCard') return
|
||||
|
||||
setNodes((nds) => {
|
||||
const modelNodes = nds.filter((n) => n.type === 'modelCard')
|
||||
const sortedNodes = [...modelNodes].sort((a, b) => a.position.y - b.position.y)
|
||||
const sortedProviderIds = sortedNodes.map((node) => node.id)
|
||||
|
||||
// Update provider order outside of setNodes callback
|
||||
updateProviderOrder(sortedProviderIds)
|
||||
// Update all node positions in a single pass
|
||||
return nds.map((n) => {
|
||||
if (n.type === 'modelCard') {
|
||||
const index = sortedNodes.findIndex((sn) => sn.id === n.id)
|
||||
return {
|
||||
...n,
|
||||
position: {
|
||||
x: LAYOUT.MODEL_NODE_X,
|
||||
y: LAYOUT.NODE_START_Y + index * LAYOUT.NODE_GAP
|
||||
}
|
||||
}
|
||||
}
|
||||
if (n.type === 'keyCard') {
|
||||
const modelId = n.id.replace('-keys', '')
|
||||
const modelNode = sortedNodes.find((mn) => mn.id === modelId)
|
||||
if (modelNode) {
|
||||
const index = sortedNodes.findIndex((sn) => sn.id === modelId)
|
||||
return {
|
||||
...n,
|
||||
position: {
|
||||
x: LAYOUT.KEY_NODE_X,
|
||||
y: node.position.y
|
||||
y: LAYOUT.NODE_START_Y + index * LAYOUT.NODE_GAP + 16
|
||||
}
|
||||
}
|
||||
}
|
||||
return n
|
||||
})
|
||||
}
|
||||
return n
|
||||
})
|
||||
},
|
||||
[setNodes]
|
||||
)
|
||||
|
||||
const onNodeDragStop: any = useCallback(
|
||||
(_: any, node: Node<any>) => {
|
||||
if (node.type !== 'modelCard') return
|
||||
|
||||
setNodes((nds) => {
|
||||
const modelNodes = nds.filter((n) => n.type === 'modelCard')
|
||||
const sortedNodes = [...modelNodes].sort((a, b) => a.position.y - b.position.y)
|
||||
|
||||
return nds.map((n) => {
|
||||
if (n.type === 'modelCard') {
|
||||
const index = sortedNodes.findIndex((sn) => sn.id === n.id)
|
||||
return {
|
||||
...n,
|
||||
position: {
|
||||
x: LAYOUT.MODEL_NODE_X,
|
||||
y: LAYOUT.NODE_START_Y + index * LAYOUT.NODE_GAP
|
||||
}
|
||||
}
|
||||
}
|
||||
if (n.type === 'keyCard') {
|
||||
const modelId = n.id.replace('-keys', '')
|
||||
const modelNode = sortedNodes.find((mn) => mn.id === modelId)
|
||||
if (modelNode) {
|
||||
const index = sortedNodes.findIndex((sn) => sn.id === modelId)
|
||||
return {
|
||||
...n,
|
||||
position: {
|
||||
x: LAYOUT.KEY_NODE_X,
|
||||
y: LAYOUT.NODE_START_Y + index * LAYOUT.NODE_GAP
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return n
|
||||
})
|
||||
})
|
||||
},
|
||||
[setNodes]
|
||||
)
|
||||
})
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="w-full h-full" style={{ height: 'calc(100vh - 64px)' }}>
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onNodeDrag={onNodeDrag}
|
||||
proOptions={{ hideAttribution: true }}
|
||||
onNodeDragStop={onNodeDragStop}
|
||||
draggable={false}
|
||||
nodeTypes={nodeTypes}
|
||||
edgeTypes={edgeTypes}
|
||||
zoomOnScroll={false}
|
||||
zoomOnPinch={false}
|
||||
zoomOnDoubleClick={false}
|
||||
panOnScroll={true}
|
||||
panOnScrollMode={PanOnScrollMode.Vertical}
|
||||
defaultEdgeOptions={{
|
||||
type: 'custom'
|
||||
}}
|
||||
translateExtent={calculateExtent()}
|
||||
/>
|
||||
<div className="w-full h-full">
|
||||
{loading ? (
|
||||
<div className="flex justify-center items-center h-full">
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
) : modelData.length === 0 ? (
|
||||
<Space className="flex flex-col justify-center items-center h-[200px]">
|
||||
<div>{$t('未配置 AI 模型')}</div>
|
||||
<Button type="primary" onClick={() => navigate('/aisetting?status=unconfigure')}>
|
||||
{$t('前往设置')}
|
||||
</Button>
|
||||
</Space>
|
||||
) : (
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onNodeDragStop={onNodeDragStop}
|
||||
proOptions={{ hideAttribution: true }}
|
||||
draggable={false}
|
||||
nodeTypes={nodeTypes}
|
||||
elementsSelectable={false}
|
||||
edgeTypes={edgeTypes}
|
||||
zoomOnScroll={false}
|
||||
panOnDrag={false}
|
||||
zoomOnPinch={false}
|
||||
zoomOnDoubleClick={false}
|
||||
panOnScroll={true}
|
||||
panOnScrollMode={PanOnScrollMode.Vertical}
|
||||
defaultEdgeOptions={{
|
||||
type: 'custom'
|
||||
}}
|
||||
translateExtent={calculateExtent()}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
import Icon, { LoadingOutlined } from '@ant-design/icons'
|
||||
import WithPermission from '@common/components/aoplatform/WithPermission'
|
||||
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 { App, Button, Card, Empty, Spin, Tag } from 'antd'
|
||||
import { memo, useEffect, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useAiSetting } from './contexts/AiSettingContext'
|
||||
import { AiSettingListItem } from './types'
|
||||
|
||||
const CardBox = memo(({ provider }: { provider: AiSettingListItem }) => {
|
||||
const { openConfigModal } = useAiSetting()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const handleOpenModal = async (provider: AiSettingListItem) => {
|
||||
await openConfigModal(provider)
|
||||
navigate('/aisetting?status=configure')
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={
|
||||
<div className="flex w-full items-center justify-between gap-[4px]">
|
||||
<div className="flex flex-1 overflow-hidden items-center gap-[4px]">
|
||||
<span
|
||||
className=" flex items-center h-[22px] ai-setting-svg-container"
|
||||
dangerouslySetInnerHTML={{ __html: provider.logo }}
|
||||
></span>
|
||||
<span className="font-normal truncate">{provider.name}</span>
|
||||
</div>
|
||||
<Tag
|
||||
bordered={false}
|
||||
color={provider.configured ? 'green' : undefined}
|
||||
className="h-[22px] px-[4px] text-center"
|
||||
>
|
||||
{provider.configured ? $t('已配置') : $t('未配置')}
|
||||
</Tag>
|
||||
</div>
|
||||
}
|
||||
className="shadow-[0_5px_10px_0_rgba(0,0,0,0.05)] rounded-[10px] overflow-visible h-[156px] m-0 flex flex-col "
|
||||
classNames={{ header: 'border-b-[0px] p-[20px] px-[24px]', body: 'pt-0 flex-1' }}
|
||||
>
|
||||
<div className="flex flex-col justify-between h-full gap-btnbase">
|
||||
<div className="flex items-center w-full h-[32px] flex-1">
|
||||
{provider.configured && (
|
||||
<>
|
||||
<label className="text-nowrap">{$t('默认')}:</label>
|
||||
<span className="overflow-hidden flex-1 truncate">{provider.defaultLlm}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<WithPermission access="system.settings.ai_provider.view">
|
||||
<Button
|
||||
block
|
||||
icon={<Icon icon="ic:outline-settings" width={18} height={18} />}
|
||||
onClick={() => handleOpenModal(provider)}
|
||||
classNames={{ icon: 'h-[18px]' }}
|
||||
>
|
||||
{$t('设置')}
|
||||
</Button>
|
||||
</WithPermission>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
})
|
||||
const ModelCardArea = ({ modelList, className }: { modelList: AiSettingListItem[]; className?: string }) => {
|
||||
return (
|
||||
<>
|
||||
{modelList.length > 0 ? (
|
||||
<div
|
||||
className={className}
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))',
|
||||
gap: '20px'
|
||||
}}
|
||||
>
|
||||
{modelList.map((provider: AiSettingListItem) => (
|
||||
<CardBox key={provider.id} provider={provider} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const AIUnConfigure = () => {
|
||||
const [modelData, setModelData] = useState<AiSettingListItem[]>([])
|
||||
const { fetchData } = useFetch()
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const { aiConfigFlushed } = useGlobalContext()
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true)
|
||||
fetchData<BasicResponse<{ providers: Omit<AiSettingListItem>[] }>>(`ai/providers/unconfigured`, {
|
||||
method: 'GET',
|
||||
eoTransformKeys: ['default_llm', 'default_llm_logo']
|
||||
})
|
||||
.then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
setModelData(data.providers)
|
||||
} else {
|
||||
const { message } = App.useApp()
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
.finally(() => setLoading(false))
|
||||
}, [aiConfigFlushed])
|
||||
|
||||
return (
|
||||
<Spin
|
||||
className="h-full"
|
||||
wrapperClassName="h-full pr-PAGE_INSIDE_X"
|
||||
indicator={<LoadingOutlined style={{ fontSize: 24 }} spin />}
|
||||
spinning={loading}
|
||||
>
|
||||
{modelData && modelData.length > 0 ? (
|
||||
<div>
|
||||
{modelData.filter((item) => !item.configured).length > 0 && (
|
||||
<>
|
||||
<ModelCardArea modelList={modelData.filter((item) => !item.configured) || []} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
)}
|
||||
</Spin>
|
||||
)
|
||||
}
|
||||
export default AIUnConfigure
|
||||
@@ -1,243 +1,71 @@
|
||||
import { LoadingOutlined } from '@ant-design/icons'
|
||||
import InsidePage from '@common/components/aoplatform/InsidePage'
|
||||
import WithPermission from '@common/components/aoplatform/WithPermission'
|
||||
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 { checkAccess } from '@common/utils/permission'
|
||||
import { Icon } from '@iconify/react/dist/iconify.js'
|
||||
import { App, Button, Card, Divider, Empty, Spin, Tag } from 'antd'
|
||||
import { memo, useEffect, useRef, useState } from 'react'
|
||||
import { Tabs } from 'antd'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useSearchParams } from 'react-router-dom'
|
||||
import AIFlowChart from './AIFlowChart'
|
||||
import AiSettingModalContent, { AiSettingModalContentHandle } from './AiSettingModal'
|
||||
import AIUnConfigure from './AIUnconfigure'
|
||||
import { AiSettingProvider } from './contexts/AiSettingContext'
|
||||
|
||||
export type AiSettingListItem = {
|
||||
name: string
|
||||
id: string
|
||||
logo: string
|
||||
defaultLlm: string
|
||||
defaultLlmLogo: string
|
||||
enable: boolean
|
||||
configured: boolean
|
||||
}
|
||||
const CONTENT_STYLE = { height: 'calc(-300px + 100vh)' } as const
|
||||
|
||||
export type AiProviderLlmsItems = {
|
||||
id: string
|
||||
logo: string
|
||||
scopes: ('chat' | 'completions')[]
|
||||
config: string
|
||||
}
|
||||
|
||||
export type AiProviderDefaultConfig = {
|
||||
id: string
|
||||
provider: string
|
||||
name: string
|
||||
logo: string
|
||||
defaultLlm: string
|
||||
scopes: string[]
|
||||
}
|
||||
|
||||
export type AiProviderConfig = {
|
||||
id: string
|
||||
name: string
|
||||
config: string
|
||||
getApikeyUrl: string
|
||||
}
|
||||
const AiSettingList = () => {
|
||||
const { modal, message } = App.useApp()
|
||||
const { fetchData } = useFetch()
|
||||
const [aiSettingList, setAiSettingList] = useState<AiSettingListItem[]>([])
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const modalRef = useRef<AiSettingModalContentHandle>()
|
||||
const { setAiConfigFlushed, accessData } = useGlobalContext()
|
||||
|
||||
const getAiSettingList = () => {
|
||||
setLoading(true)
|
||||
return fetchData<BasicResponse<{ providers: Omit<AiSettingListItem, 'availableLlms' | 'llmListStatus'>[] }>>(
|
||||
`ai/providers/unconfigured`,
|
||||
{ method: 'GET', eoTransformKeys: ['default_llm', 'default_llm_logo'] }
|
||||
// eoApiPrefix: 'http://uat.apikit.com:11204/mockApi/aoplatform/api/v1/'
|
||||
)
|
||||
.then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
setAiSettingList(
|
||||
data.providers?.map((x: AiSettingListItem) => ({
|
||||
...x,
|
||||
name: $t(x.name),
|
||||
llmListStatus: 'unload',
|
||||
availableLlms: []
|
||||
}))
|
||||
)
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
.finally(() => setLoading(false))
|
||||
}
|
||||
|
||||
const openModal = async (entity: AiSettingListItem) => {
|
||||
message.loading($t(RESPONSE_TIPS.loading))
|
||||
const { code, data, msg } = await fetchData<BasicResponse<{ provider: AiProviderConfig }>>('ai/provider/config', {
|
||||
method: 'GET',
|
||||
eoParams: { provider: entity!.id },
|
||||
eoTransformKeys: ['get_apikey_url']
|
||||
})
|
||||
message.destroy()
|
||||
if (code !== STATUS_CODE.SUCCESS) {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
return
|
||||
}
|
||||
modal.confirm({
|
||||
title: $t('模型配置'),
|
||||
content: (
|
||||
<AiSettingModalContent
|
||||
ref={modalRef}
|
||||
entity={{ ...data.provider, defaultLlm: entity.defaultLlm }}
|
||||
readOnly={!checkAccess('system.devops.ai_provider.edit', accessData)}
|
||||
/>
|
||||
),
|
||||
onOk: () => {
|
||||
return modalRef.current?.save().then((res) => {
|
||||
if (res === true) setAiConfigFlushed(true)
|
||||
getAiSettingList()
|
||||
})
|
||||
},
|
||||
width: 600,
|
||||
okText: $t('确认'),
|
||||
footer: (_, { OkBtn, CancelBtn }) => {
|
||||
return (
|
||||
<div className="flex justify-between items-center">
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={data.provider.getApikeyUrl}
|
||||
className="flex items-center gap-[8px]"
|
||||
>
|
||||
<span>{$t('从 (0) 获取 API KEY', [data.provider.name])}</span>
|
||||
<Icon icon="ic:baseline-open-in-new" width={16} height={16} />
|
||||
</a>
|
||||
<div>
|
||||
<CancelBtn />
|
||||
{checkAccess('system.devops.ai_provider.edit', accessData) ? <OkBtn /> : null}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
cancelText: $t('取消'),
|
||||
closable: true,
|
||||
icon: <></>
|
||||
})
|
||||
}
|
||||
const AiSettingContent = () => {
|
||||
const [searchParams, setSearchParams] = useSearchParams()
|
||||
const [activeKey, setActiveKey] = useState(searchParams.get('status') === 'unconfigure' ? 'config' : 'flow')
|
||||
|
||||
useEffect(() => {
|
||||
getAiSettingList()
|
||||
}, [])
|
||||
|
||||
const CardBox = memo(({ provider }: { provider: AiSettingListItem }) => {
|
||||
return (
|
||||
<Card
|
||||
title={
|
||||
<div className="flex w-full items-center justify-between gap-[4px]">
|
||||
<div className="flex flex-1 overflow-hidden items-center gap-[4px]">
|
||||
<span
|
||||
className=" flex items-center h-[22px] ai-setting-svg-container"
|
||||
dangerouslySetInnerHTML={{ __html: provider.logo }}
|
||||
></span>
|
||||
<span className="font-normal truncate">{provider.name}</span>
|
||||
</div>
|
||||
<Tag
|
||||
bordered={false}
|
||||
color={provider.configured ? 'green' : undefined}
|
||||
className="h-[22px] px-[4px] text-center"
|
||||
>
|
||||
{provider.configured ? $t('已配置') : $t('未配置')}
|
||||
</Tag>
|
||||
</div>
|
||||
}
|
||||
className="shadow-[0_5px_10px_0_rgba(0,0,0,0.05)] rounded-[10px] overflow-visible h-[156px] m-0 flex flex-col "
|
||||
classNames={{ header: 'border-b-[0px] p-[20px] px-[24px]', body: 'pt-0 flex-1' }}
|
||||
>
|
||||
<div className="flex flex-col justify-between h-full gap-btnbase">
|
||||
<div className="flex items-center w-full h-[32px] flex-1">
|
||||
{provider.configured && (
|
||||
<>
|
||||
<label className="text-nowrap">{$t('默认')}:</label>
|
||||
<span className="overflow-hidden flex-1 truncate">{provider.defaultLlm}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<WithPermission access="system.settings.ai_provider.view">
|
||||
<Button
|
||||
block
|
||||
icon={<Icon icon="ic:outline-settings" width={18} height={18} />}
|
||||
onClick={() => openModal(provider)}
|
||||
classNames={{ icon: 'h-[18px]' }}
|
||||
>
|
||||
{$t('设置')}
|
||||
</Button>
|
||||
</WithPermission>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
})
|
||||
|
||||
const ModelCardArea = ({ modelList, className }: { modelList: AiSettingListItem[]; className?: string }) => {
|
||||
return (
|
||||
<>
|
||||
{modelList.length > 0 ? (
|
||||
<div
|
||||
className={className}
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))',
|
||||
gap: '20px'
|
||||
}}
|
||||
>
|
||||
{modelList.map((provider: AiSettingListItem) => (
|
||||
<CardBox key={provider.id} provider={provider} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
const newActiveKey = searchParams.get('status') === 'unconfigure' ? 'config' : 'flow'
|
||||
setActiveKey(newActiveKey)
|
||||
}, [searchParams])
|
||||
|
||||
return (
|
||||
<>
|
||||
<InsidePage
|
||||
className="overflow-y-auto pb-PAGE_INSIDE_B"
|
||||
pageTitle={$t('AI 模型')}
|
||||
description={$t('配置好 AI 模型后,你可以使用对应的大模型来创建 AI 服务')}
|
||||
showBorder={false}
|
||||
scrollPage={false}
|
||||
>
|
||||
<AIFlowChart />
|
||||
<Spin
|
||||
className="h-full"
|
||||
wrapperClassName="h-full pr-PAGE_INSIDE_X"
|
||||
indicator={<LoadingOutlined style={{ fontSize: 24 }} spin />}
|
||||
spinning={loading}
|
||||
>
|
||||
{aiSettingList && aiSettingList.length > 0 ? (
|
||||
<div>
|
||||
{aiSettingList.filter((item) => !item.configured).length > 0 && (
|
||||
<>
|
||||
<Divider style={{ margin: '20px 0 !important;' }} />
|
||||
<p className="text-[14px] text-[#666] mb-[4px] mt-[20px] font-bold">{$t('未配置')}</p>
|
||||
<ModelCardArea modelList={aiSettingList.filter((item) => !item.configured) || []} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
)}
|
||||
</Spin>
|
||||
</InsidePage>
|
||||
</>
|
||||
<InsidePage
|
||||
className="h-full pb-PAGE_INSIDE_B"
|
||||
pageTitle={$t('AI 模型')}
|
||||
description={$t('配置好 AI 模型后,你可以使用对应的大模型来创建 AI 服务')}
|
||||
showBorder={false}
|
||||
scrollPage={false}
|
||||
>
|
||||
<div className="flex flex-col h-full">
|
||||
<Tabs
|
||||
activeKey={activeKey}
|
||||
onChange={(key) => {
|
||||
setActiveKey(key)
|
||||
setSearchParams({ status: key === 'config' ? 'unconfigure' : 'configure' })
|
||||
}}
|
||||
className="sticky top-0 flex-shrink-0"
|
||||
items={[
|
||||
{
|
||||
key: 'flow',
|
||||
label: $t('已设置'),
|
||||
children: (
|
||||
<div className="overflow-auto" style={CONTENT_STYLE}>
|
||||
<AIFlowChart />
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'config',
|
||||
label: $t('未设置'),
|
||||
children: (
|
||||
<div className="overflow-auto" style={CONTENT_STYLE}>
|
||||
<AIUnConfigure />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</InsidePage>
|
||||
)
|
||||
}
|
||||
|
||||
const AiSettingList = () => {
|
||||
return (
|
||||
<AiSettingProvider>
|
||||
<AiSettingContent />
|
||||
</AiSettingProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default AiSettingList
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { QuestionCircleOutlined } from '@ant-design/icons'
|
||||
import { Codebox } from '@common/components/postcat/api/Codebox'
|
||||
import { BasicResponse, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
|
||||
import { useFetch } from '@common/hooks/http'
|
||||
import { $t } from '@common/locales'
|
||||
import { App, Form, Select, Tag } from 'antd'
|
||||
import { App, Form, InputNumber, Select, Switch, Tag, Tooltip } from 'antd'
|
||||
import { forwardRef, useEffect, useImperativeHandle, useState } from 'react'
|
||||
import { AiProviderConfig, AiProviderLlmsItems } from './AiSettingList'
|
||||
import { AiProviderLlmsItems, ModelDetailData } from './types'
|
||||
|
||||
export type AiSettingModalContentProps = {
|
||||
entity: AiProviderConfig & { defaultLlm: string }
|
||||
entity: ModelDetailData & { defaultLlm: string }
|
||||
readOnly: boolean
|
||||
}
|
||||
|
||||
@@ -15,11 +16,6 @@ export type AiSettingModalContentHandle = {
|
||||
save: () => Promise<boolean | string>
|
||||
}
|
||||
|
||||
type AiSettingModalContentField = {
|
||||
config: string
|
||||
defaultLlm: string
|
||||
}
|
||||
|
||||
const AiSettingModalContent = forwardRef<AiSettingModalContentHandle, AiSettingModalContentProps>((props, ref) => {
|
||||
const [form] = Form.useForm()
|
||||
const { message } = App.useApp()
|
||||
@@ -27,7 +23,7 @@ const AiSettingModalContent = forwardRef<AiSettingModalContentHandle, AiSettingM
|
||||
const { fetchData } = useFetch()
|
||||
const [llmList, setLlmList] = useState<AiProviderLlmsItems[]>()
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
|
||||
const [enableState, setEnableState] = useState<boolean>(entity.status === 'enabled')
|
||||
const getLlmList = () => {
|
||||
setLoading(true)
|
||||
fetchData<BasicResponse<{ llms: AiProviderLlmsItems[] }>>(`ai/provider/llms`, {
|
||||
@@ -52,12 +48,16 @@ const AiSettingModalContent = forwardRef<AiSettingModalContentHandle, AiSettingM
|
||||
try {
|
||||
form.setFieldsValue({
|
||||
defaultLlm: entity.defaultLlm,
|
||||
config: entity!.config ? JSON.stringify(JSON.parse(entity!.config), null, 2) : ''
|
||||
config: entity!.config ? JSON.stringify(JSON.parse(entity!.config), null, 2) : '',
|
||||
priority: entity.priority || 1,
|
||||
enable: entity.status === 'enabled'
|
||||
})
|
||||
} catch (e) {
|
||||
form.setFieldsValue({
|
||||
defaultLlm: entity.defaultLlm,
|
||||
config: ''
|
||||
config: '',
|
||||
priority: 1,
|
||||
enable: true
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
@@ -67,11 +67,17 @@ const AiSettingModalContent = forwardRef<AiSettingModalContentHandle, AiSettingM
|
||||
form
|
||||
.validateFields()
|
||||
.then((value) => {
|
||||
const finalValue = {
|
||||
...value,
|
||||
priority: Math.max(1, value.priority)
|
||||
}
|
||||
|
||||
fetchData<BasicResponse<null>>('ai/provider/config', {
|
||||
method: 'PUT',
|
||||
eoParams: { provider: entity?.id },
|
||||
eoBody: value,
|
||||
eoBody: finalValue,
|
||||
eoTransformKeys: ['defaultLlm']
|
||||
// eoApiPrefix: 'http://uat.apikit.com:11204/mockApi/aoplatform/api/v1/'
|
||||
})
|
||||
.then((response) => {
|
||||
const { code, msg } = response
|
||||
@@ -89,21 +95,29 @@ const AiSettingModalContent = forwardRef<AiSettingModalContentHandle, AiSettingM
|
||||
})
|
||||
}
|
||||
|
||||
const getTooltipText = (isChecked: boolean) => {
|
||||
if (!isChecked) {
|
||||
return $t('保存后供应商状态变为【停用】,使用本供应商的 API 将临时使用负载优先级最高的正常供应商。')
|
||||
}
|
||||
return $t('保存后供应商状态变为【正常】,恢复调用本供应商的 AI 能力。')
|
||||
}
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
save
|
||||
}))
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
labelAlign="left"
|
||||
scrollToFirstError
|
||||
form={form}
|
||||
className="flex flex-col mx-auto h-full"
|
||||
name="aiServiceInsideRouterModalConfig"
|
||||
autoComplete="off"
|
||||
disabled={readOnly}
|
||||
>
|
||||
<Form.Item<AiSettingModalContentField> label={$t('模型')} name="defaultLlm" rules={[{ required: true }]}>
|
||||
<Form.Item<ModelDetailData> label={$t('默认模型')} name="defaultLlm" rules={[{ required: true }]}>
|
||||
<Select
|
||||
className="w-INPUT_NORMAL"
|
||||
placeholder={$t(PLACEHOLDER.select)}
|
||||
@@ -113,23 +127,77 @@ const AiSettingModalContent = forwardRef<AiSettingModalContentHandle, AiSettingM
|
||||
label: (
|
||||
<div className="flex items-center gap-[10px]">
|
||||
<span>{x.id}</span>
|
||||
{x?.scopes?.map((s) => <Tag>{s?.toLocaleUpperCase()}</Tag>)}
|
||||
{x?.scopes?.map((s) => <Tag key={s}>{s?.toLocaleUpperCase()}</Tag>)}
|
||||
</div>
|
||||
)
|
||||
}))}
|
||||
></Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<AiSettingModalContentField> label={$t('参数')} name="config">
|
||||
<Form.Item<ModelDetailData>
|
||||
label={
|
||||
<span className="flex items-center">
|
||||
{$t('负载优先级')}
|
||||
<Tooltip
|
||||
title={$t('负载优先级决定在原供应商异常或停用后,优先使用哪一个供应商。优先级数字越小,优先级越高。')}
|
||||
>
|
||||
<QuestionCircleOutlined className="ml-1 text-gray-500" />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name="priority"
|
||||
rules={[
|
||||
{ required: true },
|
||||
{
|
||||
validator: async (_, value) => {
|
||||
if (value <= 0) {
|
||||
throw new Error($t('优先级必须大于 0'))
|
||||
}
|
||||
return Promise.resolve()
|
||||
}
|
||||
}
|
||||
]}
|
||||
initialValue={1}
|
||||
>
|
||||
<InputNumber className="w-INPUT_NORMAL" min={1} placeholder={$t('请输入优先级')} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<ModelDetailData> label={$t('API Key(默认 Key)')} name="config">
|
||||
<Codebox
|
||||
editorTheme="vs-dark"
|
||||
readOnly={readOnly}
|
||||
width="100%"
|
||||
height="300px"
|
||||
height="200px"
|
||||
language="json"
|
||||
enableToolbar={false}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{entity.configured && (
|
||||
<Form.Item className="p-4 bg-white rounded-lg" label={$t('LLM 状态管理')}>
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<span className="text-gray-600">{$t('当前调用状态:')}</span>
|
||||
{entity.status === 'enabled' && <Tag color="success">{$t('正常')}</Tag>}
|
||||
{entity.status === 'disabled' && <Tag color="warning">{$t('停用')}</Tag>}
|
||||
{entity.status === 'abnormal' && <Tag color="error">{$t('异常')}</Tag>}
|
||||
</div>
|
||||
<Form.Item name="enable" valuePropName="checked" noStyle>
|
||||
<Switch
|
||||
checkedChildren={$t('启用')}
|
||||
unCheckedChildren={$t('停用')}
|
||||
onChange={(checked) => {
|
||||
form.setFieldsValue({ enable: checked })
|
||||
setEnableState(checked)
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
{(entity.status === 'enabled' && !enableState) || (entity.status !== 'enabled' && enableState) ? (
|
||||
<div className="mt-2 text-sm text-gray-500">* {getTooltipText(enableState)}</div>
|
||||
) : null}
|
||||
</Form.Item>
|
||||
)}
|
||||
</Form>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BaseEdge, EdgeLabelRenderer, EdgeProps, getSmoothStepPath } from '@xyflow/react'
|
||||
import { BaseEdge, EdgeLabelRenderer, EdgeProps, getSmoothStepPath, useStore } from '@xyflow/react'
|
||||
|
||||
export default function CustomEdge({
|
||||
id,
|
||||
@@ -11,14 +11,26 @@ export default function CustomEdge({
|
||||
style = {},
|
||||
markerEnd,
|
||||
label,
|
||||
data
|
||||
data,
|
||||
source,
|
||||
target
|
||||
}: EdgeProps) {
|
||||
const [edgePath, labelX, labelY] = getSmoothStepPath({
|
||||
// Get all edges to check for duplicates
|
||||
const edges = useStore((state) => state.edges)
|
||||
|
||||
// Find duplicate edges between the same source and target
|
||||
const duplicateEdges = edges.filter((edge) => edge.source === source && edge.target === target)
|
||||
const edgeIndex = duplicateEdges.findIndex((edge) => edge.id === id)
|
||||
|
||||
// Adjust the path if this is a duplicate edge
|
||||
const offset = edgeIndex * 20 // 20px offset for each duplicate edge
|
||||
|
||||
const [edgePath] = getSmoothStepPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourceY: sourceY,
|
||||
sourcePosition,
|
||||
targetX,
|
||||
targetY,
|
||||
targetY: targetY + offset,
|
||||
targetPosition,
|
||||
borderRadius: 16
|
||||
})
|
||||
@@ -27,24 +39,29 @@ export default function CustomEdge({
|
||||
|
||||
return (
|
||||
<>
|
||||
<BaseEdge path={edgePath} markerEnd={markerEnd} style={{ stroke: '#ddd', cursor: 'pointer', strokeWidth: 1 }} />
|
||||
<BaseEdge
|
||||
path={edgePath}
|
||||
markerEnd={markerEnd}
|
||||
style={{
|
||||
...style,
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
/>
|
||||
{label && (
|
||||
<EdgeLabelRenderer>
|
||||
<a
|
||||
href={`/aiSetting/model?modelId=${modelId}`}
|
||||
href={`${label?.toString().includes('apis') ? '/aiApis' : '/keysetting'}?modelId=${modelId}`}
|
||||
target="_blank"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
transform: `translate(${targetX - 80}px,${targetY - 20}px)`,
|
||||
transform: `translate(${targetX - 80}px,${targetY - 20 + offset}px)`,
|
||||
borderRadius: '4px',
|
||||
fontSize: 12,
|
||||
fontWeight: 500,
|
||||
color: 'var(--primary-color)',
|
||||
cursor: 'pointer',
|
||||
pointerEvents: 'all',
|
||||
textDecoration: 'none'
|
||||
}}
|
||||
className="nodrag nopan"
|
||||
>
|
||||
{label}
|
||||
</a>
|
||||
|
||||
@@ -22,7 +22,7 @@ export const KeyStatusNode: React.FC<{ data: KeyStatusNodeData }> = ({ data }) =
|
||||
style={{ border: '1px solid var(--border-color)' }}
|
||||
>
|
||||
<Handle type="target" position={Position.Left} />
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex flex-col">
|
||||
<div className="text-sm text-gray-900">{title}</div>
|
||||
<div
|
||||
className="flex gap-1 w-full"
|
||||
|
||||
@@ -1,26 +1,36 @@
|
||||
import { $t } from '@common/locales'
|
||||
import { Icon } from '@iconify/react'
|
||||
import { Handle, Position } from '@xyflow/react'
|
||||
import { t } from 'i18next'
|
||||
import React from 'react'
|
||||
import { ModelStatus } from '../types'
|
||||
import { useAiSetting } from '../contexts/AiSettingContext'
|
||||
import { AiSettingListItem, ModelDetailData, ModelStatus } from '../types'
|
||||
|
||||
interface ModelCardData {
|
||||
title: string
|
||||
status: ModelStatus
|
||||
logo: string
|
||||
defaultModel: string
|
||||
}
|
||||
|
||||
type ModelCardNodeData = ModelCardData & {
|
||||
type ModelCardNodeData = ModelDetailData & {
|
||||
id: string
|
||||
position: { x: number; y: number }
|
||||
alternativeModel?: ModelDetailData
|
||||
}
|
||||
|
||||
export const ModelCardNode: React.FC<{ data: ModelCardNodeData }> = ({ data }) => {
|
||||
const { title, status, defaultModel, logo } = data
|
||||
const { name, status, defaultLlm, logo, alternativeModel } = data
|
||||
const { openConfigModal } = useAiSetting()
|
||||
|
||||
const getStatusIcon = (status: ModelStatus) => {
|
||||
switch (status) {
|
||||
case 'enabled':
|
||||
return { icon: 'mdi:check-circle', color: 'text-green-500' }
|
||||
case 'disabled':
|
||||
return { icon: 'mdi:pause-circle', color: 'text-gray-400' }
|
||||
case 'abnormal':
|
||||
return { icon: 'mdi:alert-circle', color: 'text-red-500' }
|
||||
}
|
||||
}
|
||||
|
||||
const statusConfig = getStatusIcon(status)
|
||||
|
||||
return (
|
||||
<div
|
||||
className="node-card bg-white rounded-lg shadow-sm p-4 min-w-[280px] group"
|
||||
className="node-card bg-white rounded-lg shadow-sm p-4 min-w-[280px] group"
|
||||
style={{ border: '1px solid var(--border-color)' }}
|
||||
>
|
||||
<Handle type="target" position={Position.Left} />
|
||||
@@ -28,17 +38,14 @@ export const ModelCardNode: React.FC<{ data: ModelCardNodeData }> = ({ data }) =
|
||||
<div>
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex gap-2 items-center">
|
||||
<div className="flex flex-1 overflow-hidden items-center gap-[4px]">
|
||||
<div className="flex flex-1 overflow-hidden items-center gap-[4px]">
|
||||
<span
|
||||
className=" flex items-center h-[22px] ai-setting-svg-container"
|
||||
className="flex items-center h-[22px] ai-setting-svg-container"
|
||||
dangerouslySetInnerHTML={{ __html: logo }}
|
||||
></span>
|
||||
</div>
|
||||
<span className="text-base text-gray-900 max-w-[180px] truncate">{title}</span>
|
||||
<Icon
|
||||
icon={status === 'enable' ? 'mdi:check-circle' : 'mdi:close-circle'}
|
||||
className={`text-xl ${status === 'enable' ? 'text-green-500' : 'text-red-500'}`}
|
||||
/>
|
||||
<span className="text-base text-gray-900 max-w-[180px] truncate">{name}</span>
|
||||
<Icon icon={statusConfig?.icon} className={`text-xl ${statusConfig?.color}`} />
|
||||
</div>
|
||||
|
||||
{/* Action buttons */}
|
||||
@@ -46,14 +53,21 @@ export const ModelCardNode: React.FC<{ data: ModelCardNodeData }> = ({ data }) =
|
||||
<Icon
|
||||
icon="mdi:cog"
|
||||
className="text-xl text-gray-400 cursor-pointer hover:text-[--primary-color]"
|
||||
onClick={() => console.log('Default:', data.id)}
|
||||
onClick={() => {
|
||||
openConfigModal({ id: data.id, defaultLlm: defaultLlm } as AiSettingListItem)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 text-sm text-gray-500">
|
||||
{t('默认:')}
|
||||
{defaultModel}
|
||||
{$t('默认:')}
|
||||
{defaultLlm}
|
||||
</div>
|
||||
{status !== 'enabled' && alternativeModel && (
|
||||
<div className="mt-1 text-sm text-gray-500">
|
||||
{$t('关联 API 已转用')} {alternativeModel.name}/{alternativeModel.defaultLlm}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -11,7 +11,7 @@ export const ServiceCardNode: React.FC<NodeProps> = () => {
|
||||
<Handle type="source" position={Position.Right} />
|
||||
<div className="flex flex-col gap-2 items-center">
|
||||
<Icon icon="mdi:robot" className="text-3xl text-[--primary-color]" />
|
||||
<span className="text-base text-gray-900">AI Service</span>
|
||||
<span className="text-base text-gray-900">AI Services</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export const LAYOUT = {
|
||||
SERVICE_NODE_X: 50,
|
||||
SERVICE_NODE_X: 0,
|
||||
NODE_START_Y: 20,
|
||||
NODE_GAP: 120,
|
||||
MODEL_NODE_X: 500,
|
||||
KEY_NODE_X: 900,
|
||||
MODEL_NODE_X: 700,
|
||||
KEY_NODE_X: 1200,
|
||||
} as const;
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import Icon from '@ant-design/icons'
|
||||
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 { checkAccess } from '@common/utils/permission'
|
||||
import { App } from 'antd'
|
||||
import { createContext, useContext, useRef } from 'react'
|
||||
import AiSettingModalContent, { AiSettingModalContentHandle } from '../AiSettingModal'
|
||||
import { AiSettingListItem, ModelDetailData } from '../types'
|
||||
|
||||
interface AiSettingContextType {
|
||||
openConfigModal: (entity: AiSettingListItem) => Promise<void>
|
||||
}
|
||||
|
||||
const AiSettingContext = createContext<AiSettingContextType | undefined>(undefined)
|
||||
|
||||
export const AiSettingProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const { modal, message } = App.useApp()
|
||||
const { fetchData } = useFetch()
|
||||
const { aiConfigFlushed, setAiConfigFlushed, accessData } = useGlobalContext()
|
||||
const modalRef = useRef<AiSettingModalContentHandle>()
|
||||
|
||||
const openConfigModal = async (entity: AiSettingListItem) => {
|
||||
message.loading($t(RESPONSE_TIPS.loading))
|
||||
const { code, data, msg } = await fetchData<BasicResponse<{ provider: ModelDetailData }>>('ai/provider/config', {
|
||||
method: 'GET',
|
||||
eoParams: { provider: entity!.id },
|
||||
eoTransformKeys: ['get_apikey_url']
|
||||
})
|
||||
message.destroy()
|
||||
if (code !== STATUS_CODE.SUCCESS) {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
return
|
||||
}
|
||||
|
||||
modal.confirm({
|
||||
title: $t('模型配置'),
|
||||
content: (
|
||||
<AiSettingModalContent
|
||||
ref={modalRef}
|
||||
entity={{ ...data.provider, defaultLlm: entity.defaultLlm }}
|
||||
readOnly={!checkAccess('system.devops.ai_provider.edit', accessData)}
|
||||
/>
|
||||
),
|
||||
onOk: () => {
|
||||
return modalRef.current?.save().then((res) => {
|
||||
if (res === true) {
|
||||
setAiConfigFlushed(!aiConfigFlushed)
|
||||
}
|
||||
})
|
||||
},
|
||||
width: 600,
|
||||
okText: $t('确认'),
|
||||
footer: (_, { OkBtn, CancelBtn }) => {
|
||||
return (
|
||||
<div className="flex justify-between items-center">
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={data.provider.getApikeyUrl}
|
||||
className="flex items-center gap-[8px]"
|
||||
>
|
||||
<span>{$t('从 (0) 获取 API KEY', [data.provider.name])}</span>
|
||||
<Icon icon="ic:baseline-open-in-new" width={16} height={16} />
|
||||
</a>
|
||||
<div>
|
||||
<CancelBtn />
|
||||
{checkAccess('system.devops.ai_provider.edit', accessData) ? <OkBtn /> : null}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
cancelText: $t('取消'),
|
||||
closable: true,
|
||||
icon: <></>
|
||||
})
|
||||
}
|
||||
|
||||
return <AiSettingContext.Provider value={{ openConfigModal }}>{children}</AiSettingContext.Provider>
|
||||
}
|
||||
|
||||
export const useAiSetting = () => {
|
||||
const context = useContext(AiSettingContext)
|
||||
if (!context) {
|
||||
throw new Error('useAiSetting must be used within an AiSettingProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
@@ -1,4 +1,20 @@
|
||||
/* Flow Chart Styles */
|
||||
.react-flow {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 500px;
|
||||
}
|
||||
|
||||
.react-flow__container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.react-flow__renderer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.react-flow__node {
|
||||
padding: 0;
|
||||
border-radius: 8px;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type ModelStatus = 'enable' | 'abnormal'|'disabled'
|
||||
export type ModelStatus = 'enabled' | 'abnormal'|'disabled'
|
||||
export type KeyStatus ='normal' | 'abnormal'|'disabled'
|
||||
|
||||
export interface KeyData {
|
||||
@@ -7,14 +7,51 @@ export interface KeyData {
|
||||
status: KeyStatus,
|
||||
}
|
||||
|
||||
export interface ModelData {
|
||||
export interface ModelListData {
|
||||
id: string
|
||||
name: string
|
||||
logo: string
|
||||
default_llm: string
|
||||
defaultLlm: string
|
||||
status: ModelStatus
|
||||
api_count: number
|
||||
key_count: number
|
||||
keys: KeyData[]
|
||||
}
|
||||
export interface ModelDetailData extends ModelListData{
|
||||
enable:boolean
|
||||
config: string,
|
||||
priority?: number
|
||||
getApikeyUrl: string
|
||||
status: ModelStatus
|
||||
configured: boolean
|
||||
}
|
||||
|
||||
|
||||
export type AiSettingListItem = {
|
||||
name: string
|
||||
id: string
|
||||
logo: string
|
||||
defaultLlm: string
|
||||
defaultLlmLogo: string
|
||||
enable: boolean
|
||||
configured: boolean
|
||||
priority?: number
|
||||
}
|
||||
|
||||
export type AiProviderLlmsItems = {
|
||||
id: string
|
||||
logo: string
|
||||
scopes: ('chat' | 'completions')[]
|
||||
config: string
|
||||
}
|
||||
|
||||
export type AiProviderDefaultConfig = {
|
||||
id: string
|
||||
provider: string
|
||||
name: string
|
||||
logo: string
|
||||
defaultLlm: string
|
||||
scopes: string[]
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ const ApiKeyContent: React.FC<ApiKeyContentProps> = forwardRef(({ provider, enti
|
||||
setNeverExpire(isNeverExpire)
|
||||
form.setFieldsValue({
|
||||
name: entity.name,
|
||||
expire_time: isNeverExpire ? undefined : dayjs(entity.expire_time),
|
||||
expire_time: isNeverExpire ? undefined : dayjs(entity.expire_time * 1000),
|
||||
config: entity.config
|
||||
})
|
||||
} catch (e) {
|
||||
@@ -41,7 +41,7 @@ const ApiKeyContent: React.FC<ApiKeyContentProps> = forwardRef(({ provider, enti
|
||||
try {
|
||||
const values = await form.validateFields()
|
||||
const { expire_time, ...restValues } = values
|
||||
const expireTime = neverExpire ? 0 : expire_time.valueOf()
|
||||
const expireTime = neverExpire ? 0 : Math.trunc(expire_time.valueOf() / 1000)
|
||||
|
||||
const response = await fetchData<BasicResponse<null>>('ai/resource/key', {
|
||||
method: entity.id ? 'PUT' : 'POST',
|
||||
|
||||
@@ -11,22 +11,22 @@ 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 }
|
||||
{ label: $t('正常'), value: 'normal', color: token.colorSuccess },
|
||||
{ label: $t('超额'), value: 'exceeded', color: token.colorError },
|
||||
{ label: $t('过期'), value: 'expired', color: token.colorWarning },
|
||||
{ label: $t('停用'), value: 'disabled', color: token.colorTextDisabled },
|
||||
{ label: $t('错误'), value: 'error', color: token.colorError }
|
||||
]
|
||||
|
||||
return (
|
||||
<Space>
|
||||
<span>{$t('Status')}:</span>
|
||||
<span>{$t('状态')}:</span>
|
||||
<Select
|
||||
mode="multiple"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
style={{ width: 300 }}
|
||||
placeholder={$t('Filter by status')}
|
||||
placeholder={$t('请选择状态')}
|
||||
allowClear
|
||||
options={options.map((option) => ({
|
||||
...option,
|
||||
|
||||
@@ -12,20 +12,22 @@ 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 { useSearchParams } from 'react-router-dom'
|
||||
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 [searchParams] = useSearchParams()
|
||||
const [selectedProvider, setSelectedProvider] = useState<string>(searchParams.get('modelId') || '')
|
||||
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()
|
||||
const [apiKeys, setApiKeys] = useState<APIKey[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
pageListRef.current?.reload()
|
||||
@@ -65,7 +67,7 @@ const KeySettings: React.FC = () => {
|
||||
const newEntity = entity as EditAPIKey
|
||||
|
||||
modal.confirm({
|
||||
title: mode === 'add' ? $t(`添加 ${provider?.name} APIKey`) : $t('编辑 APIKey'),
|
||||
title: mode === 'add' ? $t('添加 (0) APIKey', [provider?.name]) : $t('编辑 APIKey'),
|
||||
content: <ApiKeyContent ref={modalRef} entity={newEntity} provider={provider} />,
|
||||
onOk: () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -153,16 +155,30 @@ const KeySettings: React.FC = () => {
|
||||
}
|
||||
|
||||
const handleDragSortEnd = async (beforeIndex: number, afterIndex: number, newDataSource: APIKey[]) => {
|
||||
console.log(beforeIndex, afterIndex, newDataSource)
|
||||
try {
|
||||
let targetId
|
||||
let sortDirection
|
||||
|
||||
// Check if there's an item before afterIndex
|
||||
if (afterIndex > 0) {
|
||||
targetId = newDataSource[afterIndex - 1].id
|
||||
sortDirection = 'after'
|
||||
} else if (afterIndex < newDataSource.length - 1) {
|
||||
// If no item before, use the item after
|
||||
targetId = newDataSource[afterIndex + 1].id
|
||||
sortDirection = 'before'
|
||||
}
|
||||
|
||||
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'
|
||||
provider: selectedProvider
|
||||
},
|
||||
eoBody: {
|
||||
origin: apiKeys[beforeIndex].id,
|
||||
target: targetId,
|
||||
sort: sortDirection
|
||||
}
|
||||
// eoApiPrefix: 'http://uat.apikit.com:11204/mockApi/aoplatform/api/v1/'
|
||||
})
|
||||
|
||||
if (response.code === STATUS_CODE.SUCCESS) {
|
||||
@@ -170,9 +186,13 @@ const KeySettings: React.FC = () => {
|
||||
pageListRef.current?.reload()
|
||||
} else {
|
||||
message.error(response.msg || RESPONSE_TIPS.error)
|
||||
// Revert the UI if API call fails
|
||||
pageListRef.current?.reload()
|
||||
}
|
||||
} catch (error) {
|
||||
message.error(RESPONSE_TIPS.error)
|
||||
// Revert the UI if API call fails
|
||||
pageListRef.current?.reload()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,12 +206,15 @@ const KeySettings: React.FC = () => {
|
||||
page_size: params.pageSize,
|
||||
keyword: searchWord,
|
||||
page: params.current
|
||||
//TODO API 筛选
|
||||
// statuses: params.statuses || []
|
||||
}
|
||||
// eoApiPrefix: 'http://uat.apikit.com:11204/mockApi/aoplatform/api/v1/'
|
||||
})
|
||||
|
||||
if (response.code === STATUS_CODE.SUCCESS) {
|
||||
setTotal(response.data.total)
|
||||
setApiKeys(response.data.keys)
|
||||
return {
|
||||
data: response.data.keys,
|
||||
success: true,
|
||||
@@ -306,7 +329,7 @@ const KeySettings: React.FC = () => {
|
||||
render: (dom: React.ReactNode, entity: APIKey) => {
|
||||
return entity.expire_time === 0
|
||||
? $t('永不过期')
|
||||
: dayjs(Number(entity.expire_time)).format('YYYY-MM-DD HH:mm:ss')
|
||||
: dayjs(Number(entity.expire_time) * 1000).format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
},
|
||||
...operation
|
||||
@@ -340,6 +363,7 @@ const KeySettings: React.FC = () => {
|
||||
request={requestApiKeys}
|
||||
onSearchWordChange={(e) => {
|
||||
setSearchWord(e.target.value)
|
||||
pageListRef.current?.reload()
|
||||
}}
|
||||
showPagination={true}
|
||||
searchPlaceholder={$t('请输入名称搜索')}
|
||||
|
||||
@@ -1,93 +1,95 @@
|
||||
import InsidePage from '@common/components/aoplatform/InsidePage'
|
||||
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
|
||||
import { DynamicMenuItem } from '@common/const/type'
|
||||
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
|
||||
import { useFetch } from '@common/hooks/http'
|
||||
import { $t } from '@common/locales'
|
||||
import { getItem } from '@common/utils/navigation'
|
||||
import { RouterParams } from '@core/components/aoplatform/RenderRoutes'
|
||||
import { Menu, MenuProps, Skeleton, message } from 'antd'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { Link, Outlet, useNavigate, useParams } from 'react-router-dom'
|
||||
|
||||
import { Menu, MenuProps, Skeleton, message } from "antd";
|
||||
import { Link, Outlet, useNavigate, useParams } from "react-router-dom";
|
||||
import InsidePage from "@common/components/aoplatform/InsidePage";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from "@common/const/const";
|
||||
import { DynamicMenuItem, } from "@common/const/type";
|
||||
import { useFetch } from "@common/hooks/http";
|
||||
import { getItem } from "@common/utils/navigation";
|
||||
import { RouterParams } from "@core/components/aoplatform/RenderRoutes";
|
||||
import { $t } from "@common/locales";
|
||||
import { useGlobalContext } from "@common/contexts/GlobalStateContext";
|
||||
const LogSettings = () => {
|
||||
const { moduleId } = useParams<RouterParams>()
|
||||
const [menuItems, setMenuItems] = useState<MenuProps['items']>([])
|
||||
const [activeMenu, setActiveMenu] = useState<string>()
|
||||
const { fetchData } = useFetch()
|
||||
const [loading, setLoading] = useState<boolean>(true)
|
||||
const navigateTo = useNavigate()
|
||||
const { state } = useGlobalContext()
|
||||
|
||||
const LogSettings = ()=>{
|
||||
const {moduleId} = useParams<RouterParams>();
|
||||
const [menuItems, setMenuItems ] = useState<MenuProps['items']>([])
|
||||
const [activeMenu, setActiveMenu] = useState<string>()
|
||||
const {fetchData} = useFetch()
|
||||
const [loading, setLoading] = useState<boolean>(true)
|
||||
const navigateTo = useNavigate()
|
||||
const {state} = useGlobalContext()
|
||||
|
||||
const getDynamicMenuList = ()=>{
|
||||
return fetchData<BasicResponse<{ dynamics:DynamicMenuItem[] }>>(`simple/dynamics/log`,{method:'GET'}).then(response=>{
|
||||
const {code,data,msg} = response
|
||||
if(code === STATUS_CODE.SUCCESS){
|
||||
|
||||
setMenuItems(data.dynamics)
|
||||
if(!activeMenu || activeMenu.length === 0){
|
||||
navigateTo(`/logsettings/template/${data.dynamics[0].name}`)
|
||||
}
|
||||
return Promise.resolve(data.dynamics)
|
||||
}else{
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
return Promise.reject(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const menuData = useMemo(()=>{
|
||||
const newMenu = menuItems?.map((x:DynamicMenuItem)=>{
|
||||
return getItem(
|
||||
<Link to={`template/${x.name}`}>{$t(x.title)}</Link>,
|
||||
x.name,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
'system.settings.log_configuration.view')
|
||||
})
|
||||
return newMenu
|
||||
},[state.language,menuItems])
|
||||
|
||||
const onMenuClick: MenuProps['onClick'] = ({key}) => {
|
||||
setActiveMenu(key)
|
||||
};
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
setActiveMenu(moduleId)
|
||||
}, [ moduleId]);
|
||||
|
||||
useEffect(()=>{
|
||||
setLoading(true)
|
||||
Promise.all([getDynamicMenuList()]).finally(()=>setLoading(false))
|
||||
},[])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Skeleton className='m-btnbase w-calc-100vw-minus-padding-r' active loading={loading}>
|
||||
<InsidePage
|
||||
pageTitle={$t('日志配置')}
|
||||
description={'APIPark '+$t("提供详尽的 API 调用日志,帮助企业监控、分析和审计 API 的运行状况。")}
|
||||
>
|
||||
<div className="flex h-full">
|
||||
<Menu
|
||||
className="h-full overflow-y-auto"
|
||||
selectedKeys={[activeMenu || '']}
|
||||
onClick={onMenuClick}
|
||||
style={{ width: 220 }}
|
||||
mode="inline"
|
||||
items={menuData}
|
||||
/>
|
||||
<div className={`w-full flex flex-1 flex-col h-full overflow-auto bg-MAIN_BG pt-btnbase pl-btnbase pr-PAGE_INSIDE_X pb-PAGE_INSIDE_B overflow-x-hidden`}>
|
||||
<Outlet context={{accessPrefix:'system.devops.log_configuration'}}/>
|
||||
</div>
|
||||
</div>
|
||||
</InsidePage>
|
||||
</Skeleton>
|
||||
</>
|
||||
const getDynamicMenuList = () => {
|
||||
return fetchData<BasicResponse<{ dynamics: DynamicMenuItem[] }>>(`simple/dynamics/log`, { method: 'GET' }).then(
|
||||
(response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
setMenuItems(data.dynamics)
|
||||
if (!activeMenu || activeMenu.length === 0) {
|
||||
navigateTo(`/logsettings/template/${data.dynamics[0].name}`)
|
||||
}
|
||||
return Promise.resolve(data.dynamics)
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
return Promise.reject(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const menuData = useMemo(() => {
|
||||
const newMenu = menuItems?.map((x: DynamicMenuItem) => {
|
||||
return getItem(
|
||||
<Link to={`template/${x.name}`}>{$t(x.title)}</Link>,
|
||||
x.name,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
'system.settings.log_configuration.view'
|
||||
)
|
||||
})
|
||||
return newMenu
|
||||
}, [state.language, menuItems])
|
||||
|
||||
const onMenuClick: MenuProps['onClick'] = ({ key }) => {
|
||||
setActiveMenu(key)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setActiveMenu(moduleId)
|
||||
}, [moduleId])
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true)
|
||||
Promise.all([getDynamicMenuList()]).finally(() => setLoading(false))
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Skeleton className="m-btnbase w-calc-100vw-minus-padding-r" active loading={loading}>
|
||||
<InsidePage
|
||||
pageTitle={$t('日志配置')}
|
||||
description={'APIPark ' + $t('提供详尽的 API 调用日志,帮助企业监控、分析和审计 API 的运行状况。')}
|
||||
>
|
||||
<div className="flex h-full">
|
||||
<Menu
|
||||
className="overflow-y-auto h-full"
|
||||
selectedKeys={[activeMenu || '']}
|
||||
onClick={onMenuClick}
|
||||
style={{ width: 220 }}
|
||||
mode="inline"
|
||||
items={menuData}
|
||||
/>
|
||||
<div
|
||||
className={`flex overflow-auto overflow-x-hidden flex-col flex-1 w-full h-full bg-MAIN_BG pt-btnbase pl-btnbase pr-PAGE_INSIDE_X pb-PAGE_INSIDE_B`}
|
||||
>
|
||||
<Outlet context={{ accessPrefix: 'system.settings.log_configuration' }} />
|
||||
</div>
|
||||
</div>
|
||||
</InsidePage>
|
||||
</Skeleton>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default LogSettings;
|
||||
export default LogSettings
|
||||
|
||||
@@ -1,95 +1,89 @@
|
||||
import InsidePage from '@common/components/aoplatform/InsidePage'
|
||||
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
|
||||
import { DynamicMenuItem } from '@common/const/type'
|
||||
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
|
||||
import { useFetch } from '@common/hooks/http'
|
||||
import { $t } from '@common/locales'
|
||||
import { getItem } from '@common/utils/navigation'
|
||||
import { RouterParams } from '@core/components/aoplatform/RenderRoutes'
|
||||
import { Menu, MenuProps, Skeleton, message } from 'antd'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { Link, Outlet, useNavigate, useParams } from 'react-router-dom'
|
||||
|
||||
import { Menu, MenuProps, Skeleton, message } from "antd";
|
||||
import { Link, Outlet, useNavigate, useParams } from "react-router-dom";
|
||||
import InsidePage from "@common/components/aoplatform/InsidePage";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from "@common/const/const";
|
||||
import { DynamicMenuItem } from "@common/const/type";
|
||||
import { useFetch } from "@common/hooks/http";
|
||||
import { getItem } from "@common/utils/navigation";
|
||||
import { RouterParams } from "@core/components/aoplatform/RenderRoutes";
|
||||
import { $t } from "@common/locales";
|
||||
import { useGlobalContext } from "@common/contexts/GlobalStateContext";
|
||||
const LogSettings = () => {
|
||||
const { moduleId } = useParams<RouterParams>()
|
||||
const [menuItems, setMenuItems] = useState<MenuProps['items']>([])
|
||||
const [activeMenu, setActiveMenu] = useState<string>()
|
||||
const { fetchData } = useFetch()
|
||||
const [loading, setLoading] = useState<boolean>(true)
|
||||
const navigateTo = useNavigate()
|
||||
const { state } = useGlobalContext()
|
||||
|
||||
const LogSettings = ()=>{
|
||||
const {moduleId} = useParams<RouterParams>();
|
||||
const [menuItems, setMenuItems ] = useState<MenuProps['items']>([])
|
||||
const [activeMenu, setActiveMenu] = useState<string>()
|
||||
const {fetchData} = useFetch()
|
||||
const [loading, setLoading] = useState<boolean>(true)
|
||||
const navigateTo = useNavigate()
|
||||
const {state} = useGlobalContext()
|
||||
|
||||
const getDynamicMenuList = ()=>{
|
||||
setLoading(true)
|
||||
fetchData<BasicResponse<{ dynamics:DynamicMenuItem[] }>>(`simple/dynamics/resource`,{method:'GET'}).then(response=>{
|
||||
const {code,data,msg} = response
|
||||
if(code === STATUS_CODE.SUCCESS){
|
||||
|
||||
|
||||
setMenuItems(data.dynamics)
|
||||
if(!activeMenu || activeMenu.length === 0){
|
||||
navigateTo(`/resourcesettings/template/${data.dynamics[0].name}`)
|
||||
}
|
||||
}else{
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
}).finally(()=>setLoading(false))
|
||||
}
|
||||
const getDynamicMenuList = () => {
|
||||
setLoading(true)
|
||||
fetchData<BasicResponse<{ dynamics: DynamicMenuItem[] }>>(`simple/dynamics/resource`, { method: 'GET' })
|
||||
.then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
setMenuItems(data.dynamics)
|
||||
if (!activeMenu || activeMenu.length === 0) {
|
||||
navigateTo(`/resourcesettings/template/${data.dynamics[0].name}`)
|
||||
}
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
.finally(() => setLoading(false))
|
||||
}
|
||||
|
||||
|
||||
const menuData = useMemo(()=>{
|
||||
const newMenu = menuItems?.map((x:DynamicMenuItem)=>{
|
||||
|
||||
return getItem(
|
||||
<Link to={`template/${x.name}`}>{$t(x.title)}</Link>,
|
||||
x.name,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
'system.settings.log_configuration.view')
|
||||
})
|
||||
return newMenu
|
||||
},[state.language,menuItems])
|
||||
const menuData = useMemo(() => {
|
||||
const newMenu = menuItems?.map((x: DynamicMenuItem) => {
|
||||
return getItem(
|
||||
<Link to={`template/${x.name}`}>{$t(x.title)}</Link>,
|
||||
x.name,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
'system.settings.log_configuration.view'
|
||||
)
|
||||
})
|
||||
return newMenu
|
||||
}, [state.language, menuItems])
|
||||
|
||||
const onMenuClick: MenuProps['onClick'] = ({ key }) => {
|
||||
setActiveMenu(key)
|
||||
}
|
||||
|
||||
const onMenuClick: MenuProps['onClick'] = ({key}) => {
|
||||
setActiveMenu(key)
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setActiveMenu(moduleId)
|
||||
}, [ moduleId]);
|
||||
|
||||
useEffect(()=>{
|
||||
setLoading(true)
|
||||
getDynamicMenuList()
|
||||
},[])
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<Skeleton className='m-btnbase w-calc-100vw-minus-padding-r' active loading={loading}>
|
||||
<InsidePage
|
||||
pageTitle={$t('资源配置')}
|
||||
>
|
||||
<div className="flex h-full">
|
||||
<Menu
|
||||
className="h-full overflow-y-auto"
|
||||
selectedKeys={[activeMenu || '']}
|
||||
onClick={onMenuClick}
|
||||
style={{ width: 220 }}
|
||||
mode="inline"
|
||||
items={menuData}
|
||||
/>
|
||||
<div className={`w-full flex flex-1 flex-col h-full overflow-auto bg-MAIN_BG`}>
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
</InsidePage>
|
||||
</Skeleton>
|
||||
</>
|
||||
)
|
||||
useEffect(() => {
|
||||
setActiveMenu(moduleId)
|
||||
}, [moduleId])
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true)
|
||||
getDynamicMenuList()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Skeleton className="m-btnbase w-calc-100vw-minus-padding-r" active loading={loading}>
|
||||
<InsidePage pageTitle={$t('资源配置')}>
|
||||
<div className="flex h-full">
|
||||
<Menu
|
||||
className="overflow-y-auto h-full"
|
||||
selectedKeys={[activeMenu || '']}
|
||||
onClick={onMenuClick}
|
||||
style={{ width: 220 }}
|
||||
mode="inline"
|
||||
items={menuData}
|
||||
/>
|
||||
<div className={`flex overflow-auto flex-col flex-1 w-full h-full bg-MAIN_BG`}>
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
</InsidePage>
|
||||
</Skeleton>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default LogSettings;
|
||||
export default LogSettings
|
||||
|
||||
@@ -1,251 +1,322 @@
|
||||
import {App, Button, Col, Form, Input, Row, Select, Space, Spin, Switch} from "antd";
|
||||
import {forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState} from "react";
|
||||
import EditableTableWithModal from "@common/components/aoplatform/EditableTableWithModal.tsx";
|
||||
import {BasicResponse, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE} from "@common/const/const.tsx";
|
||||
import {useFetch} from "@common/hooks/http.ts";
|
||||
import { API_PATH_MATCH_RULES, API_PROTOCOL, HTTP_METHOD, MATCH_CONFIG, MatchPositionEnum, MatchTypeEnum } from "../../../const/system/const.tsx";
|
||||
import { SystemInsideRouterCreateHandle, SystemInsideRouterCreateProps, SystemApiProxyFieldType, SystemInsideApiProxyHandle } from "../../../const/system/type.ts";
|
||||
import { MatchItem, RouterParams } from "@common/const/type.ts";
|
||||
import { validateUrlSlash } from "@common/utils/validate.ts";
|
||||
import { $t } from "@common/locales/index.ts";
|
||||
import SystemInsideApiProxy from "@core/pages/system/api/SystemInsideApiProxy.tsx";
|
||||
import { LoadingOutlined } from "@ant-design/icons";
|
||||
import { useGlobalContext } from "@common/contexts/GlobalStateContext.tsx";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { useSystemContext } from "@core/contexts/SystemContext.tsx";
|
||||
import InsidePage from "@common/components/aoplatform/InsidePage.tsx";
|
||||
import { LoadingOutlined } from '@ant-design/icons'
|
||||
import EditableTableWithModal from '@common/components/aoplatform/EditableTableWithModal.tsx'
|
||||
import InsidePage from '@common/components/aoplatform/InsidePage.tsx'
|
||||
import { BasicResponse, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const.tsx'
|
||||
import { MatchItem, RouterParams } from '@common/const/type.ts'
|
||||
import { useGlobalContext } from '@common/contexts/GlobalStateContext.tsx'
|
||||
import { useFetch } from '@common/hooks/http.ts'
|
||||
import { $t } from '@common/locales/index.ts'
|
||||
import { validateUrlSlash } from '@common/utils/validate.ts'
|
||||
import { useSystemContext } from '@core/contexts/SystemContext.tsx'
|
||||
import SystemInsideApiProxy from '@core/pages/system/api/SystemInsideApiProxy.tsx'
|
||||
import { App, Button, Col, Form, Input, Row, Select, Space, Spin, Switch } from 'antd'
|
||||
import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
import {
|
||||
API_PATH_MATCH_RULES,
|
||||
API_PROTOCOL,
|
||||
HTTP_METHOD,
|
||||
MATCH_CONFIG,
|
||||
MatchPositionEnum,
|
||||
MatchTypeEnum
|
||||
} from '../../../const/system/const.tsx'
|
||||
import {
|
||||
SystemApiProxyFieldType,
|
||||
SystemInsideApiProxyHandle,
|
||||
SystemInsideRouterCreateHandle,
|
||||
SystemInsideRouterCreateProps
|
||||
} from '../../../const/system/type.ts'
|
||||
|
||||
const SystemInsideRouterCreate = forwardRef<SystemInsideRouterCreateHandle,SystemInsideRouterCreateProps>((props, ref) => {
|
||||
const SystemInsideRouterCreate = forwardRef<SystemInsideRouterCreateHandle, SystemInsideRouterCreateProps>(
|
||||
(props, ref) => {
|
||||
const { message } = App.useApp()
|
||||
const {serviceId, teamId, routeId} = useParams<RouterParams>()
|
||||
const [form] = Form.useForm();
|
||||
const {fetchData} = useFetch()
|
||||
const { serviceId, teamId, routeId } = useParams<RouterParams>()
|
||||
const [form] = Form.useForm()
|
||||
const { fetchData } = useFetch()
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const proxyRef = useRef<SystemInsideApiProxyHandle>(null)
|
||||
const { state } = useGlobalContext()
|
||||
const {apiPrefix, prefixForce} = useSystemContext()
|
||||
const { apiPrefix, prefixForce } = useSystemContext()
|
||||
const navigator = useNavigate()
|
||||
|
||||
const onFinish = ()=>{
|
||||
return Promise.all([proxyRef.current?.validate?.(), form.validateFields()]).then(([,formValue])=>{
|
||||
const body = {...formValue,
|
||||
path: `${prefixForce ? apiPrefix + '/' : ''}${formValue.path.trim()}${formValue.pathMatch === 'prefix' ? '/*' : ''}`,
|
||||
proxy:{...formValue.proxy,path:formValue.proxy.path ? (formValue.proxy.path.startsWith('/')? formValue.proxy.path: '/'+ formValue.proxy.path) : undefined}}
|
||||
return fetchData<BasicResponse<null>>('service/router',{
|
||||
method: routeId ? 'PUT' :'POST' ,eoBody:(body), eoParams: {service:serviceId,team:teamId, router:routeId },eoTransformKeys:['matchType','disable']}).then(response=>{
|
||||
const {code,msg} = response
|
||||
if(code === STATUS_CODE.SUCCESS){
|
||||
message.success(msg || $t(RESPONSE_TIPS.success))
|
||||
navigator(`/service/${teamId}/inside/${serviceId}/route`)
|
||||
return Promise.resolve(true)
|
||||
}else{
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
return Promise.reject(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
}).catch(errInfo=>Promise.reject(errInfo))
|
||||
const onFinish = () => {
|
||||
return Promise.all([proxyRef.current?.validate?.(), form.validateFields()]).then(([, formValue]) => {
|
||||
const body = {
|
||||
...formValue,
|
||||
path: `${prefixForce ? apiPrefix + '/' : ''}${formValue.path.trim()}${formValue.pathMatch === 'prefix' ? '/*' : ''}`,
|
||||
proxy: {
|
||||
...formValue.proxy,
|
||||
path: formValue.proxy.path
|
||||
? formValue.proxy.path.startsWith('/')
|
||||
? formValue.proxy.path
|
||||
: '/' + formValue.proxy.path
|
||||
: undefined
|
||||
}
|
||||
}
|
||||
return fetchData<BasicResponse<null>>('service/router', {
|
||||
method: routeId ? 'PUT' : 'POST',
|
||||
eoBody: body,
|
||||
eoParams: { service: serviceId, team: teamId, router: routeId },
|
||||
eoTransformKeys: ['matchType', 'disable']
|
||||
})
|
||||
}
|
||||
|
||||
const copy: ()=>Promise<boolean | string> = ()=>{
|
||||
return new Promise((resolve, reject)=>{
|
||||
return form.validateFields().then((value)=>{
|
||||
fetchData<BasicResponse<{api:SystemApiProxyFieldType}>>('service/api/copy',{method:'POST',eoParams:{service:serviceId,team:teamId, api:routeId},eoBody:({...value,path:value.path.trim()})}).then(response=>{
|
||||
const {code,data,msg} = response
|
||||
if(code === STATUS_CODE.SUCCESS){
|
||||
message.success(msg || $t(RESPONSE_TIPS.success))
|
||||
return resolve(data.api.id)
|
||||
}else{
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
return reject(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
}).catch((errorInfo)=> reject(errorInfo))
|
||||
}).catch((errorInfo)=> reject(errorInfo))
|
||||
})
|
||||
}
|
||||
|
||||
useImperativeHandle(ref, ()=>({
|
||||
copy,
|
||||
save:onFinish
|
||||
})
|
||||
)
|
||||
|
||||
const getRouterConfig = ()=>{
|
||||
setLoading(true)
|
||||
fetchData<BasicResponse<{router:SystemApiProxyFieldType}>>('service/router/detail',{method:'GET',eoParams:{service:serviceId,team:teamId, router:routeId}, eoTransformKeys:['create_time','update_time','match_type','upstream_id','opt_type']}).then(response=>{
|
||||
const {code,data,msg} = response
|
||||
if(code === STATUS_CODE.SUCCESS){
|
||||
const {disable, protocols, path, methods, description, match, proxy} = data.router
|
||||
let newPath = path
|
||||
let pathMatch = 'full'
|
||||
if(prefixForce && path?.startsWith(apiPrefix + '/')){
|
||||
newPath = path.slice((apiPrefix?.length || 0) + 1)
|
||||
}
|
||||
if(newPath.endsWith('/*')){
|
||||
newPath = newPath.slice(0,-2)
|
||||
pathMatch = 'prefix'
|
||||
}
|
||||
form.setFieldsValue({
|
||||
disable,
|
||||
protocols,
|
||||
path:newPath,
|
||||
pathMatch,
|
||||
methods,
|
||||
description,
|
||||
match,
|
||||
proxy
|
||||
})
|
||||
}else{
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
.then((response) => {
|
||||
const { code, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
message.success(msg || $t(RESPONSE_TIPS.success))
|
||||
navigator(`/service/${teamId}/inside/${serviceId}/route`)
|
||||
return Promise.resolve(true)
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
return Promise.reject(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
}).catch((errorInfo)=> console.error(errorInfo))
|
||||
.finally(()=>setLoading(false))
|
||||
})
|
||||
.catch((errInfo) => Promise.reject(errInfo))
|
||||
})
|
||||
}
|
||||
|
||||
const copy: () => Promise<boolean | string> = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
return form
|
||||
.validateFields()
|
||||
.then((value) => {
|
||||
fetchData<BasicResponse<{ api: SystemApiProxyFieldType }>>('service/api/copy', {
|
||||
method: 'POST',
|
||||
eoParams: { service: serviceId, team: teamId, api: routeId },
|
||||
eoBody: { ...value, path: value.path.trim() }
|
||||
})
|
||||
.then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
message.success(msg || $t(RESPONSE_TIPS.success))
|
||||
return resolve(data.api.id)
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
return reject(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
.catch((errorInfo) => reject(errorInfo))
|
||||
})
|
||||
.catch((errorInfo) => reject(errorInfo))
|
||||
})
|
||||
}
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
copy,
|
||||
save: onFinish
|
||||
}))
|
||||
|
||||
const getRouterConfig = () => {
|
||||
setLoading(true)
|
||||
fetchData<BasicResponse<{ router: SystemApiProxyFieldType }>>('service/router/detail', {
|
||||
method: 'GET',
|
||||
eoParams: { service: serviceId, team: teamId, router: routeId },
|
||||
eoTransformKeys: ['create_time', 'update_time', 'match_type', 'upstream_id', 'opt_type']
|
||||
})
|
||||
.then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
const { disable, protocols, path, methods, description, match, proxy } = data.router
|
||||
let newPath = path
|
||||
let pathMatch = 'full'
|
||||
if (prefixForce && path?.startsWith(apiPrefix + '/')) {
|
||||
newPath = path.slice((apiPrefix?.length || 0) + 1)
|
||||
}
|
||||
if (newPath.endsWith('/*')) {
|
||||
newPath = newPath.slice(0, -2)
|
||||
pathMatch = 'prefix'
|
||||
}
|
||||
form.setFieldsValue({
|
||||
disable,
|
||||
protocols,
|
||||
path: newPath,
|
||||
pathMatch,
|
||||
methods,
|
||||
description,
|
||||
match,
|
||||
proxy
|
||||
})
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
.catch((errorInfo) => console.error(errorInfo))
|
||||
.finally(() => setLoading(false))
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if(routeId){
|
||||
getRouterConfig()
|
||||
}else{
|
||||
form.setFieldValue('prefix',apiPrefix)
|
||||
form.setFieldValue(['proxy','timeout'],10000)
|
||||
form.setFieldValue(['proxy','retry'],0)
|
||||
form.setFieldValue('protocols',['HTTP','HTTPS'])
|
||||
form.setFieldValue('pathMatch','prefix')
|
||||
if (routeId) {
|
||||
getRouterConfig()
|
||||
} else {
|
||||
form.setFieldValue('prefix', apiPrefix)
|
||||
form.setFieldValue(['proxy', 'timeout'], 10000)
|
||||
form.setFieldValue(['proxy', 'retry'], 0)
|
||||
form.setFieldValue('protocols', ['HTTP', 'HTTPS'])
|
||||
form.setFieldValue('pathMatch', 'prefix')
|
||||
}
|
||||
return form.setFieldsValue({})
|
||||
}, [])
|
||||
|
||||
const translatedMatchConfig = useMemo(() => {
|
||||
return MATCH_CONFIG.map((item) => {
|
||||
if (item.key === 'position') {
|
||||
return {
|
||||
...item,
|
||||
component: (
|
||||
<Select
|
||||
className="w-INPUT_NORMAL"
|
||||
options={Object.entries(MatchPositionEnum)?.map(([key, value]) => {
|
||||
return { label: $t(value), value: key }
|
||||
})}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
return (form.setFieldsValue({}))
|
||||
}, []);
|
||||
|
||||
|
||||
const translatedMatchConfig = useMemo(()=>{
|
||||
return MATCH_CONFIG.map((item)=>{
|
||||
if(item.key === 'position'){
|
||||
return ({...item,component:<Select className="w-INPUT_NORMAL" options={Object.entries(MatchPositionEnum)?.map(([key,value])=>{
|
||||
return { label:$t(value), value:key}
|
||||
})}/>})
|
||||
}
|
||||
if(item.key === 'matchType'){
|
||||
return ({...item, component: <Select className="w-INPUT_NORMAL" options={Object.entries(MatchTypeEnum)?.map(([key,value])=>{
|
||||
return { label:$t(value), value:key}
|
||||
})}/>})
|
||||
}
|
||||
return {...item}
|
||||
})
|
||||
if (item.key === 'matchType') {
|
||||
return {
|
||||
...item,
|
||||
component: (
|
||||
<Select
|
||||
className="w-INPUT_NORMAL"
|
||||
options={Object.entries(MatchTypeEnum)?.map(([key, value]) => {
|
||||
return { label: $t(value), value: key }
|
||||
})}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
return { ...item }
|
||||
})
|
||||
}, [state.language])
|
||||
|
||||
|
||||
const apiPathMatchRulesOptions = useMemo(()=>API_PATH_MATCH_RULES.map(
|
||||
x=>({label:$t(x.label), value:x.value})),[state.language])
|
||||
|
||||
const apiPathMatchRulesOptions = useMemo(
|
||||
() => API_PATH_MATCH_RULES.map((x) => ({ label: $t(x.label), value: x.value })),
|
||||
[state.language]
|
||||
)
|
||||
|
||||
return (
|
||||
<InsidePage pageTitle={ $t('API 路由设置')|| '-'}
|
||||
showBorder={false}
|
||||
scrollPage={false}
|
||||
className="overflow-y-auto"
|
||||
backUrl={`/service/${teamId}/inside/${serviceId}/route`}
|
||||
customBtn={
|
||||
<div className="flex gap-btnbase items-center">
|
||||
<Button type="primary" onClick={onFinish}>
|
||||
{$t('保存')}
|
||||
</Button>
|
||||
</div>
|
||||
}>
|
||||
<Spin indicator={<LoadingOutlined style={{ fontSize: 24 }} spin />} spinning={loading} className=''>
|
||||
<Form
|
||||
layout='vertical'
|
||||
labelAlign='left'
|
||||
scrollToFirstError
|
||||
form={form}
|
||||
className="mx-auto flex flex-col h-full"
|
||||
name="SystemInsideRouterCreate"
|
||||
onFinish={onFinish}
|
||||
autoComplete="off"
|
||||
>
|
||||
<div className="">
|
||||
<Row className="mb-btnybase" > <Col ><span className="font-bold mr-[13px]">{$t('API 基础信息')}</span></Col></Row>
|
||||
<Form.Item<SystemApiProxyFieldType>
|
||||
label={$t("拦截该接口的请求")}
|
||||
name="disable"
|
||||
extra={$t('开启拦截后,网关会拦截所有该路径的请求,相当于防火墙禁用了特定路径的访问。')}
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<SystemApiProxyFieldType>
|
||||
label={$t("请求协议")}
|
||||
name="protocols"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Select className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.select)} mode="multiple" options={API_PROTOCOL}>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label={$t("请求路径")}>
|
||||
<Space.Compact block>
|
||||
<Form.Item
|
||||
name="pathMatch"
|
||||
rules={[{ required: true,whitespace:true },
|
||||
{
|
||||
validator: validateUrlSlash,
|
||||
}]}
|
||||
noStyle
|
||||
>
|
||||
<Select placeholder={$t(PLACEHOLDER.select)} options={apiPathMatchRulesOptions} className="w-[30%] min-w-[100px]"/>
|
||||
</Form.Item>
|
||||
<Form.Item<SystemApiProxyFieldType>
|
||||
name="path"
|
||||
rules={[{ required: true,whitespace:true },
|
||||
{
|
||||
validator: validateUrlSlash,
|
||||
}]}
|
||||
noStyle
|
||||
>
|
||||
<Input prefix={(prefixForce ? `${apiPrefix}/` :"/")} className="w-INPUT_NORMAL"
|
||||
placeholder={$t(PLACEHOLDER.input)} onChange={(e)=>{
|
||||
if((e.target.value as string).endsWith('/*')){
|
||||
form.setFieldValue('path',e.target.value.slice(0,-2))
|
||||
form.setFieldValue('pathMatch','prefix')
|
||||
}
|
||||
}}/>
|
||||
</Form.Item>
|
||||
</Space.Compact>
|
||||
</Form.Item>
|
||||
<InsidePage
|
||||
pageTitle={$t('API 路由设置') || '-'}
|
||||
showBorder={false}
|
||||
scrollPage={false}
|
||||
className="overflow-y-auto"
|
||||
backUrl={`/service/${teamId}/inside/${serviceId}/route`}
|
||||
customBtn={
|
||||
<div className="flex items-center gap-btnbase">
|
||||
<Button type="primary" onClick={onFinish}>
|
||||
{$t('保存')}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Spin indicator={<LoadingOutlined style={{ fontSize: 24 }} spin />} spinning={loading} className="">
|
||||
<Form
|
||||
layout="vertical"
|
||||
labelAlign="left"
|
||||
scrollToFirstError
|
||||
form={form}
|
||||
className="flex flex-col mx-auto h-full"
|
||||
name="SystemInsideRouterCreate"
|
||||
onFinish={onFinish}
|
||||
autoComplete="off"
|
||||
>
|
||||
<div className="">
|
||||
<Row className="mb-btnybase">
|
||||
{' '}
|
||||
<Col>
|
||||
<span className="font-bold mr-[13px]">{$t('API 基础信息')}</span>
|
||||
</Col>
|
||||
</Row>
|
||||
<Form.Item<SystemApiProxyFieldType>
|
||||
label={$t('拦截该接口的请求')}
|
||||
name="disable"
|
||||
extra={$t('开启拦截后,网关会拦截所有该路径的请求,相当于防火墙禁用了特定路径的访问。')}
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<SystemApiProxyFieldType>
|
||||
label={$t("请求方式")}
|
||||
name="methods"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Select className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.select)} mode="multiple" options={HTTP_METHOD.map((method:string)=>{
|
||||
return { label:method, value:method}
|
||||
})}>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item<SystemApiProxyFieldType> label={$t('请求协议')} name="protocols" rules={[{ required: true }]}>
|
||||
<Select
|
||||
className="w-INPUT_NORMAL"
|
||||
placeholder={$t(PLACEHOLDER.select)}
|
||||
mode="multiple"
|
||||
options={API_PROTOCOL}
|
||||
></Select>
|
||||
</Form.Item>
|
||||
<Form.Item label={$t('请求路径')}>
|
||||
<Space.Compact block>
|
||||
<Form.Item
|
||||
name="pathMatch"
|
||||
rules={[
|
||||
{ required: true, whitespace: true },
|
||||
{
|
||||
validator: validateUrlSlash
|
||||
}
|
||||
]}
|
||||
noStyle
|
||||
>
|
||||
<Select
|
||||
placeholder={$t(PLACEHOLDER.select)}
|
||||
options={apiPathMatchRulesOptions}
|
||||
className="w-[30%] min-w-[100px]"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item<SystemApiProxyFieldType>
|
||||
name="path"
|
||||
rules={[
|
||||
{ required: true, whitespace: true },
|
||||
{
|
||||
validator: validateUrlSlash
|
||||
}
|
||||
]}
|
||||
noStyle
|
||||
>
|
||||
<Input
|
||||
prefix={prefixForce ? `${apiPrefix}/` : '/'}
|
||||
className="w-INPUT_NORMAL"
|
||||
placeholder={$t(PLACEHOLDER.input)}
|
||||
onChange={(e) => {
|
||||
if ((e.target.value as string).endsWith('/*')) {
|
||||
form.setFieldValue('path', e.target.value.slice(0, -2))
|
||||
form.setFieldValue('pathMatch', 'prefix')
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Space.Compact>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<SystemApiProxyFieldType>
|
||||
label={$t("描述")}
|
||||
name="description"
|
||||
>
|
||||
<Input.TextArea className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)}/>
|
||||
</Form.Item>
|
||||
<Form.Item<SystemApiProxyFieldType> label={$t('请求方式')} name="methods" rules={[{ required: true }]}>
|
||||
<Select
|
||||
className="w-INPUT_NORMAL"
|
||||
placeholder={$t(PLACEHOLDER.select)}
|
||||
mode="multiple"
|
||||
options={HTTP_METHOD.map((method: string) => {
|
||||
return { label: method, value: method }
|
||||
})}
|
||||
></Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<SystemApiProxyFieldType>
|
||||
label={$t("高级匹配")}
|
||||
name="match"
|
||||
>
|
||||
<EditableTableWithModal<MatchItem & {_id:string}>
|
||||
configFields={translatedMatchConfig}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item<SystemApiProxyFieldType> label={$t('描述')} name="description">
|
||||
<Input.TextArea className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)} />
|
||||
</Form.Item>
|
||||
|
||||
<Row className="mb-btnybase mt-[40px]"><Col ><span className="font-bold mr-[13px]">{$t('转发规则设置')} </span></Col></Row>
|
||||
<Form.Item<SystemApiProxyFieldType>
|
||||
className="mb-0 bg-transparent border-none p-0"
|
||||
name="proxy"
|
||||
>
|
||||
<SystemInsideApiProxy type={routeId ? 'edit' : 'add'} ref={proxyRef} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
</Form>
|
||||
</Spin>
|
||||
</InsidePage>
|
||||
<Form.Item<SystemApiProxyFieldType> label={$t('高级匹配')} name="match">
|
||||
<EditableTableWithModal<MatchItem & { _id: string }> configFields={translatedMatchConfig} />
|
||||
</Form.Item>
|
||||
|
||||
<Row className="mb-btnybase mt-[40px]">
|
||||
<Col>
|
||||
<span className="font-bold mr-[13px]">{$t('转发规则设置')} </span>
|
||||
</Col>
|
||||
</Row>
|
||||
<Form.Item<SystemApiProxyFieldType> className="p-0 mb-0 bg-transparent border-none" name="proxy">
|
||||
<SystemInsideApiProxy type={routeId ? 'edit' : 'add'} ref={proxyRef} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
</Form>
|
||||
</Spin>
|
||||
</InsidePage>
|
||||
)
|
||||
})
|
||||
export default SystemInsideRouterCreate
|
||||
}
|
||||
)
|
||||
export default SystemInsideRouterCreate
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import { CloseOutlined, ExpandOutlined, SearchOutlined } from '@ant-design/icons'
|
||||
import { Select, Input, Button, App, Drawer } from 'antd'
|
||||
import { debounce } from 'lodash-es'
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { MonitorApiData, SearchBody } from '@dashboard/const/type'
|
||||
import { getTime } from '../utils/dashboard'
|
||||
import ScrollableSection from '@common/components/aoplatform/ScrollableSection'
|
||||
import TimeRangeSelector, {
|
||||
RangeValue,
|
||||
TimeRange,
|
||||
TimeRangeButton
|
||||
} from '@common/components/aoplatform/TimeRangeSelector'
|
||||
import MonitorTable, { MonitorTableHandler } from './MonitorTable'
|
||||
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
|
||||
import { DefaultOptionType } from 'antd/es/select'
|
||||
import { useExcelExport } from '@common/hooks/excel'
|
||||
import { API_TABLE_GLOBAL_COLUMNS_CONFIG } from '@dashboard/const/const'
|
||||
import { useFetch } from '@common/hooks/http'
|
||||
import { EntityItem } from '@common/const/type'
|
||||
import { useExcelExport } from '@common/hooks/excel'
|
||||
import { useFetch } from '@common/hooks/http'
|
||||
import { $t } from '@common/locales'
|
||||
import { API_TABLE_GLOBAL_COLUMNS_CONFIG } from '@dashboard/const/const'
|
||||
import { MonitorApiData, SearchBody } from '@dashboard/const/type'
|
||||
import { App, Button, Drawer, Input, Select } from 'antd'
|
||||
import { DefaultOptionType } from 'antd/es/select'
|
||||
import { debounce } from 'lodash-es'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { getTime } from '../utils/dashboard'
|
||||
import MonitorTable, { MonitorTableHandler } from './MonitorTable'
|
||||
export type MonitorApiPageProps = {
|
||||
fetchTableData: (body: SearchBody) => Promise<BasicResponse<{ statistics: MonitorApiData[] }>>
|
||||
detailDrawerContent: React.ReactNode
|
||||
@@ -100,6 +100,7 @@ export default function MonitorApiPage(props: MonitorApiPageProps) {
|
||||
const getMonitorData = () => {
|
||||
let query = queryData
|
||||
if (!queryData || queryData.start === undefined) {
|
||||
console.log(timeButton, datePickerValue)
|
||||
const { startTime, endTime } = getTime(timeButton, datePickerValue || [])
|
||||
query = { ...query, start: startTime, end: endTime }
|
||||
}
|
||||
@@ -186,7 +187,7 @@ export default function MonitorApiPage(props: MonitorApiPageProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full overflow-hidden">
|
||||
<div className="overflow-hidden h-full">
|
||||
<ScrollableSection>
|
||||
<div className="pl-btnbase pr-btnrbase pb-btnbase content-before">
|
||||
<TimeRangeSelector
|
||||
@@ -196,8 +197,8 @@ export default function MonitorApiPage(props: MonitorApiPageProps) {
|
||||
initialDatePickerValue={datePickerValue}
|
||||
onTimeRangeChange={handleTimeRangeChange}
|
||||
/>
|
||||
<div className="flex flex-nowrap items-center pt-btnybase mr-btnybase">
|
||||
<label className=" whitespace-nowrap inline-block">{$t('服务')}:</label>
|
||||
<div className="flex flex-nowrap items-center pt-btnybase mr-btnybase">
|
||||
<label className="inline-block whitespace-nowrap">{$t('服务')}:</label>
|
||||
<Select
|
||||
className="w-[346px]"
|
||||
value={queryData?.services}
|
||||
@@ -212,7 +213,7 @@ export default function MonitorApiPage(props: MonitorApiPageProps) {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-nowrap items-center pt-btnybase mr-btnybase">
|
||||
<div className="flex flex-nowrap items-center pt-btnybase mr-btnybase">
|
||||
<label className=" whitespace-nowrap inline-block w-[42px] text-right">API :</label>
|
||||
<Select
|
||||
className="w-[346px]"
|
||||
@@ -226,7 +227,7 @@ export default function MonitorApiPage(props: MonitorApiPageProps) {
|
||||
setQueryData((prevData) => ({ ...(prevData || {}), apis: value }))
|
||||
}}
|
||||
/>
|
||||
<label className="ml-btnybase whitespace-nowrap">{$t('路径')}:</label>
|
||||
<label className="whitespace-nowrap ml-btnybase">{$t('路径')}:</label>
|
||||
<div className="w-[346px] inline-block">
|
||||
{/* <SearchInputGroup eoSingle={false} eoInputVal={queryData.path} eoClick={() => setQueryData({ ...queryData, path: '' })} /> */}
|
||||
<Input
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# Translation Workflow in Windsurf
|
||||
|
||||
Follow these steps to manage translations in the project:
|
||||
|
||||
1. **Scan for New Translations**
|
||||
* Navigate to the `frontend` directory
|
||||
* Run `pnpm run scan` to detect new translatable content
|
||||
|
||||
2. **Locate New Translation Fields**
|
||||
* Go to `packages/common/src/locales/scan/newJson`
|
||||
* Find the language-specific JSON files (e.g., en-US.json, ja-JP.json)
|
||||
* These files contain the new fields that need translation
|
||||
|
||||
3. **Apply Translations**
|
||||
* After translating the content, go to `packages/common/src/locales/scan`
|
||||
* Open the corresponding language JSON file include ja-JP.json,en-US.json,zh-CH.json,zh-TW.json
|
||||
* Paste the translated content into the appropriate file
|
||||
|
||||
4. **Save and Apply**
|
||||
* Save the file
|
||||
* Changes will take effect immediately
|
||||
* No additional build or restart is required
|
||||
|
||||
Note: Available language files are en-US.json, ja-JP.json, zh-CN.json, and zh-TW.json.
|
||||
Reference in New Issue
Block a user