update: 更新API配置,支持手动输入模型
This commit is contained in:
@@ -356,6 +356,11 @@ async def get_available_models(
|
|||||||
|
|
||||||
except httpx.HTTPStatusError as e:
|
except httpx.HTTPStatusError as e:
|
||||||
logger.error(f"获取模型列表失败 (HTTP {e.response.status_code}): {e.response.text}")
|
logger.error(f"获取模型列表失败 (HTTP {e.response.status_code}): {e.response.text}")
|
||||||
|
if e.response.status_code == 404:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=f"该 API 提供商不支持模型列表查询接口 (/models 返回 404),请手动输入模型名称。当前请求地址: {api_base_url.rstrip('/')}/models"
|
||||||
|
)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
detail=f"无法从 API 获取模型列表 (HTTP {e.response.status_code})"
|
detail=f"无法从 API 获取模型列表 (HTTP {e.response.status_code})"
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export default function SettingsPage() {
|
|||||||
const [modelOptions, setModelOptions] = useState<Array<{ value: string; label: string; description: string }>>([]);
|
const [modelOptions, setModelOptions] = useState<Array<{ value: string; label: string; description: string }>>([]);
|
||||||
const [fetchingModels, setFetchingModels] = useState(false);
|
const [fetchingModels, setFetchingModels] = useState(false);
|
||||||
const [modelsFetched, setModelsFetched] = useState(false);
|
const [modelsFetched, setModelsFetched] = useState(false);
|
||||||
|
const [modelSearchText, setModelSearchText] = useState('');
|
||||||
const [testingApi, setTestingApi] = useState(false);
|
const [testingApi, setTestingApi] = useState(false);
|
||||||
const [testResult, setTestResult] = useState<{
|
const [testResult, setTestResult] = useState<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
@@ -48,6 +49,7 @@ export default function SettingsPage() {
|
|||||||
const [presetModelOptions, setPresetModelOptions] = useState<Array<{ value: string; label: string; description: string }>>([]);
|
const [presetModelOptions, setPresetModelOptions] = useState<Array<{ value: string; label: string; description: string }>>([]);
|
||||||
const [fetchingPresetModels, setFetchingPresetModels] = useState(false);
|
const [fetchingPresetModels, setFetchingPresetModels] = useState(false);
|
||||||
const [presetModelsFetched, setPresetModelsFetched] = useState(false);
|
const [presetModelsFetched, setPresetModelsFetched] = useState(false);
|
||||||
|
const [presetModelSearchText, setPresetModelSearchText] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadSettings();
|
loadSettings();
|
||||||
@@ -430,6 +432,7 @@ export default function SettingsPage() {
|
|||||||
// 清除预设模型列表状态
|
// 清除预设模型列表状态
|
||||||
setPresetModelOptions([]);
|
setPresetModelOptions([]);
|
||||||
setPresetModelsFetched(false);
|
setPresetModelsFetched(false);
|
||||||
|
setPresetModelSearchText('');
|
||||||
};
|
};
|
||||||
|
|
||||||
// 预设编辑窗口:获取模型列表
|
// 预设编辑窗口:获取模型列表
|
||||||
@@ -1100,14 +1103,19 @@ export default function SettingsPage() {
|
|||||||
<Select
|
<Select
|
||||||
size={isMobile ? 'middle' : 'large'}
|
size={isMobile ? 'middle' : 'large'}
|
||||||
showSearch
|
showSearch
|
||||||
placeholder={isMobile ? "选择模型" : "输入模型名称或点击获取"}
|
placeholder={isMobile ? "输入或选择模型" : "输入模型名称或点击获取"}
|
||||||
optionFilterProp="label"
|
optionFilterProp="label"
|
||||||
loading={fetchingModels}
|
loading={fetchingModels}
|
||||||
onFocus={handleModelSelectFocus}
|
onFocus={handleModelSelectFocus}
|
||||||
filterOption={(input, option) =>
|
onSearch={(value) => setModelSearchText(value)}
|
||||||
(option?.label ?? '').toLowerCase().includes(input.toLowerCase()) ||
|
onSelect={() => setModelSearchText('')}
|
||||||
(option?.description ?? '').toLowerCase().includes(input.toLowerCase())
|
onBlur={() => setModelSearchText('')}
|
||||||
}
|
filterOption={(input, option) => {
|
||||||
|
// 手动输入的选项始终显示
|
||||||
|
if (option?.value === input && !modelOptions.some(m => m.value === input)) return true;
|
||||||
|
return (option?.label ?? '').toLowerCase().includes(input.toLowerCase()) ||
|
||||||
|
(option?.description ?? '').toLowerCase().includes(input.toLowerCase());
|
||||||
|
}}
|
||||||
dropdownRender={(menu) => (
|
dropdownRender={(menu) => (
|
||||||
<>
|
<>
|
||||||
{menu}
|
{menu}
|
||||||
@@ -1116,14 +1124,14 @@ export default function SettingsPage() {
|
|||||||
<Spin size="small" /> 正在获取模型列表...
|
<Spin size="small" /> 正在获取模型列表...
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!fetchingModels && modelOptions.length === 0 && modelsFetched && (
|
{!fetchingModels && modelOptions.length === 0 && modelsFetched && !modelSearchText && (
|
||||||
<div style={{ padding: '8px 12px', color: '#ff4d4f', textAlign: 'center', fontSize: isMobile ? '12px' : '14px' }}>
|
<div style={{ padding: '8px 12px', color: '#ff4d4f', textAlign: 'center', fontSize: isMobile ? '12px' : '14px' }}>
|
||||||
未能获取到模型列表,请检查 API 配置
|
未能获取到模型列表,可直接输入模型名称
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!fetchingModels && modelOptions.length === 0 && !modelsFetched && (
|
{!fetchingModels && modelOptions.length === 0 && !modelsFetched && !modelSearchText && (
|
||||||
<div style={{ padding: '8px 12px', color: 'var(--color-text-secondary)', textAlign: 'center', fontSize: isMobile ? '12px' : '14px' }}>
|
<div style={{ padding: '8px 12px', color: 'var(--color-text-secondary)', textAlign: 'center', fontSize: isMobile ? '12px' : '14px' }}>
|
||||||
点击输入框自动获取模型列表
|
点击输入框自动获取,或直接输入模型名称
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
@@ -1133,11 +1141,7 @@ export default function SettingsPage() {
|
|||||||
<div style={{ padding: '8px 12px', textAlign: 'center', fontSize: isMobile ? '12px' : '14px' }}>
|
<div style={{ padding: '8px 12px', textAlign: 'center', fontSize: isMobile ? '12px' : '14px' }}>
|
||||||
<Spin size="small" /> 加载中...
|
<Spin size="small" /> 加载中...
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : null
|
||||||
<div style={{ padding: '8px 12px', color: 'var(--color-text-secondary)', textAlign: 'center', fontSize: isMobile ? '12px' : '14px' }}>
|
|
||||||
未找到匹配的模型
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
suffixIcon={
|
suffixIcon={
|
||||||
!isMobile ? (
|
!isMobile ? (
|
||||||
@@ -1171,15 +1175,36 @@ export default function SettingsPage() {
|
|||||||
</div>
|
</div>
|
||||||
) : undefined
|
) : undefined
|
||||||
}
|
}
|
||||||
options={modelOptions.map(model => ({
|
options={(() => {
|
||||||
value: model.value,
|
const opts = modelOptions.map(model => ({
|
||||||
label: model.label,
|
value: model.value,
|
||||||
description: model.description
|
label: model.label,
|
||||||
}))}
|
description: model.description
|
||||||
|
}));
|
||||||
|
// 如果用户输入了文本且不在已有选项中,添加手动输入选项
|
||||||
|
if (modelSearchText && !modelOptions.some(m =>
|
||||||
|
m.value.toLowerCase() === modelSearchText.toLowerCase() ||
|
||||||
|
m.label.toLowerCase() === modelSearchText.toLowerCase()
|
||||||
|
)) {
|
||||||
|
opts.unshift({
|
||||||
|
value: modelSearchText,
|
||||||
|
label: modelSearchText,
|
||||||
|
description: '手动输入的模型名称'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return opts;
|
||||||
|
})()}
|
||||||
optionRender={(option) => (
|
optionRender={(option) => (
|
||||||
<div>
|
<div>
|
||||||
<div style={{ fontWeight: 500, fontSize: isMobile ? '13px' : '14px' }}>{option.data.label}</div>
|
<div style={{ fontWeight: 500, fontSize: isMobile ? '13px' : '14px' }}>
|
||||||
{option.data.description && (
|
{option.data.description === '手动输入的模型名称' ? (
|
||||||
|
<Space size={4}>
|
||||||
|
<EditOutlined style={{ color: 'var(--color-primary)' }} />
|
||||||
|
<span>使用 "{option.data.label}"</span>
|
||||||
|
</Space>
|
||||||
|
) : option.data.label}
|
||||||
|
</div>
|
||||||
|
{option.data.description && option.data.description !== '手动输入的模型名称' && (
|
||||||
<div style={{ fontSize: isMobile ? '11px' : '12px', color: '#8c8c8c', marginTop: '2px' }}>
|
<div style={{ fontSize: isMobile ? '11px' : '12px', color: '#8c8c8c', marginTop: '2px' }}>
|
||||||
{option.data.description}
|
{option.data.description}
|
||||||
</div>
|
</div>
|
||||||
@@ -1594,14 +1619,19 @@ export default function SettingsPage() {
|
|||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
showSearch
|
showSearch
|
||||||
placeholder="点击获取模型列表或直接输入"
|
placeholder="输入模型名称或点击获取"
|
||||||
optionFilterProp="label"
|
optionFilterProp="label"
|
||||||
loading={fetchingPresetModels}
|
loading={fetchingPresetModels}
|
||||||
onFocus={handlePresetModelSelectFocus}
|
onFocus={handlePresetModelSelectFocus}
|
||||||
filterOption={(input, option) =>
|
onSearch={(value) => setPresetModelSearchText(value)}
|
||||||
(option?.label ?? '').toLowerCase().includes(input.toLowerCase()) ||
|
onSelect={() => setPresetModelSearchText('')}
|
||||||
(option?.description ?? '').toLowerCase().includes(input.toLowerCase())
|
onBlur={() => setPresetModelSearchText('')}
|
||||||
}
|
filterOption={(input, option) => {
|
||||||
|
// 手动输入的选项始终显示
|
||||||
|
if (option?.value === input && !presetModelOptions.some(m => m.value === input)) return true;
|
||||||
|
return (option?.label ?? '').toLowerCase().includes(input.toLowerCase()) ||
|
||||||
|
(option?.description ?? '').toLowerCase().includes(input.toLowerCase());
|
||||||
|
}}
|
||||||
dropdownRender={(menu) => (
|
dropdownRender={(menu) => (
|
||||||
<>
|
<>
|
||||||
{menu}
|
{menu}
|
||||||
@@ -1610,14 +1640,14 @@ export default function SettingsPage() {
|
|||||||
<Spin size="small" /> 正在获取模型列表...
|
<Spin size="small" /> 正在获取模型列表...
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!fetchingPresetModels && presetModelOptions.length === 0 && presetModelsFetched && (
|
{!fetchingPresetModels && presetModelOptions.length === 0 && presetModelsFetched && !presetModelSearchText && (
|
||||||
<div style={{ padding: '8px 12px', color: '#ff4d4f', textAlign: 'center', fontSize: '12px' }}>
|
<div style={{ padding: '8px 12px', color: '#ff4d4f', textAlign: 'center', fontSize: '12px' }}>
|
||||||
未能获取到模型列表,请检查 API 配置
|
未能获取到模型列表,可直接输入模型名称
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!fetchingPresetModels && presetModelOptions.length === 0 && !presetModelsFetched && (
|
{!fetchingPresetModels && presetModelOptions.length === 0 && !presetModelsFetched && !presetModelSearchText && (
|
||||||
<div style={{ padding: '8px 12px', color: 'var(--color-text-secondary)', textAlign: 'center', fontSize: '12px' }}>
|
<div style={{ padding: '8px 12px', color: 'var(--color-text-secondary)', textAlign: 'center', fontSize: '12px' }}>
|
||||||
点击输入框自动获取模型列表
|
点击输入框自动获取,或直接输入模型名称
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
@@ -1627,11 +1657,7 @@ export default function SettingsPage() {
|
|||||||
<div style={{ padding: '8px 12px', textAlign: 'center', fontSize: '12px' }}>
|
<div style={{ padding: '8px 12px', textAlign: 'center', fontSize: '12px' }}>
|
||||||
<Spin size="small" /> 加载中...
|
<Spin size="small" /> 加载中...
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : null
|
||||||
<div style={{ padding: '8px 12px', color: 'var(--color-text-secondary)', textAlign: 'center', fontSize: '12px' }}>
|
|
||||||
未找到匹配的模型
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
suffixIcon={
|
suffixIcon={
|
||||||
<div
|
<div
|
||||||
@@ -1663,15 +1689,36 @@ export default function SettingsPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
options={presetModelOptions.map(model => ({
|
options={(() => {
|
||||||
value: model.value,
|
const opts = presetModelOptions.map(model => ({
|
||||||
label: model.label,
|
value: model.value,
|
||||||
description: model.description
|
label: model.label,
|
||||||
}))}
|
description: model.description
|
||||||
|
}));
|
||||||
|
// 如果用户输入了文本且不在已有选项中,添加手动输入选项
|
||||||
|
if (presetModelSearchText && !presetModelOptions.some(m =>
|
||||||
|
m.value.toLowerCase() === presetModelSearchText.toLowerCase() ||
|
||||||
|
m.label.toLowerCase() === presetModelSearchText.toLowerCase()
|
||||||
|
)) {
|
||||||
|
opts.unshift({
|
||||||
|
value: presetModelSearchText,
|
||||||
|
label: presetModelSearchText,
|
||||||
|
description: '手动输入的模型名称'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return opts;
|
||||||
|
})()}
|
||||||
optionRender={(option) => (
|
optionRender={(option) => (
|
||||||
<div>
|
<div>
|
||||||
<div style={{ fontWeight: 500, fontSize: '13px' }}>{option.data.label}</div>
|
<div style={{ fontWeight: 500, fontSize: '13px' }}>
|
||||||
{option.data.description && (
|
{option.data.description === '手动输入的模型名称' ? (
|
||||||
|
<Space size={4}>
|
||||||
|
<EditOutlined style={{ color: 'var(--color-primary)' }} />
|
||||||
|
<span>使用 "{option.data.label}"</span>
|
||||||
|
</Space>
|
||||||
|
) : option.data.label}
|
||||||
|
</div>
|
||||||
|
{option.data.description && option.data.description !== '手动输入的模型名称' && (
|
||||||
<div style={{ fontSize: '11px', color: '#8c8c8c', marginTop: '2px' }}>
|
<div style={{ fontSize: '11px', color: '#8c8c8c', marginTop: '2px' }}>
|
||||||
{option.data.description}
|
{option.data.description}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user