Merge pull request #233 from SuanmoSuanyangTechnology/feature/model_zy

fix(web): model bugfix
This commit is contained in:
yingzhao
2026-01-29 12:11:17 +08:00
committed by GitHub
11 changed files with 173 additions and 114 deletions

View File

@@ -546,7 +546,10 @@ export const en = {
tags: 'Tags',
createCustomModel: 'Add Custom Model',
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_base: 'API Base URL',

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
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 type { ModelListItem, CompositeModelForm, GroupModelModalRef, GroupModelModalProps, ModelApiKey } from '../types';
@@ -106,6 +106,7 @@ const GroupModelModal = forwardRef<GroupModelModalRef, GroupModelModalProps>(({
<Form
form={form}
layout="vertical"
initialValues={{ balance_strategy: 'none' }}
>
<Form.Item
name="logo"
@@ -147,6 +148,19 @@ const GroupModelModal = forwardRef<GroupModelModalRef, GroupModelModalProps>(({
<Input.TextArea placeholder={t('common.pleaseEnter')} />
</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">
<ModelImplement type={type} />
</Form.Item>

View File

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

View File

@@ -1,5 +1,5 @@
import { forwardRef, useImperativeHandle, useState } from 'react';
import { Form, Cascader, App } from 'antd';
import { forwardRef, useImperativeHandle, useState, useMemo, useEffect } from 'react';
import { Form, Cascader, App, type CascaderProps } from 'antd';
import { useTranslation } from 'react-i18next';
import type { SubModelModalForm, SubModelModalRef, SubModelModalProps, ModelList } from './types';
@@ -18,7 +18,8 @@ interface Option {
}
const SubModelModal = forwardRef<SubModelModalRef, SubModelModalProps>(({
refresh,
type
type,
groupedByProvider
}, ref) => {
const { t } = useTranslation();
const { message } = App.useApp()
@@ -26,28 +27,27 @@ const SubModelModal = forwardRef<SubModelModalRef, SubModelModalProps>(({
const [form] = Form.useForm<SubModelModalForm>();
const [selecteds, setSelecteds] = useState<any[]>([])
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 = () => {
form.resetFields();
setVisible(false);
setSelecteds([])
setModelList([])
};
const handleOpen = (list?: ModelList[], provider?: string) => {
if (list?.length && provider) {
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()
}
const handleOpen = () => {
form.resetFields()
setVisible(true);
};
// 封装保存方法,添加提交逻辑
@@ -67,7 +67,6 @@ const SubModelModal = forwardRef<SubModelModalRef, SubModelModalProps>(({
const handleChange = (value: (string | number)[][], selectedOptions: Option[][]) => {
const filterList = selectedOptions.filter(vo => vo.length === 1).map(item => item[0])
const lastFilterLit = value.filter(vo => vo.length !== 1)
console.log('onchange', value, lastFilterLit, selectedOptions, filterList)
if (filterList.length) {
message.warning(`${filterList.map(vo => vo.label)}${t('modelNew.selectOneTip')}`)
form.setFieldValue('api_key_ids', lastFilterLit)
@@ -77,35 +76,51 @@ const SubModelModal = forwardRef<SubModelModalRef, SubModelModalProps>(({
const handleChangeProvider = (provider: string, api_key_ids?: any[]) => {
form.setFieldValue('api_key_ids', undefined)
getModelNewList({
provider: provider,
is_composite: false,
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
})
}
if (provider) {
getModelNewList({
provider: provider,
is_composite: false,
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
})
}
})
} 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, () => ({
@@ -154,6 +169,7 @@ const SubModelModal = forwardRef<SubModelModalRef, SubModelModalProps>(({
className="rb:w-full!"
showCheckedStrategy={SHOW_CHILD}
changeOnSelect
displayRender={displayRender}
/>
</Form.Item>
</Form>

View File

@@ -24,9 +24,6 @@ const ModelImplement: FC<ModelImplementProps> = ({ type, value, onChange }) => {
}
subModelModalRef.current?.handleOpen()
}
const handleEdit = (list: ModelList[], provider: string ) => {
subModelModalRef.current?.handleOpen(list, provider)
}
const handleDelete = (provider: string) => {
modal.confirm({
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">
<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>
<Space>
<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')]"
onClick={() => handleEdit(items, provider)}
></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>
<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>
</Flex>
<Tag className="rb:mb-2">{t(`modelNew.${provider}`)}</Tag>
</div>
@@ -98,6 +90,7 @@ const ModelImplement: FC<ModelImplementProps> = ({ type, value, onChange }) => {
ref={subModelModalRef}
refresh={handleRefresh}
type={type}
groupedByProvider={groupedByProvider}
/>
</div>
)

View File

@@ -8,9 +8,10 @@ export interface SubModelModalForm {
api_key_ids: string[][];
}
export interface SubModelModalRef {
handleOpen: (list?: ModelList[], provider?: string) => void;
handleOpen: () => void;
}
export interface SubModelModalProps {
type?: string;
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 { Button, Switch, Row, Col, Space, Tooltip } from 'antd'
@@ -8,8 +8,9 @@ import RbCard from '@/components/RbCard/Card'
import Tag from '@/components/Tag';
import PageEmpty from '@/components/Empty/PageEmpty';
import MultiKeyConfigModal from './MultiKeyConfigModal'
import { getModelNewList, updateModelStatus } from '@/api/models'
import { getModelNewList, updateModelStatus, modelTypeUrl } from '@/api/models'
import { getLogoUrl } from '../utils'
import CustomSelect from '@/components/CustomSelect'
interface ModelListDetailProps {
refresh?: () => void;
@@ -22,8 +23,10 @@ const ModelListDetail = forwardRef<ModelListDetailRef, ModelListDetailProps>(({
const [list, setList] = useState<ModelListItem[]>([])
const multiKeyConfigModalRef = useRef<MultiKeyConfigModalRef>(null)
const [loading, setLoading] = useState(false)
const [type, setType] = useState<string | undefined | null>(null)
const handleOpen = (vo: ProviderModelItem) => {
setType(null)
setOpen(true)
getData(vo)
}
@@ -53,27 +56,50 @@ const ModelListDetail = forwardRef<ModelListDetailRef, ModelListDetailProps>(({
}
const handleClose = () => {
setType(null)
setOpen(false)
refresh?.()
}
const handleRefresh = () => {
getData(data)
}
const handleTypeChange = (value: string) => {
setType(value)
}
useImperativeHandle(ref, () => ({
handleOpen,
}));
const filterList = useMemo(() => {
if (!type) return list
return list.filter(vo => vo.type === type)
}, [type, list])
return (
<RbDrawer
title={<>{t(`modelNew.${data.provider}`)} {t('modelNew.modelList')} ({list.length}{t('modelNew.item')})</>}
open={open}
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 />
: <div className="rb:grid rb:grid-cols-2 rb:gap-4">
{list.map(item => (
: <div className="rb:grid rb:grid-cols-2 rb:gap-4 rb:mt-3">
{filterList.map(item => (
<RbCard
key={item.id}
title={item.name}

View File

@@ -1,9 +1,9 @@
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 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 PageTabs from '@/components/PageTabs'
import GroupModel from './Group'
@@ -17,11 +17,12 @@ const tabKeys = ['group', 'list', 'square']
const ModelManagement: FC = () => {
const { t } = useTranslation();
const [activeTab, setActiveTab] = useState('group');
const [query, setQuery] = useState({})
const configModalRef = useRef<GroupModelModalRef>(null)
const customModelModalRef = useRef<CustomModelModalRef>(null)
const groupRef = useRef<BaseRef>(null)
const squareRef = useRef<BaseRef>(null)
const [form] = Form.useForm<Query>()
const query = Form.useWatch([], form)
const formatTabItems = () => {
return tabKeys.map(value => ({
@@ -31,7 +32,7 @@ const ModelManagement: FC = () => {
}
const handleChangeTab = (value: SegmentedProps['value']) => {
setActiveTab(value as string);
setQuery({})
form.resetFields()
}
const handleEdit = (vo?: ModelListItem | ModelPlazaItem) => {
@@ -54,15 +55,6 @@ const ModelManagement: FC = () => {
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 (
<>
@@ -73,35 +65,44 @@ const ModelManagement: FC = () => {
onChange={handleChangeTab}
/>
<Space size={12}>
{activeTab === 'list' ? <>
<CustomSelect
url={modelTypeUrl}
hasAll={false}
format={(items) => items.map((item) => ({ label: t(`modelNew.${item}`), value: String(item) }))}
onChange={handleTypeChange}
className="rb:w-30"
allowClear={true}
placeholder={t('modelNew.type')}
/>
<CustomSelect
url={modelProviderUrl}
hasAll={false}
format={(items) => items.map((item) => ({ label: t(`modelNew.${item}`), value: String(item) }))}
onChange={handleProviderChange}
className="rb:w-30"
allowClear={true}
placeholder={t('modelNew.provider')}
/>
</>
: <SearchInput
placeholder={t(`modelNew.${activeTab}SearchPlaceholder`)}
onSearch={handleSearch}
className="rb:w-70!"
/>}
{activeTab === 'group' && <Button type="primary" onClick={() => handleEdit()}>+ {t('modelNew.createGroupModel')}</Button>}
{activeTab === 'square' && <Button type="primary" onClick={() => handleEdit()}>+ {t('modelNew.createCustomModel')}</Button>}
</Space>
<Form form={form}>
<Space size={12}>
{activeTab === 'list' &&
<Form.Item name="type" noStyle>
<CustomSelect
url={modelTypeUrl}
hasAll={false}
format={(items) => items.map((item) => ({ label: t(`modelNew.${item}`), value: String(item) }))}
className="rb:w-30"
allowClear={true}
placeholder={t('modelNew.type')}
/>
</Form.Item>
}
{(activeTab === 'list' || activeTab === 'square') &&
<Form.Item name="provider" noStyle>
<CustomSelect
url={modelProviderUrl}
hasAll={false}
format={(items) => items.map((item) => ({ label: t(`modelNew.${item}`), value: String(item) }))}
className="rb:w-30"
allowClear={true}
placeholder={t('modelNew.provider')}
/>
</Form.Item>
}
{activeTab !== 'list' &&
<Form.Item name="search" noStyle>
<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>
<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: {
type: 'customSelect',
url: getModelListUrl,
params: { type: 'llm,chat', is_active: true }, // llm/chat
params: { type: 'llm,chat', pagesize: 100, is_active: true }, // llm/chat
valueKey: 'id',
labelKey: 'name',
},
@@ -166,7 +166,7 @@ export const nodeLibrary: NodeLibrary[] = [
model_id: {
type: 'customSelect',
url: getModelListUrl,
params: { type: 'llm,chat', is_active: true }, // llm/chat
params: { type: 'llm,chat', pagesize: 100, is_active: true }, // llm/chat
valueKey: 'id',
labelKey: 'name',
},
@@ -259,7 +259,7 @@ export const nodeLibrary: NodeLibrary[] = [
model_id: {
type: 'customSelect',
url: getModelListUrl,
params: { type: 'llm,chat', is_active: true }, // llm/chat
params: { type: 'llm,chat', pagesize: 100, is_active: true }, // llm/chat
valueKey: 'id',
labelKey: 'name',
},