fix(web): model bugfix

This commit is contained in:
zhaoying
2026-01-29 12:10:19 +08:00
parent 0eb335d112
commit 1e16b06a24
11 changed files with 173 additions and 114 deletions

View File

@@ -546,7 +546,10 @@ export const en = {
tags: 'Tags', tags: 'Tags',
createCustomModel: 'Add Custom Model', createCustomModel: 'Add Custom Model',
edit: 'Edit', edit: 'Edit',
selectOneTip: 'Model API KEY not configured, please configure in Model Plaza first', selectOneTip: 'Model API KEY not configured, please configure it in the model list first',
load_balance_strategy: 'Concurrency Strategy',
round_robin: 'Sequential Execution - Call each model in order',
none: 'None',
api_key: 'API KEY', api_key: 'API KEY',
api_base: 'API Base URL', api_base: 'API Base URL',

View File

@@ -1112,7 +1112,10 @@ export const zh = {
tags: '标签', tags: '标签',
createCustomModel: '添加自定义模型', createCustomModel: '添加自定义模型',
edit: '编辑', edit: '编辑',
selectOneTip: '模型未配置API KEY请先在模型广场配置', selectOneTip: '模型未配置API KEY请先在模型列表配置',
load_balance_strategy: '并发策略',
round_robin: '顺序执行 - 按顺序依次调用每个模型',
none: '无',
api_key: 'API KEY', api_key: 'API KEY',
api_base: 'API Base URL', api_base: 'API Base URL',

View File

@@ -129,6 +129,7 @@ const CustomModelModal = forwardRef<CustomModelModalRef, CustomModelModalProps>(
<CustomSelect <CustomSelect
url={modelTypeUrl} url={modelTypeUrl}
hasAll={false} hasAll={false}
disabled={isEdit}
format={(items) => items.map((item) => ({ label: t(`modelNew.${item}`), value: String(item) }))} format={(items) => items.map((item) => ({ label: t(`modelNew.${item}`), value: String(item) }))}
/> />
</Form.Item> </Form.Item>
@@ -141,6 +142,7 @@ const CustomModelModal = forwardRef<CustomModelModalRef, CustomModelModalProps>(
<CustomSelect <CustomSelect
url={modelProviderUrl} url={modelProviderUrl}
hasAll={false} hasAll={false}
disabled={isEdit}
format={(items) => items.map((item) => ({ label: t(`modelNew.${item}`), value: String(item) }))} format={(items) => items.map((item) => ({ label: t(`modelNew.${item}`), value: String(item) }))}
/> />
</Form.Item> </Form.Item>

View File

@@ -1,5 +1,5 @@
import { forwardRef, useImperativeHandle, useState } from 'react'; import { forwardRef, useImperativeHandle, useState } from 'react';
import { Form, Input, App } from 'antd'; import { Form, Input, App, Select } from 'antd';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import type { ModelListItem, CompositeModelForm, GroupModelModalRef, GroupModelModalProps, ModelApiKey } from '../types'; import type { ModelListItem, CompositeModelForm, GroupModelModalRef, GroupModelModalProps, ModelApiKey } from '../types';
@@ -106,6 +106,7 @@ const GroupModelModal = forwardRef<GroupModelModalRef, GroupModelModalProps>(({
<Form <Form
form={form} form={form}
layout="vertical" layout="vertical"
initialValues={{ balance_strategy: 'none' }}
> >
<Form.Item <Form.Item
name="logo" name="logo"
@@ -147,6 +148,19 @@ const GroupModelModal = forwardRef<GroupModelModalRef, GroupModelModalProps>(({
<Input.TextArea placeholder={t('common.pleaseEnter')} /> <Input.TextArea placeholder={t('common.pleaseEnter')} />
</Form.Item> </Form.Item>
<Form.Item
name="load_balance_strategy"
label={t('modelNew.load_balance_strategy')}
>
<Select
options={['round_robin', 'none'].map(key => ({
label: t(`modelNew.${key}`),
value: key
}))}
placeholder={t('common.pleaseSelect')}
/>
</Form.Item>
<Form.Item name="api_key_ids"> <Form.Item name="api_key_ids">
<ModelImplement type={type} /> <ModelImplement type={type} />
</Form.Item> </Form.Item>

View File

@@ -35,12 +35,12 @@ const KeyConfigModal = forwardRef<KeyConfigModalRef, KeyConfigModalProps>(({
updateProviderApiKeys({ updateProviderApiKeys({
...values, ...values,
provider: model.provider provider: model.provider
}).then(() => { }).then((res) => {
if (refresh) { if (refresh) {
refresh(); refresh();
} }
handleClose() handleClose()
message.success(t('common.updateSuccess')) message.success(res as string)
}) })
.catch(() => { .catch(() => {
setLoading(false) setLoading(false)

View File

@@ -1,5 +1,5 @@
import { forwardRef, useImperativeHandle, useState } from 'react'; import { forwardRef, useImperativeHandle, useState, useMemo, useEffect } from 'react';
import { Form, Cascader, App } from 'antd'; import { Form, Cascader, App, type CascaderProps } from 'antd';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import type { SubModelModalForm, SubModelModalRef, SubModelModalProps, ModelList } from './types'; import type { SubModelModalForm, SubModelModalRef, SubModelModalProps, ModelList } from './types';
@@ -18,7 +18,8 @@ interface Option {
} }
const SubModelModal = forwardRef<SubModelModalRef, SubModelModalProps>(({ const SubModelModal = forwardRef<SubModelModalRef, SubModelModalProps>(({
refresh, refresh,
type type,
groupedByProvider
}, ref) => { }, ref) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { message } = App.useApp() const { message } = App.useApp()
@@ -26,28 +27,27 @@ const SubModelModal = forwardRef<SubModelModalRef, SubModelModalProps>(({
const [form] = Form.useForm<SubModelModalForm>(); const [form] = Form.useForm<SubModelModalForm>();
const [selecteds, setSelecteds] = useState<any[]>([]) const [selecteds, setSelecteds] = useState<any[]>([])
const [modelList, setModelList] = useState<Option[]>([]) const [modelList, setModelList] = useState<Option[]>([])
const provider = Form.useWatch(['provider'], form)
useEffect(() => {
if (provider && groupedByProvider) {
const lastModels = groupedByProvider[provider] || []
const list = lastModels.map(vo => [{ name: vo.model_name, id: vo.model_config_ids[0], value: vo.model_config_ids[0], provider }, { value: vo.id }])
setSelecteds(list)
form.setFieldValue('api_key_ids', lastModels.map(vo => [vo.model_config_ids[0], vo.id]))
}
}, [groupedByProvider, provider])
// 封装取消方法,添加关闭弹窗逻辑 // 封装取消方法,添加关闭弹窗逻辑
const handleClose = () => { const handleClose = () => {
form.resetFields(); form.resetFields();
setVisible(false); setVisible(false);
setSelecteds([]) setSelecteds([])
setModelList([])
}; };
const handleOpen = (list?: ModelList[], provider?: string) => { const handleOpen = () => {
if (list?.length && provider) { form.resetFields()
const initialValue: SubModelModalForm = {
provider,
api_key_ids: list.map(vo => {
return [vo.model_config_ids[0], vo.id]
})
}
form.setFieldsValue(initialValue);
handleChangeProvider(provider, initialValue.api_key_ids)
} else {
form.resetFields()
}
setVisible(true); setVisible(true);
}; };
// 封装保存方法,添加提交逻辑 // 封装保存方法,添加提交逻辑
@@ -67,7 +67,6 @@ const SubModelModal = forwardRef<SubModelModalRef, SubModelModalProps>(({
const handleChange = (value: (string | number)[][], selectedOptions: Option[][]) => { const handleChange = (value: (string | number)[][], selectedOptions: Option[][]) => {
const filterList = selectedOptions.filter(vo => vo.length === 1).map(item => item[0]) const filterList = selectedOptions.filter(vo => vo.length === 1).map(item => item[0])
const lastFilterLit = value.filter(vo => vo.length !== 1) const lastFilterLit = value.filter(vo => vo.length !== 1)
console.log('onchange', value, lastFilterLit, selectedOptions, filterList)
if (filterList.length) { if (filterList.length) {
message.warning(`${filterList.map(vo => vo.label)}${t('modelNew.selectOneTip')}`) message.warning(`${filterList.map(vo => vo.label)}${t('modelNew.selectOneTip')}`)
form.setFieldValue('api_key_ids', lastFilterLit) form.setFieldValue('api_key_ids', lastFilterLit)
@@ -77,35 +76,51 @@ const SubModelModal = forwardRef<SubModelModalRef, SubModelModalProps>(({
const handleChangeProvider = (provider: string, api_key_ids?: any[]) => { const handleChangeProvider = (provider: string, api_key_ids?: any[]) => {
form.setFieldValue('api_key_ids', undefined) form.setFieldValue('api_key_ids', undefined)
getModelNewList({ if (provider) {
provider: provider, getModelNewList({
is_composite: false, provider: provider,
is_active: true, is_composite: false,
type is_active: true,
}) type
.then(res => {
const response = res as ProviderModelItem[]
const list = response[0]?.models || []
setModelList(list.map(vo => {
const children = vo.api_keys.map(item => ({
label: item.api_key,
value: item.id,
}))
return {
...vo,
label: vo.name,
value: vo.id,
children: children
}
}))
if (api_key_ids?.length) {
form.setFieldsValue({
api_key_ids: api_key_ids
})
}
}) })
.then(res => {
const response = res as ProviderModelItem[]
const list = response[0]?.models || []
setModelList(list.map(vo => {
const children = vo.api_keys.map(item => ({
label: item.api_key,
value: item.id,
}))
return {
...vo,
label: vo.name,
value: vo.id,
children: children
}
}))
if (api_key_ids?.length) {
form.setFieldsValue({
api_key_ids: api_key_ids
})
}
})
} else {
setModelList([])
}
} }
const displayRender: CascaderProps<Option>['displayRender'] = (labels, selectedOptions = []) =>
labels.map((label, i) => {
const option = selectedOptions[i];
if (i === labels.length - 1) {
return (
<span key={option?.value || i}>
{label}
</span>
);
}
return <span key={option?.value || i}>{label} / </span>;
});
// 暴露给父组件的方法 // 暴露给父组件的方法
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
@@ -154,6 +169,7 @@ const SubModelModal = forwardRef<SubModelModalRef, SubModelModalProps>(({
className="rb:w-full!" className="rb:w-full!"
showCheckedStrategy={SHOW_CHILD} showCheckedStrategy={SHOW_CHILD}
changeOnSelect changeOnSelect
displayRender={displayRender}
/> />
</Form.Item> </Form.Item>
</Form> </Form>

View File

@@ -24,9 +24,6 @@ const ModelImplement: FC<ModelImplementProps> = ({ type, value, onChange }) => {
} }
subModelModalRef.current?.handleOpen() subModelModalRef.current?.handleOpen()
} }
const handleEdit = (list: ModelList[], provider: string ) => {
subModelModalRef.current?.handleOpen(list, provider)
}
const handleDelete = (provider: string) => { const handleDelete = (provider: string) => {
modal.confirm({ modal.confirm({
title: t('common.confirmDeleteDesc', { name: provider }), title: t('common.confirmDeleteDesc', { name: provider }),
@@ -78,16 +75,11 @@ const ModelImplement: FC<ModelImplementProps> = ({ type, value, onChange }) => {
<div key={provider} className="rb:mb-4 last:rb:mb-0"> <div key={provider} className="rb:mb-4 last:rb:mb-0">
<Flex justify="space-between" align="center" className="rb:mb-2 last:rb:mb-0"> <Flex justify="space-between" align="center" className="rb:mb-2 last:rb:mb-0">
<div className="rb:font-medium">{[...new Set(items?.map((vo) => vo.model_name))].join(', ')}</div> <div className="rb:font-medium">{[...new Set(items?.map((vo) => vo.model_name))].join(', ')}</div>
<Space>
<div <div
className="rb:w-6 rb:h-6 rb:cursor-pointer rb:bg-[url('@/assets/images/editBorder.svg')] rb:hover:bg-[url('@/assets/images/editBg.svg')]" className="rb:w-6 rb:h-6 rb:cursor-pointer rb:bg-[url('@/assets/images/deleteBorder.svg')] rb:hover:bg-[url('@/assets/images/deleteBg.svg')]"
onClick={() => handleEdit(items, provider)} onClick={() => handleDelete(provider)}
></div> ></div>
<div
className="rb:w-6 rb:h-6 rb:cursor-pointer rb:bg-[url('@/assets/images/deleteBorder.svg')] rb:hover:bg-[url('@/assets/images/deleteBg.svg')]"
onClick={() => handleDelete(provider)}
></div>
</Space>
</Flex> </Flex>
<Tag className="rb:mb-2">{t(`modelNew.${provider}`)}</Tag> <Tag className="rb:mb-2">{t(`modelNew.${provider}`)}</Tag>
</div> </div>
@@ -98,6 +90,7 @@ const ModelImplement: FC<ModelImplementProps> = ({ type, value, onChange }) => {
ref={subModelModalRef} ref={subModelModalRef}
refresh={handleRefresh} refresh={handleRefresh}
type={type} type={type}
groupedByProvider={groupedByProvider}
/> />
</div> </div>
) )

View File

@@ -8,9 +8,10 @@ export interface SubModelModalForm {
api_key_ids: string[][]; api_key_ids: string[][];
} }
export interface SubModelModalRef { export interface SubModelModalRef {
handleOpen: (list?: ModelList[], provider?: string) => void; handleOpen: () => void;
} }
export interface SubModelModalProps { export interface SubModelModalProps {
type?: string; type?: string;
refresh?: (vo: ModelList[]) => void; refresh?: (vo: ModelList[]) => void;
groupedByProvider?: Record<string, ModelList[]>
} }

View File

@@ -1,4 +1,4 @@
import { useState, useImperativeHandle, forwardRef, useRef } from 'react'; import { useState, useImperativeHandle, forwardRef, useRef, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Button, Switch, Row, Col, Space, Tooltip } from 'antd' import { Button, Switch, Row, Col, Space, Tooltip } from 'antd'
@@ -8,8 +8,9 @@ import RbCard from '@/components/RbCard/Card'
import Tag from '@/components/Tag'; import Tag from '@/components/Tag';
import PageEmpty from '@/components/Empty/PageEmpty'; import PageEmpty from '@/components/Empty/PageEmpty';
import MultiKeyConfigModal from './MultiKeyConfigModal' import MultiKeyConfigModal from './MultiKeyConfigModal'
import { getModelNewList, updateModelStatus } from '@/api/models' import { getModelNewList, updateModelStatus, modelTypeUrl } from '@/api/models'
import { getLogoUrl } from '../utils' import { getLogoUrl } from '../utils'
import CustomSelect from '@/components/CustomSelect'
interface ModelListDetailProps { interface ModelListDetailProps {
refresh?: () => void; refresh?: () => void;
@@ -22,8 +23,10 @@ const ModelListDetail = forwardRef<ModelListDetailRef, ModelListDetailProps>(({
const [list, setList] = useState<ModelListItem[]>([]) const [list, setList] = useState<ModelListItem[]>([])
const multiKeyConfigModalRef = useRef<MultiKeyConfigModalRef>(null) const multiKeyConfigModalRef = useRef<MultiKeyConfigModalRef>(null)
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [type, setType] = useState<string | undefined | null>(null)
const handleOpen = (vo: ProviderModelItem) => { const handleOpen = (vo: ProviderModelItem) => {
setType(null)
setOpen(true) setOpen(true)
getData(vo) getData(vo)
} }
@@ -53,27 +56,50 @@ const ModelListDetail = forwardRef<ModelListDetailRef, ModelListDetailProps>(({
} }
const handleClose = () => { const handleClose = () => {
setType(null)
setOpen(false) setOpen(false)
refresh?.() refresh?.()
} }
const handleRefresh = () => { const handleRefresh = () => {
getData(data) getData(data)
} }
const handleTypeChange = (value: string) => {
setType(value)
}
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
handleOpen, handleOpen,
})); }));
const filterList = useMemo(() => {
if (!type) return list
return list.filter(vo => vo.type === type)
}, [type, list])
return ( return (
<RbDrawer <RbDrawer
title={<>{t(`modelNew.${data.provider}`)} {t('modelNew.modelList')} ({list.length}{t('modelNew.item')})</>} title={<>{t(`modelNew.${data.provider}`)} {t('modelNew.modelList')} ({list.length}{t('modelNew.item')})</>}
open={open} open={open}
onClose={handleClose} onClose={handleClose}
> >
{list.length === 0 <Row gutter={16}>
<Col span={12}>
<CustomSelect
value={type}
url={modelTypeUrl}
hasAll={false}
format={(items) => items.map((item) => ({ label: t(`modelNew.${item}`), value: String(item) }))}
onChange={handleTypeChange}
className="rb:w-full"
allowClear={true}
placeholder={t('modelNew.type')}
/>
</Col>
</Row>
{filterList.length === 0
? <PageEmpty /> ? <PageEmpty />
: <div className="rb:grid rb:grid-cols-2 rb:gap-4"> : <div className="rb:grid rb:grid-cols-2 rb:gap-4 rb:mt-3">
{list.map(item => ( {filterList.map(item => (
<RbCard <RbCard
key={item.id} key={item.id}
title={item.name} title={item.name}

View File

@@ -1,9 +1,9 @@
import { useState, useRef, type FC } from 'react'; import { useState, useRef, type FC } from 'react';
import { Button, Flex, Space, type SegmentedProps } from 'antd' import { Button, Flex, Space, type SegmentedProps, Form } from 'antd'
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import GroupModelModal from './components/GroupModelModal' import GroupModelModal from './components/GroupModelModal'
import type { ModelListItem, GroupModelModalRef, CustomModelModalRef, ModelPlazaItem, BaseRef } from './types' import type { ModelListItem, GroupModelModalRef, CustomModelModalRef, ModelPlazaItem, BaseRef, Query } from './types'
import SearchInput from '@/components/SearchInput' import SearchInput from '@/components/SearchInput'
import PageTabs from '@/components/PageTabs' import PageTabs from '@/components/PageTabs'
import GroupModel from './Group' import GroupModel from './Group'
@@ -17,11 +17,12 @@ const tabKeys = ['group', 'list', 'square']
const ModelManagement: FC = () => { const ModelManagement: FC = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const [activeTab, setActiveTab] = useState('group'); const [activeTab, setActiveTab] = useState('group');
const [query, setQuery] = useState({})
const configModalRef = useRef<GroupModelModalRef>(null) const configModalRef = useRef<GroupModelModalRef>(null)
const customModelModalRef = useRef<CustomModelModalRef>(null) const customModelModalRef = useRef<CustomModelModalRef>(null)
const groupRef = useRef<BaseRef>(null) const groupRef = useRef<BaseRef>(null)
const squareRef = useRef<BaseRef>(null) const squareRef = useRef<BaseRef>(null)
const [form] = Form.useForm<Query>()
const query = Form.useWatch([], form)
const formatTabItems = () => { const formatTabItems = () => {
return tabKeys.map(value => ({ return tabKeys.map(value => ({
@@ -31,7 +32,7 @@ const ModelManagement: FC = () => {
} }
const handleChangeTab = (value: SegmentedProps['value']) => { const handleChangeTab = (value: SegmentedProps['value']) => {
setActiveTab(value as string); setActiveTab(value as string);
setQuery({}) form.resetFields()
} }
const handleEdit = (vo?: ModelListItem | ModelPlazaItem) => { const handleEdit = (vo?: ModelListItem | ModelPlazaItem) => {
@@ -54,15 +55,6 @@ const ModelManagement: FC = () => {
break break
} }
} }
const handleSearch = (value?: string) => {
setQuery({ search: value })
}
const handleTypeChange = (value: string) => {
setQuery(pre => ({ ...pre, type: value }))
}
const handleProviderChange = (value: string) => {
setQuery(pre => ({ ...pre, provider: value }))
}
return ( return (
<> <>
@@ -73,35 +65,44 @@ const ModelManagement: FC = () => {
onChange={handleChangeTab} onChange={handleChangeTab}
/> />
<Space size={12}> <Form form={form}>
{activeTab === 'list' ? <> <Space size={12}>
<CustomSelect {activeTab === 'list' &&
url={modelTypeUrl} <Form.Item name="type" noStyle>
hasAll={false} <CustomSelect
format={(items) => items.map((item) => ({ label: t(`modelNew.${item}`), value: String(item) }))} url={modelTypeUrl}
onChange={handleTypeChange} hasAll={false}
className="rb:w-30" format={(items) => items.map((item) => ({ label: t(`modelNew.${item}`), value: String(item) }))}
allowClear={true} className="rb:w-30"
placeholder={t('modelNew.type')} allowClear={true}
/> placeholder={t('modelNew.type')}
<CustomSelect />
url={modelProviderUrl} </Form.Item>
hasAll={false} }
format={(items) => items.map((item) => ({ label: t(`modelNew.${item}`), value: String(item) }))} {(activeTab === 'list' || activeTab === 'square') &&
onChange={handleProviderChange} <Form.Item name="provider" noStyle>
className="rb:w-30" <CustomSelect
allowClear={true} url={modelProviderUrl}
placeholder={t('modelNew.provider')} hasAll={false}
/> format={(items) => items.map((item) => ({ label: t(`modelNew.${item}`), value: String(item) }))}
</> className="rb:w-30"
: <SearchInput allowClear={true}
placeholder={t(`modelNew.${activeTab}SearchPlaceholder`)} placeholder={t('modelNew.provider')}
onSearch={handleSearch} />
className="rb:w-70!" </Form.Item>
/>} }
{activeTab === 'group' && <Button type="primary" onClick={() => handleEdit()}>+ {t('modelNew.createGroupModel')}</Button>} {activeTab !== 'list' &&
{activeTab === 'square' && <Button type="primary" onClick={() => handleEdit()}>+ {t('modelNew.createCustomModel')}</Button>} <Form.Item name="search" noStyle>
</Space> <SearchInput
placeholder={t(`modelNew.${activeTab}SearchPlaceholder`)}
className="rb:w-70!"
/>
</Form.Item>
}
{activeTab === 'group' && <Button type="primary" onClick={() => handleEdit()}>+ {t('modelNew.createGroupModel')}</Button>}
{activeTab === 'square' && <Button type="primary" onClick={() => handleEdit()}>+ {t('modelNew.createCustomModel')}</Button>}
</Space>
</Form>
</Flex> </Flex>
<div className="rb:w-full rb:h-[calc(100%-48px)] rb:my-4"> <div className="rb:w-full rb:h-[calc(100%-48px)] rb:my-4">

View File

@@ -105,7 +105,7 @@ export const nodeLibrary: NodeLibrary[] = [
model_id: { model_id: {
type: 'customSelect', type: 'customSelect',
url: getModelListUrl, url: getModelListUrl,
params: { type: 'llm,chat', is_active: true }, // llm/chat params: { type: 'llm,chat', pagesize: 100, is_active: true }, // llm/chat
valueKey: 'id', valueKey: 'id',
labelKey: 'name', labelKey: 'name',
}, },
@@ -166,7 +166,7 @@ export const nodeLibrary: NodeLibrary[] = [
model_id: { model_id: {
type: 'customSelect', type: 'customSelect',
url: getModelListUrl, url: getModelListUrl,
params: { type: 'llm,chat', is_active: true }, // llm/chat params: { type: 'llm,chat', pagesize: 100, is_active: true }, // llm/chat
valueKey: 'id', valueKey: 'id',
labelKey: 'name', labelKey: 'name',
}, },
@@ -259,7 +259,7 @@ export const nodeLibrary: NodeLibrary[] = [
model_id: { model_id: {
type: 'customSelect', type: 'customSelect',
url: getModelListUrl, url: getModelListUrl,
params: { type: 'llm,chat', is_active: true }, // llm/chat params: { type: 'llm,chat', pagesize: 100, is_active: true }, // llm/chat
valueKey: 'id', valueKey: 'id',
labelKey: 'name', labelKey: 'name',
}, },