fix: show required fields for provider

This commit is contained in:
jialin
2026-03-20 17:23:55 +08:00
committed by jialin
parent 0a0e03dc68
commit f873d4e245
12 changed files with 400 additions and 42 deletions
+15 -1
View File
@@ -23,5 +23,19 @@ export default {
'providers.form.rules.tokens': 'Please enter a valid API Key', 'providers.form.rules.tokens': 'Please enter a valid API Key',
'providers.form.rules.model': 'Please select a model', 'providers.form.rules.model': 'Please select a model',
'providers.form.model.duplicate': 'Duplicate model exists', 'providers.form.model.duplicate': 'Duplicate model exists',
'providers.table.registerRoute': 'Register Route' 'providers.table.registerRoute': 'Register Route',
'providers.form.azureServiceUrl': 'Azure OpenAI Service URL',
'providers.form.ollamaServerHost': 'Ollama Server Host',
'providers.form.ollamaServerPort': 'Ollama Server Port',
'providers.form.hunyuanAuthId': 'Hunyuan Auth ID',
'providers.form.hunyuanAuthKey': 'Hunyuan Auth Key',
'providers.form.cloudflareAccountId': 'Cloudflare Account ID',
'providers.form.targetLang': 'Target Language',
'providers.form.modelVersion': 'Model Version',
'providers.form.tritonDomain': 'Triton Server Domain',
'providers.form.modelVersion.tips':
'Specifies the model version used in Triton Server.',
'providers.form.tritonDomain.tips':
'The domain used to send requests to the Triton Server deployment.',
'providers.form.awsRegion': 'AWS Region'
}; };
+15 -1
View File
@@ -23,5 +23,19 @@ export default {
'providers.form.rules.tokens': 'Please enter a valid API Key', 'providers.form.rules.tokens': 'Please enter a valid API Key',
'providers.form.rules.model': 'Please select a model', 'providers.form.rules.model': 'Please select a model',
'providers.form.model.duplicate': 'Duplicate model exists', 'providers.form.model.duplicate': 'Duplicate model exists',
'providers.table.registerRoute': 'Register Route' 'providers.table.registerRoute': 'Register Route',
'providers.form.azureServiceUrl': 'Azure OpenAI Service URL',
'providers.form.ollamaServerHost': 'Ollama Server Host',
'providers.form.ollamaServerPort': 'Ollama Server Port',
'providers.form.hunyuanAuthId': 'Hunyuan Auth ID',
'providers.form.hunyuanAuthKey': 'Hunyuan Auth Key',
'providers.form.cloudflareAccountId': 'Cloudflare Account ID',
'providers.form.targetLang': 'Target Language',
'providers.form.modelVersion': 'Model Version',
'providers.form.tritonDomain': 'Triton Server Domain',
'providers.form.modelVersion.tips':
'Specifies the model version used in Triton Server.',
'providers.form.tritonDomain.tips':
'The domain used to send requests to the Triton Server deployment.',
'providers.form.awsRegion': 'AWS Region'
}; };
+15 -1
View File
@@ -23,5 +23,19 @@ export default {
'providers.form.rules.tokens': 'Please enter a valid API Key', 'providers.form.rules.tokens': 'Please enter a valid API Key',
'providers.form.rules.model': 'Please select a model', 'providers.form.rules.model': 'Please select a model',
'providers.form.model.duplicate': 'Duplicate model exists', 'providers.form.model.duplicate': 'Duplicate model exists',
'providers.table.registerRoute': 'Register Route' 'providers.table.registerRoute': 'Register Route',
'providers.form.azureServiceUrl': 'Azure OpenAI Service URL',
'providers.form.ollamaServerHost': 'Ollama Server Host',
'providers.form.ollamaServerPort': 'Ollama Server Port',
'providers.form.hunyuanAuthId': 'Hunyuan Auth ID',
'providers.form.hunyuanAuthKey': 'Hunyuan Auth Key',
'providers.form.cloudflareAccountId': 'Cloudflare Account ID',
'providers.form.targetLang': 'Target Language',
'providers.form.modelVersion': 'Model Version',
'providers.form.tritonDomain': 'Triton Server Domain',
'providers.form.modelVersion.tips':
'Specifies the model version used in Triton Server.',
'providers.form.tritonDomain.tips':
'The domain used to send requests to the Triton Server deployment.',
'providers.form.awsRegion': 'AWS Region'
}; };
+13 -1
View File
@@ -23,5 +23,17 @@ export default {
'providers.form.rules.tokens': '请输入有效的 API Key', 'providers.form.rules.tokens': '请输入有效的 API Key',
'providers.form.rules.model': '请选择模型', 'providers.form.rules.model': '请选择模型',
'providers.form.model.duplicate': '存在相同的模型', 'providers.form.model.duplicate': '存在相同的模型',
'providers.table.registerRoute': '注册路由' 'providers.table.registerRoute': '注册路由',
'providers.form.azureServiceUrl': 'Azure OpenAI 服务 URL',
'providers.form.ollamaServerHost': 'Ollama 服务地址',
'providers.form.ollamaServerPort': 'Ollama 服务端口',
'providers.form.hunyuanAuthId': '混元认证 ID',
'providers.form.hunyuanAuthKey': '混元认证 Key',
'providers.form.cloudflareAccountId': 'Cloudflare 账号 ID',
'providers.form.targetLang': '翻译目标语言',
'providers.form.modelVersion': '模型版本',
'providers.form.modelVersion.tips': '用于指定 Triton Server 中的模型版本。',
'providers.form.tritonDomain': 'Triton Server 域名',
'providers.form.tritonDomain.tips': 'Triton Server 部署的指定请求的域名。',
'providers.form.awsRegion': 'AWS 区域'
}; };
@@ -1,12 +1,14 @@
import { PageActionType } from '@/config/types'; import { PageActionType } from '@/config/types';
import { createContext, useContext } from 'react'; import { createContext, useContext } from 'react';
import { maasProviderType } from '.'; import { maasProviderType } from '.';
import { RequiredFields } from './types';
interface FormContextProps { interface FormContextProps {
providerType?: maasProviderType; providerType?: maasProviderType;
action: PageActionType; action: PageActionType;
currentData?: any; currentData?: any;
id?: number; id?: number;
providerFields?: RequiredFields[];
getCustomConfig?: () => Record<string, any>; getCustomConfig?: () => Record<string, any>;
} }
+16
View File
@@ -40,3 +40,19 @@ export interface MaasProviderItem {
api_token_count: number; api_token_count: number;
api_tokens: { hash: string }[]; api_tokens: { hash: string }[];
} }
export interface RequiredFields {
type: 'Input' | 'Select' | 'Password';
name: string;
label: {
text: string;
locale?: boolean;
};
required?: boolean;
placeholder?: string;
description?: {
text: string;
locale?: boolean;
};
rules?: any[];
}
+3 -11
View File
@@ -7,8 +7,9 @@ import { useIntl } from '@umijs/max';
import { Form } from 'antd'; import { Form } from 'antd';
import ProviderLogo from '../components/provider-logo'; import ProviderLogo from '../components/provider-logo';
import { useFormContext } from '../config/form-context'; import { useFormContext } from '../config/form-context';
import { maasProviderOptions, ProviderEnum } from '../config/providers'; import { maasProviderOptions } from '../config/providers';
import { FormData } from '../config/types'; import { FormData } from '../config/types';
import ProviderConfigs from './provider-configs';
const Basic: React.FC<{ const Basic: React.FC<{
onAPIKeyBlur?: (e: any) => void; onAPIKeyBlur?: (e: any) => void;
@@ -77,16 +78,7 @@ const Basic: React.FC<{
})} })}
/> />
</Form.Item> </Form.Item>
{providerType === ProviderEnum.OPENAI && ( <ProviderConfigs />
<Form.Item<FormData> name={['config', 'openaiCustomUrl']}>
<SealInput.Input
placeholder="http://<your-inference-server>/v1"
label={intl.formatMessage({
id: 'providers.form.custombeckendUrl'
})}
/>
</Form.Item>
)}
<Form.Item<FormData> <Form.Item<FormData>
name="api_key" name="api_key"
rules={[ rules={[
+32 -12
View File
@@ -10,9 +10,16 @@ import { json2Yaml, yaml2Json } from '@/pages/backends/config';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Form } from 'antd'; import { Form } from 'antd';
import _ from 'lodash'; import _ from 'lodash';
import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react'; import {
forwardRef,
useEffect,
useImperativeHandle,
useMemo,
useRef
} from 'react';
import FormContext from '../config/form-context'; import FormContext from '../config/form-context';
import { FormData, MaasProviderItem as ListItem } from '../config/types'; import { FormData, MaasProviderItem as ListItem } from '../config/types';
import useProviderRequiredFields from '../hooks/use-provider-required-fields';
import AdvanceConfig from './advance-config'; import AdvanceConfig from './advance-config';
import Basic from './basic'; import Basic from './basic';
import SupportedModels from './supported-models'; import SupportedModels from './supported-models';
@@ -44,14 +51,15 @@ const requiredFields = {
const ProviderForm: React.FC<ProviderFormProps> = forwardRef((props, ref) => { const ProviderForm: React.FC<ProviderFormProps> = forwardRef((props, ref) => {
const { action, currentData, onFinish } = props; const { action, currentData, onFinish } = props;
const intl = useIntl(); const intl = useIntl();
const providerRequiredFieldsMap = useProviderRequiredFields();
const [form] = Form.useForm(); const [form] = Form.useForm();
const { getScrollElementScrollableHeight } = useWrapperContext(); const { getScrollElementScrollableHeight } = useWrapperContext();
const configType = Form.useWatch(['config', 'type'], form);
const scrollTabsRef = useRef<any>(null); const scrollTabsRef = useRef<any>(null);
const advanceRef = useRef<any>(null); const advanceRef = useRef<any>(null);
const { const {
activeKey, activeKey,
collapseKeys, collapseKeys,
setCollapseKeys,
handleActiveChange, handleActiveChange,
handleOnCollapseChange, handleOnCollapseChange,
updateActiveKey updateActiveKey
@@ -81,6 +89,11 @@ const ProviderForm: React.FC<ProviderFormProps> = forwardRef((props, ref) => {
} }
]; ];
const providerFields = useMemo(() => {
const currentProviderFields = providerRequiredFieldsMap[configType] || [];
return currentProviderFields;
}, [providerRequiredFieldsMap, configType]);
const formatAPIKeys = (values: FormData) => { const formatAPIKeys = (values: FormData) => {
const apiTokens = values.api_tokens?.filter?.( const apiTokens = values.api_tokens?.filter?.(
(item) => item && item.trim() !== '' (item) => item && item.trim() !== ''
@@ -100,7 +113,11 @@ const ProviderForm: React.FC<ProviderFormProps> = forwardRef((props, ref) => {
const getCustomConfig = () => { const getCustomConfig = () => {
const customConfig = yaml2Json(advanceRef.current?.getYamlValue() || ''); const customConfig = yaml2Json(advanceRef.current?.getYamlValue() || '');
return customConfig; const config = form.getFieldValue('config') || {};
return {
...config,
...customConfig
};
}; };
const handleOnFinish = (values: FormData) => { const handleOnFinish = (values: FormData) => {
@@ -108,8 +125,7 @@ const ProviderForm: React.FC<ProviderFormProps> = forwardRef((props, ref) => {
..._.omit(values, ['api_key']), ..._.omit(values, ['api_key']),
api_tokens: formatAPIKeys(values), api_tokens: formatAPIKeys(values),
config: { config: {
type: values.config.type, ...values.config,
openaiCustomUrl: values.config.openaiCustomUrl || undefined,
...yaml2Json(advanceRef.current?.getYamlValue() || '') ...yaml2Json(advanceRef.current?.getYamlValue() || '')
}, },
models: _.uniqBy(values.models, 'name'), models: _.uniqBy(values.models, 'name'),
@@ -145,16 +161,19 @@ const ProviderForm: React.FC<ProviderFormProps> = forwardRef((props, ref) => {
const apiTokensList = _.get(currentData, 'api_tokens', []).map( const apiTokensList = _.get(currentData, 'api_tokens', []).map(
(item: any) => item.hash || '' (item: any) => item.hash || ''
); );
const customConfigYaml = json2Yaml( const currentProvider = _.get(
_.omit(currentData.config, ['type', 'openaiCustomUrl']) || {} providerRequiredFieldsMap,
[currentData.config.type],
[]
); );
const customConfig = const currentRequiredFields = currentProvider.map(
_.omit(currentData.config, ['type', 'openaiCustomUrl']) || {}; (item: { name: string }) => item.name
);
if (Object.keys(customConfig).length > 0) { const customConfigYaml = json2Yaml(
setCollapseKeys((prev) => [...new Set([...prev, TABKeysMap.ADVANCED])]); _.omit(currentData.config, ['type', ...currentRequiredFields]) || {}
} );
form.setFieldsValue({ form.setFieldsValue({
...currentData, ...currentData,
@@ -189,6 +208,7 @@ const ProviderForm: React.FC<ProviderFormProps> = forwardRef((props, ref) => {
action, action,
id: currentData?.id, id: currentData?.id,
currentData, currentData,
providerFields,
getCustomConfig: getCustomConfig getCustomConfig: getCustomConfig
}} }}
> >
+1 -5
View File
@@ -89,11 +89,7 @@ const ModelItem: React.FC<ModelItemProps> = ({
: null, : null,
config: { config: {
type: form.getFieldValue(['config', 'type']) || '', type: form.getFieldValue(['config', 'type']) || '',
...customConfig, ...customConfig
openaiCustomUrl:
customConfig?.openaiCustomUrl ||
form.getFieldValue(['config', 'openaiCustomUrl']) ||
null
} }
} }
}); });
@@ -0,0 +1,61 @@
import Password from '@/components/seal-form/password';
import SealInput from '@/components/seal-form/seal-input';
import { useIntl } from '@umijs/max';
import { Form } from 'antd';
import { useFormContext } from '../config/form-context';
import { FormData } from '../config/types';
const ProviderConfigs = () => {
const intl = useIntl();
const form = Form.useFormInstance<FormData>();
const { providerFields } = useFormContext();
const renderLabel = (item: any) => {
return item.label.locale
? intl.formatMessage({ id: item.label.text })
: item.label.text;
};
const renderDescription = (item: any) => {
return item.description
? item.description.locale
? intl.formatMessage({ id: item.description.text })
: item.description.text
: undefined;
};
return (
<>
{providerFields && providerFields.length > 0
? providerFields?.map((item) => {
return (
<Form.Item
name={['config', item.name]}
rules={item.rules}
key={item.name}
>
{item.type === 'Input' && (
<SealInput.Input
required={item.required}
description={renderDescription(item)}
label={renderLabel(item)}
placeholder={item.placeholder}
></SealInput.Input>
)}
{item.type === 'Password' && (
<Password
required={item.required}
label={renderLabel(item)}
description={renderDescription(item)}
placeholder={item.placeholder}
></Password>
)}
</Form.Item>
);
})
: null}
</>
);
};
export default ProviderConfigs;
@@ -18,8 +18,8 @@ const SupportedModels = () => {
const prevConfigRef = useRef<{ const prevConfigRef = useRef<{
type: string; type: string;
api_key: string; api_key: string;
openaiCustomUrl: string; [key: string]: any;
}>({ type: '', api_key: '', openaiCustomUrl: '' }); }>({ type: '', api_key: '' });
const { id, action, currentData, getCustomConfig } = useFormContext(); const { id, action, currentData, getCustomConfig } = useFormContext();
const generateCurrentAPIKey = (currentAPIKey: string) => { const generateCurrentAPIKey = (currentAPIKey: string) => {
@@ -42,7 +42,7 @@ const SupportedModels = () => {
const checkConfigChange = (current: { const checkConfigChange = (current: {
type: string; type: string;
api_key: string; api_key: string;
openaiCustomUrl: string; [key: string]: any;
}) => { }) => {
return ( return (
!_.isEqual(current, prevConfigRef.current) && !_.isEqual(current, prevConfigRef.current) &&
@@ -58,13 +58,12 @@ const SupportedModels = () => {
const proxyConfigEnabled = form.getFieldValue('proxy_enabled'); const proxyConfigEnabled = form.getFieldValue('proxy_enabled');
const currentAPIKey = form.getFieldValue('api_key') || ''; const currentAPIKey = form.getFieldValue('api_key') || '';
const configType = form.getFieldValue(['config', 'type']); const configType = form.getFieldValue(['config', 'type']);
const openaiCustomUrl = form.getFieldValue(['config', 'openaiCustomUrl']);
const customConfig = getCustomConfig?.(); const customConfig = getCustomConfig?.();
const currentConfig = { const currentConfig = {
type: configType, type: configType,
api_key: currentAPIKey, api_key: currentAPIKey,
openaiCustomUrl: customConfig?.openaiCustomUrl || openaiCustomUrl || '' ...customConfig
}; };
// Avoid repeated requests with the same API key // Avoid repeated requests with the same API key
@@ -82,9 +81,7 @@ const SupportedModels = () => {
: null, : null,
config: { config: {
type: form.getFieldValue(['config', 'type']) || '', type: form.getFieldValue(['config', 'type']) || '',
...customConfig, ...customConfig
openaiCustomUrl:
customConfig?.openaiCustomUrl || openaiCustomUrl || null
} }
} }
}); });
@@ -92,8 +89,7 @@ const SupportedModels = () => {
} catch (error) { } catch (error) {
prevConfigRef.current = { prevConfigRef.current = {
type: '', type: '',
api_key: '', api_key: ''
openaiCustomUrl: ''
}; };
// If validation fails, reset the provider model list to avoid confusion // If validation fails, reset the provider model list to avoid confusion
} }
@@ -0,0 +1,221 @@
import useAppUtils from '@/hooks/use-app-utils';
import { useIntl } from '@umijs/max';
import { ProviderEnum } from '../config/providers';
import { RequiredFields } from '../config/types';
const useProviderRequiredFields = () => {
const intl = useIntl();
const { getRuleMessage } = useAppUtils();
const providerRequiredFieldsMap: Record<string, RequiredFields[]> = {
[ProviderEnum.OPENAI]: [
{
type: 'Input',
name: 'openaiCustomUrl',
placeholder: 'http://<your-inference-server>/v1',
required: false,
label: {
text: 'providers.form.custombeckendUrl',
locale: true
}
}
],
[ProviderEnum.AZURE]: [
{
type: 'Input',
name: 'azureServiceUrl',
required: true,
label: {
text: 'providers.form.azureServiceUrl',
locale: true
},
rules: [
{
required: true,
message: getRuleMessage('input', 'providers.form.azureServiceUrl')
}
]
}
],
[ProviderEnum.OLLAMA]: [
{
type: 'Input',
name: 'ollamaServerHost',
required: true,
label: {
text: 'providers.form.ollamaServerHost',
locale: true
},
rules: [
{
required: true,
message: getRuleMessage('input', 'providers.form.ollamaServerHost')
}
]
},
{
type: 'Input',
name: 'ollamaServerPort',
required: true,
label: {
text: 'providers.form.ollamaServerPort',
locale: true
},
rules: [
{
required: true,
message: getRuleMessage('input', 'providers.form.ollamaServerPort')
}
]
}
],
[ProviderEnum.HUNYUAN]: [
{
type: 'Input',
name: 'hunyuanAuthId',
required: true,
label: {
text: 'providers.form.hunyuanAuthId',
locale: true
},
rules: [
{
required: true,
message: getRuleMessage('input', 'providers.form.hunyuanAuthId')
}
]
},
{
type: 'Password',
name: 'hunyuanAuthKey',
required: true,
label: {
text: 'providers.form.hunyuanAuthKey',
locale: true
},
rules: [
{
required: true,
message: getRuleMessage('input', 'providers.form.hunyuanAuthKey')
}
]
}
],
[ProviderEnum.CLOUDFLARE]: [
{
type: 'Input',
name: 'cloudflareAccountId',
required: true,
label: {
text: 'providers.form.cloudflareAccountId',
locale: true
},
rules: [
{
required: true,
message: getRuleMessage(
'input',
'providers.form.cloudflareAccountId'
)
}
]
}
],
[ProviderEnum.DEEPL]: [
{
type: 'Input',
name: 'targetLang',
required: true,
label: {
text: 'providers.form.targetLang',
locale: true
},
rules: [
{
required: true,
message: getRuleMessage('input', 'providers.form.targetLang')
}
]
}
],
[ProviderEnum.BEDROCK]: [
{
type: 'Input',
name: 'modelVersion',
required: false,
label: {
text: 'providers.form.modelVersion',
locale: true
},
description: {
text: 'providers.form.modelVersion.tips',
locale: true
}
},
{
type: 'Input',
name: 'tritonDomain',
required: false,
label: {
text: 'providers.form.tritonDomain',
locale: true
},
description: {
text: 'providers.form.tritonDomain.tips',
locale: true
}
}
],
[ProviderEnum.TRITON]: [
{
type: 'Input',
name: 'awsAccessKey',
required: true,
label: {
text: 'AWS Access Key',
locale: false
},
rules: [
{
required: true,
message: getRuleMessage('input', 'AWS Access Key', false)
}
]
},
{
type: 'Password',
name: 'awsSecretKey',
required: true,
label: {
text: 'AWS Secret Key',
locale: false
},
rules: [
{
required: true,
message: getRuleMessage('input', 'AWS Secret Key', false)
}
]
},
{
type: 'Input',
name: 'awsRegion',
required: true,
label: {
text: 'providers.form.awsRegion',
locale: true
},
rules: [
{
required: true,
message: getRuleMessage('input', 'providers.form.awsRegion')
}
]
}
]
};
return providerRequiredFieldsMap;
};
export default useProviderRequiredFields;