fix: AI API should remove prefix matching drop-down selection

This commit is contained in:
scarqin
2025-01-06 19:29:15 +08:00
parent 2a1f0175a9
commit 6ceae331ce
3 changed files with 314 additions and 254 deletions
@@ -9,7 +9,8 @@ export const AI_SERVICE_ROUTER_TABLE_COLUMNS: PageProColumns<AiServiceRouterTabl
{
title: 'URL',
dataIndex: 'requestPath',
ellipsis: true
ellipsis: true,
width: 200
},
{
title: '名称',
@@ -4,6 +4,7 @@ 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'
@@ -16,11 +17,10 @@ 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, Select, Space, Spin, Switch, Tag } from 'antd'
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'
import WithPermission from '@common/components/aoplatform/WithPermission'
type AiServiceRouterField = {
name: string
@@ -285,11 +285,11 @@ const AiServiceInsideRouterCreate = () => {
{defaultLlm?.scopes?.map((x) => <Tag>{x?.toLocaleUpperCase()}</Tag>)}
</div>
</Button>
{
type !== 'apiDetail' && (<Button type="primary" onClick={onFinish}>
{type !== 'apiDetail' && (
<Button type="primary" onClick={onFinish}>
{$t('保存')}
</Button>)
}
</Button>
)}
</div>
}
>
@@ -323,22 +323,6 @@ const AiServiceInsideRouterCreate = () => {
<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={[
@@ -365,7 +349,11 @@ const AiServiceInsideRouterCreate = () => {
</Row>
<Form.Item<AiServiceRouterField> label={$t('提示词')} name="prompt">
<PromptEditorResizable disabled={isDelete} variablesChange={handleVariablesChange} promptVariables={variablesTable} />
<PromptEditorResizable
disabled={isDelete}
variablesChange={handleVariablesChange}
promptVariables={variablesTable}
/>
</Form.Item>
<Form.Item<AiServiceRouterField>
@@ -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