From 2a99db29080251d1d802b437f4a0cb3df803cb3d Mon Sep 17 00:00:00 2001 From: lzt404 <2596933790@qq.com> Date: Sun, 13 Sep 2026 14:45:10 +0800 Subject: [PATCH] feat(api-access): unify DeepSeek model discovery and selection - require explicit model discovery before saving DeepSeek connections - preserve selected models when refreshing the catalog - mark model lists stale when connection details change - add model refresh support to easy mode - apply built-in DeepSeek reasoning levels consistently --- src/i18n/ja.ts | 7 +- src/i18n/locales/en.ts | 7 +- src/i18n/locales/zh-CN.ts | 7 +- src/pages/ApiAccessPage.tsx | 109 +++++++++++++++++++++----------- src/pages/EasyModePage.tsx | 73 +++++++++++++++++---- src/services/modelService.ts | 50 +++++++++++++++ src/styles.css | 10 +++ tests/modelService.test.ts | 60 ++++++++++++++++++ tests/operationFeedback.test.ts | 15 +++++ tests/providerConfig.test.ts | 17 +---- 10 files changed, 287 insertions(+), 68 deletions(-) create mode 100644 tests/modelService.test.ts diff --git a/src/i18n/ja.ts b/src/i18n/ja.ts index 6582355..7e3913e 100644 --- a/src/i18n/ja.ts +++ b/src/i18n/ja.ts @@ -26,6 +26,7 @@ export const jaOverrides = { 'easyMode.api.fetchingModels': 'モデル一覧を取得中…', 'easyMode.api.modelListTitle': 'モデルを選択', 'easyMode.api.modelListHint': '接続するモデルを選択してください', + 'easyMode.api.modelListStale': '接続情報が変更されました。保存する前にモデル一覧を更新してください', 'easyMode.api.credentialsRequired': '先に API の URL と API キーを入力してください', 'easyMode.api.modelRequired': 'モデル一覧を取得し、モデルを選択してください', 'easyMode.api.testSuccess': '接続に成功しました。{count} 件のモデルが見つかりました', @@ -1126,6 +1127,7 @@ export const jaOverrides = { 'apiAccess.error.requiredKey': 'API キーを入力してください', 'apiAccess.error.baseRequired': '{provider} 接続には Base URL が必要です', 'apiAccess.error.noModels': '公開可能なモデルが見つかりません。Base URL と API キーを確認してください', + 'apiAccess.error.fetchModelsBeforeSave': 'モデル一覧を正常に取得し、少なくとも 1 件選択してください', 'apiAccess.error.stale': '接続設定が別の操作で変更されました。更新してから再試行してください', 'apiAccess.error.duplicate': '同じ接続設定がすでに存在します', 'apiAccess.error.remarkInvalid': 'メモは 80 文字以内で、改行や制御文字を含めないでください', @@ -1186,6 +1188,9 @@ export const jaOverrides = { 'apiAccess.error.noAvailableModels': '利用可能なモデルが見つかりません', 'apiAccess.error.baseBeforeModels': 'モデルを取得する前に Base URL を入力してください', 'apiAccess.models.selected': '{count} 件のモデルを選択済み', + 'apiAccess.models.selectionRequired': 'モデルが選択されていません', + 'apiAccess.models.selectionRequiredHint': 'モデル一覧を取得し、少なくとも 1 件選択してください', + 'apiAccess.models.staleHint': '接続情報が変更されました。モデル一覧を再取得してください', 'apiAccess.models.restricted': 'モデル制限を設定済み', 'apiAccess.models.autoAll': '保存時にすべてのモデルを既定で公開', 'apiAccess.models.upstreamDefault': '上流の既定モデルを使用', @@ -1201,7 +1206,7 @@ export const jaOverrides = { 'apiAccess.baseRequiredPlaceholder': '必須、例:https://api.example.com', 'apiAccess.baseOptionalPlaceholder': '任意。空欄の場合は既定のアドレスを使用', 'apiAccess.preset.title': 'OpenAI 互換プリセット', - 'apiAccess.preset.description': 'DeepSeek 公式アドレスを入力済みです。保存時にすべてのモデルを自動検出して公開します', + 'apiAccess.preset.description': 'DeepSeek 公式アドレスを入力済みです。モデル一覧を取得し、公開するモデルを選択してください', 'apiAccess.thinking.builtIn': '組み込み推論レベル', 'apiAccess.thinking.builtInDescription': '現在公開中のすべてのモデルに自動適用します', 'apiAccess.thinking.title': '推論レベル', diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index cbe6e00..2028842 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -244,6 +244,7 @@ export const en: Record = { 'easyMode.api.fetchingModels': 'Fetching model list...', 'easyMode.api.modelListTitle': 'Select models', 'easyMode.api.modelListHint': 'Select the models to connect', + 'easyMode.api.modelListStale': 'Connection details changed. Refresh the model list before saving.', 'easyMode.api.credentialsRequired': 'Enter an API URL and API key first', 'easyMode.api.modelRequired': 'Fetch and select a model', 'easyMode.api.testSuccess': 'Connection successful! Found {count} models', @@ -1126,6 +1127,7 @@ export const en: Record = { 'apiAccess.error.requiredKey': 'API key is required', 'apiAccess.error.baseRequired': 'A Base URL is required for {provider}', 'apiAccess.error.noModels': 'No models were discovered. Check the Base URL and API key.', + 'apiAccess.error.fetchModelsBeforeSave': 'Fetch the model list successfully and select at least one model', 'apiAccess.error.stale': 'The connection configuration changed elsewhere. Refresh and try again.', 'apiAccess.error.duplicate': 'An identical connection already exists', 'apiAccess.error.remarkInvalid': 'The remark cannot exceed 80 characters or contain line breaks or control characters', @@ -1186,6 +1188,9 @@ export const en: Record = { 'apiAccess.error.noAvailableModels': 'No available models found', 'apiAccess.error.baseBeforeModels': 'Enter a Base URL before fetching models', 'apiAccess.models.selected': '{count} models selected', + 'apiAccess.models.selectionRequired': 'No models selected', + 'apiAccess.models.selectionRequiredHint': 'Fetch the model list and select at least one model', + 'apiAccess.models.staleHint': 'Connection details changed. Fetch the model list again.', 'apiAccess.models.restricted': 'Model restrictions configured', 'apiAccess.models.autoAll': 'All models will be opened by default when saved', 'apiAccess.models.upstreamDefault': 'Using upstream default models', @@ -1201,7 +1206,7 @@ export const en: Record = { 'apiAccess.baseRequiredPlaceholder': 'Required, for example https://api.example.com', 'apiAccess.baseOptionalPlaceholder': 'Optional; leave blank to use the default address', 'apiAccess.preset.title': 'OpenAI-Compatible Preset', - 'apiAccess.preset.description': 'The official DeepSeek address is prefilled; all discovered models are opened when saved', + 'apiAccess.preset.description': 'The official DeepSeek address is prefilled; fetch the list and select the models to open', 'apiAccess.thinking.builtIn': 'Built-in Reasoning Levels', 'apiAccess.thinking.builtInDescription': 'Applied automatically to all currently open models', 'apiAccess.thinking.title': 'Reasoning Levels', diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts index 778e0c4..101be27 100644 --- a/src/i18n/locales/zh-CN.ts +++ b/src/i18n/locales/zh-CN.ts @@ -243,6 +243,7 @@ export const zhCN = { 'easyMode.api.fetchingModels': '正在获取模型列表...', 'easyMode.api.modelListTitle': '选择模型', 'easyMode.api.modelListHint': '勾选需要接入的模型', + 'easyMode.api.modelListStale': '连接信息已变化,请刷新模型列表后再保存', 'easyMode.api.credentialsRequired': '请先填写 API URL 和 API 密钥', 'easyMode.api.modelRequired': '请先获取模型列表并选择模型', 'easyMode.api.testSuccess': '连接成功!发现 {count} 个模型', @@ -1125,6 +1126,7 @@ export const zhCN = { 'apiAccess.error.requiredKey': 'API 密钥不能为空', 'apiAccess.error.baseRequired': '{provider} 接入必须填写 Base URL', 'apiAccess.error.noModels': '没有发现可放行的模型,请确认 Base URL 和 API 密钥', + 'apiAccess.error.fetchModelsBeforeSave': '请先成功获取模型列表,并至少选择一个模型', 'apiAccess.error.stale': '接入配置已被其他操作修改,请刷新后重试', 'apiAccess.error.duplicate': '相同的接入配置已经存在', 'apiAccess.error.remarkInvalid': '备注不能超过 80 个字符,且不能包含换行或控制字符', @@ -1185,6 +1187,9 @@ export const zhCN = { 'apiAccess.error.noAvailableModels': '未发现可用模型', 'apiAccess.error.baseBeforeModels': '请先填写 Base URL,再获取模型', 'apiAccess.models.selected': '已选择 {count} 个模型', + 'apiAccess.models.selectionRequired': '尚未选择模型', + 'apiAccess.models.selectionRequiredHint': '请先获取模型列表,并选择至少一个模型', + 'apiAccess.models.staleHint': '连接信息已变化,请重新获取模型列表', 'apiAccess.models.restricted': '已配置模型限制', 'apiAccess.models.autoAll': '保存时默认开放全部模型', 'apiAccess.models.upstreamDefault': '使用上游默认模型', @@ -1200,7 +1205,7 @@ export const zhCN = { 'apiAccess.baseRequiredPlaceholder': '必填,例如 https://api.example.com', 'apiAccess.baseOptionalPlaceholder': '可选,留空使用默认地址', 'apiAccess.preset.title': 'OpenAI 兼容预设', - 'apiAccess.preset.description': '已预填 DeepSeek 官方地址,保存时自动发现并开放全部模型', + 'apiAccess.preset.description': '已预填 DeepSeek 官方地址;请获取模型列表并选择需要开放的模型', 'apiAccess.thinking.builtIn': '内置思考等级', 'apiAccess.thinking.builtInDescription': '自动应用到当前开放的全部模型', 'apiAccess.thinking.title': '思考等级', diff --git a/src/pages/ApiAccessPage.tsx b/src/pages/ApiAccessPage.tsx index 69a09bc..451223f 100644 --- a/src/pages/ApiAccessPage.tsx +++ b/src/pages/ApiAccessPage.tsx @@ -1,3 +1,4 @@ +import { createPortal } from 'react-dom'; import { useConfirmation } from '../components/ConfirmationDialog'; import { type CSSProperties, @@ -53,9 +54,14 @@ import { responseList, } from '../services/managementApi'; import { + applyDeepSeekModelPreset, + DEEPSEEK_BASE_URL, + DEEPSEEK_THINKING_LEVELS, fetchModels, + mergeModelOptions, modelsFromRecord, normalizeBaseUrl, + reconcileModelSelection, type ModelOption, type ModelProvider, } from '../services/modelService'; @@ -80,9 +86,8 @@ export type ProviderSection = export type ProviderCategory = ProviderSection | 'deepseek'; -export const DEEPSEEK_BASE_URL = 'https://api.deepseek.com'; export const OPENAI_THINKING_LEVELS = ['low', 'medium', 'high', 'xhigh'] as const; -export const DEEPSEEK_THINKING_LEVELS = ['low', 'high', 'max'] as const; +export { DEEPSEEK_BASE_URL, DEEPSEEK_THINKING_LEVELS }; type ProviderDefinition = { id: ProviderCategory; @@ -430,9 +435,6 @@ export const modelSelectionForDiscovery = ( ); }; -export const allModelSelectionForDiscovery = (models: ModelOption[]) => - new Set(models.map((model) => model.name.trim().toLowerCase()).filter(Boolean)); - export const parseProviderApiKeys = (value: string) => value .split(/\r?\n/) .map((item) => item.trim()) @@ -556,10 +558,15 @@ export const applyProviderPreset = ( category: ProviderCategory, draft: ProviderDraft, ): ProviderDraft => { + if (category === 'deepseek') { + return { + ...draft, + thinkingLevels: [...DEEPSEEK_THINKING_LEVELS], + models: applyDeepSeekModelPreset(draft.models), + }; + } if (!definitionFor(category).openAi || draft.thinkingLevels === undefined) return draft; - const levels = category === 'deepseek' - ? [...DEEPSEEK_THINKING_LEVELS] - : draft.thinkingLevels; + const levels = draft.thinkingLevels; return { ...draft, models: draft.models.map((model) => { @@ -805,7 +812,6 @@ export function ApiAccessPage() { const [error, setError] = useState(''); const feedback = useAppNotice(); const { showNotice: setNotice } = feedback; - const [feedbackRow, setFeedbackRow] = useState(null); const [dialogOpen, setDialogOpen] = useState(false); const [editingRow, setEditingRow] = useState(null); const [dialogDraft, setDialogDraft] = useState(emptyProviderDraft); @@ -908,7 +914,6 @@ export function ApiAccessPage() { const openCreate = () => { feedback.clearNotice(); - setFeedbackRow(null); setError(''); setEditingRow(null); setDialogDraft(createProviderDraft(activeCategory)); @@ -917,7 +922,6 @@ export function ApiAccessPage() { const openEdit = (row: ProviderRow) => { feedback.clearNotice(); - setFeedbackRow(null); setError(''); setEditingRow(row); const draft = draftFromRow(row); @@ -927,8 +931,10 @@ export function ApiAccessPage() { setDialogOpen(true); }; - const saveProvider = async (nextDraft: ProviderDraft): Promise => { - setFeedbackRow(null); + const saveProvider = async ( + nextDraft: ProviderDraft, + modelDiscoveryReady: boolean, + ): Promise => { const definition = activeDefinition; const preparedDraft = applyProviderRemarkIdentity( activeCategory, @@ -956,6 +962,16 @@ export function ApiAccessPage() { : t('apiAccess.error.requiredKey'), }; } + if ( + activeCategory === 'deepseek' + && (!modelDiscoveryReady || preparedDraftForSave.models.length === 0) + ) { + return { + saved: false, + target: 'models', + error: t('apiAccess.error.fetchModelsBeforeSave'), + }; + } if (Array.from(preparedDraft.remark.trim()).length > 80 || /[\u0000-\u001f\u007f]/.test(preparedDraft.remark)) { return { saved: false, target: 'form', error: t('apiAccess.error.remarkInvalid') }; } @@ -972,7 +988,11 @@ export function ApiAccessPage() { setError(''); try { let draftToSave = { ...preparedDraftForSave, baseUrl }; - if (definition.openAi && draftToSave.models.length === 0) { + if ( + definition.openAi + && activeCategory !== 'deepseek' + && draftToSave.models.length === 0 + ) { let fetchedModels: ModelOption[]; try { fetchedModels = await fetchModels( @@ -1057,7 +1077,6 @@ export function ApiAccessPage() { const deleteRow = async (row: ProviderRow) => { if (!await askConfirmation({ title: t('common.delete'), message: t('apiAccess.deleteConfirm', { remark: row.remark || row.name }), confirmText: t('common.delete'), variant: 'danger' })) return; - setFeedbackRow(providerDragId(row)); feedback.clearNotice(); setBusy(true); setError(''); @@ -1077,7 +1096,6 @@ export function ApiAccessPage() { remark: '', }, }); - setFeedbackRow(null); setNotice({ key: 'apiAccess.notice.deleted' }); await loadProviders(); } catch (requestError) { @@ -1088,7 +1106,6 @@ export function ApiAccessPage() { }; const toggleProvider = async (row: ProviderRow) => { - setFeedbackRow(providerDragId(row)); setBusy(true); setError(''); setNotice(''); @@ -1131,7 +1148,6 @@ export function ApiAccessPage() { const reorderProviders = async (source: ProviderRow, target: ProviderRow) => { if (source.section !== target.section || source.index === target.index) return; - setFeedbackRow(providerDragId(source)); setBusy(true); setError(''); setNotice(''); @@ -1197,7 +1213,6 @@ export function ApiAccessPage() { {error ?
{error}
: null} -
- {feedbackRow === null ? : null} {loading ? (
{t('apiAccess.loading')}
) : rows.length === 0 ? ( @@ -1273,7 +1286,6 @@ export function ApiAccessPage() { {row.baseUrl || t('apiAccess.defaultUrl')} {row.models.length > 0 ? {t('apiAccess.models.summary', { count: row.models.length })} : null} - {feedbackRow === providerDragId(row) ? : null} {row.priority === null ? null : (
@@ -1335,6 +1347,17 @@ export function ApiAccessPage() { onClose={() => setHealthDialogRow(null)} /> ) : null} + {typeof document === 'undefined' + ? null + : createPortal( + , + document.body, + )} ); } @@ -1563,7 +1586,7 @@ type ApiProviderDialogProps = { initialDraft: ProviderDraft; busy: boolean; onClose: () => void; - onSave: (draft: ProviderDraft) => Promise; + onSave: (draft: ProviderDraft, modelDiscoveryReady: boolean) => Promise; }; function ApiProviderDialog({ @@ -1583,21 +1606,22 @@ function ApiProviderDialog({ const [formError, setFormError] = useState(''); const [discoveredModels, setDiscoveredModels] = useState([]); const [modelDiscoveryOpen, setModelDiscoveryOpen] = useState(false); + const [modelDiscoveryReady, setModelDiscoveryReady] = useState( + () => activeCategory !== 'deepseek' + || Boolean(editingRow && initialDraft.models.some((model) => model.name.trim())), + ); const [modelSearch, setModelSearch] = useState(''); const [thinkingLevelInput, setThinkingLevelInput] = useState(''); const [selectedModelNames, setSelectedModelNames] = useState>( () => new Set(initialDraft.models.map((model) => model.name.toLowerCase())), ); const modelCardRef = useRef(null); + const discoverySelectionInitializedRef = useRef(false); - const modelOptions = useMemo(() => { - const options = new Map(); - [...discoveredModels, ...draft.models].forEach((model) => { - const name = model.name.trim(); - if (name) options.set(name.toLowerCase(), { ...model, name }); - }); - return Array.from(options.values()); - }, [discoveredModels, draft.models]); + const modelOptions = useMemo( + () => mergeModelOptions(discoveredModels, draft.models), + [discoveredModels, draft.models], + ); const configuredModels = useMemo( () => draft.models.filter((model) => model.name.trim()), @@ -1622,6 +1646,7 @@ function ApiProviderDialog({ setFormError(''); if (field === 'apiKey' || field === 'baseUrl' || field === 'headersText') { setModelError(''); + if (activeCategory === 'deepseek') setModelDiscoveryReady(false); } setDraft((current) => ({ ...current, [field]: value })); }; @@ -1707,12 +1732,12 @@ function ApiProviderDialog({ activeCategory, { ...draft, models: fetchedModels }, ).models; + const selectionMode = discoverySelectionInitializedRef.current ? 'refresh' : 'initial'; setDiscoveredModels(models); - setSelectedModelNames(new Set( - [...models, ...draft.models] - .map((model) => model.name.trim().toLowerCase()) - .filter(Boolean), - )); + setSelectedModelNames((current) => + reconcileModelSelection(models, draft.models, current, selectionMode)); + discoverySelectionInitializedRef.current = true; + if (activeCategory === 'deepseek') setModelDiscoveryReady(true); if (!models.length) setModelError(t('apiAccess.error.noAvailableModels')); } catch (requestError) { setModelError(requestErrorMessage(requestError)); @@ -1729,6 +1754,7 @@ function ApiProviderDialog({ return; } setModelSearch(''); + discoverySelectionInitializedRef.current = false; setSelectedModelNames(new Set(draft.models.map((model) => model.name.toLowerCase()))); setModelDiscoveryOpen(true); void discoverModels(); @@ -1777,7 +1803,7 @@ function ApiProviderDialog({ const submit = async (event: FormEvent) => { event.preventDefault(); setFormError(''); - const result = await onSave(draft); + const result = await onSave(draft, modelDiscoveryReady); if (result.saved) { onClose(); return; @@ -1794,15 +1820,22 @@ function ApiProviderDialog({ const hasModelExclusions = activeSection !== 'openai-compatibility' && Boolean(draft.excludedModelsText?.trim()); + const deepSeekModelsStale = activeCategory === 'deepseek' && !modelDiscoveryReady; const modelSummaryTitle = configuredModels.length > 0 ? t('apiAccess.models.selected', { count: configuredModels.length }) + : activeCategory === 'deepseek' + ? t('apiAccess.models.selectionRequired') : hasModelExclusions ? t('apiAccess.models.restricted') : activeSection === 'openai-compatibility' ? t('apiAccess.models.autoAll') : t('apiAccess.models.upstreamDefault'); const modelSummaryDetail = configuredModels.length > 0 - ? configuredModels.slice(0, 3).map((model) => model.name).join('、') + ? deepSeekModelsStale + ? t('apiAccess.models.staleHint') + : configuredModels.slice(0, 3).map((model) => model.name).join('、') + : activeCategory === 'deepseek' + ? t('apiAccess.models.selectionRequiredHint') : hasModelExclusions ? t('apiAccess.models.hiddenHint') : activeSection === 'openai-compatibility' diff --git a/src/pages/EasyModePage.tsx b/src/pages/EasyModePage.tsx index 96f65c9..a975809 100644 --- a/src/pages/EasyModePage.tsx +++ b/src/pages/EasyModePage.tsx @@ -17,6 +17,7 @@ import { LoaderCircle, Monitor, Moon, + RefreshCw, Sun, X, } from "lucide-react"; @@ -30,8 +31,12 @@ import { responseList, } from "../services/managementApi"; import { + applyDeepSeekModelPreset, + DEEPSEEK_BASE_URL, fetchModels, + mergeModelOptions, normalizeBaseUrl, + reconcileModelSelection, type ModelOption, type ModelProvider, } from "../services/modelService"; @@ -84,7 +89,7 @@ const apiSectionOptions: ApiSectionOption[] = [ { id: "claude", managementSection: "claude-api-key", nameKey: "easyMode.api.platformName.claude", provider: "claude", defaultBaseUrl: "", icon: claudeIcon }, { id: "codex", managementSection: "codex-api-key", nameKey: "easyMode.api.platformName.codex", provider: "codex", defaultBaseUrl: "", icon: codexIcon }, { id: "gemini", managementSection: "gemini-api-key", nameKey: "easyMode.api.platformName.gemini", provider: "gemini", defaultBaseUrl: "", icon: geminiIcon }, - { id: "deepseek", managementSection: "openai-compatibility", nameKey: "easyMode.api.platformName.deepseek", provider: "openai", defaultBaseUrl: "https://api.deepseek.com", icon: deepseekIcon }, + { id: "deepseek", managementSection: "openai-compatibility", nameKey: "easyMode.api.platformName.deepseek", provider: "openai", defaultBaseUrl: DEEPSEEK_BASE_URL, icon: deepseekIcon }, ]; const isDeepSeekRecord = (record: Record) => { @@ -135,6 +140,7 @@ export function EasyModePage({ const [apiTesting, setApiTesting] = useState(false); const [apiTestedModels, setApiTestedModels] = useState([]); const [apiSelectedModels, setApiSelectedModels] = useState([]); + const [apiModelsReady, setApiModelsReady] = useState(false); const [apiErrorMessage, setApiTestError] = useState(""); const apiTestError = typeof apiErrorMessage === "string" ? apiErrorMessage @@ -321,6 +327,7 @@ export function EasyModePage({ if (opt) setApiBaseUrl(opt.defaultBaseUrl); setApiTestedModels([]); setApiSelectedModels([]); + setApiModelsReady(false); setApiTestError(""); clearApiNotice(); setGuideApiSaved(false); @@ -338,10 +345,7 @@ export function EasyModePage({ } setApiTesting(true); setApiTestError(""); - setApiTestedModels([]); - setApiSelectedModels([]); setGuideApiSaved(false); - setGuideApiModelsFetched(false); const opt = apiSectionOptions.find((o) => o.id === selectedApiSection); const providerType = opt ? opt.provider : "openai"; @@ -356,8 +360,18 @@ export function EasyModePage({ 10000, ); if (models.length > 0) { - setApiTestedModels(models); - setApiSelectedModels(models); + const mergedModels = mergeModelOptions(models); + const selectedNames = reconcileModelSelection( + mergedModels, + [], + apiSelectedModels.map((model) => model.name), + apiModelsReady ? "refresh" : "initial", + ); + setApiTestedModels(mergedModels); + setApiSelectedModels( + mergedModels.filter((model) => selectedNames.has(model.name.trim().toLowerCase())), + ); + setApiModelsReady(true); setGuideApiModelsFetched(true); } else { setApiTestError({ key: "easyMode.api.noModelsFound" }); @@ -390,6 +404,10 @@ export function EasyModePage({ setApiTestError({ key: "easyMode.api.apiKeyRequired" }); return; } + if (selectedApiSection === "deepseek" && !apiModelsReady) { + setApiTestError({ key: "easyMode.api.fetchListFirst" }); + return; + } if (apiSelectedModels.length === 0) { setApiTestError({ key: "easyMode.api.modelRequired" }); return; @@ -408,7 +426,10 @@ export function EasyModePage({ const managementSection = selectedOption?.managementSection ?? "openai-compatibility"; const configPayload = await managementApi.get("/config"); const list = responseList(configPayload, managementSection); - const models = apiSelectedModels.map((model) => ({ name: model.name.trim() })); + const selectedModels = apiSelectedModels.map((model) => ({ name: model.name.trim() })); + const models = selectedApiSection === "deepseek" + ? applyDeepSeekModelPreset(selectedModels) + : selectedModels; const newEntry = managementSection === "openai-compatibility" ? { name: apiRemark.trim() || `${selectedApiSection} (${list.length + 1})`, @@ -923,7 +944,12 @@ export function EasyModePage({ type="text" className="text-input" value={apiBaseUrl} - onChange={(e) => { setApiBaseUrl(e.target.value); setGuideApiSaved(false); }} + onChange={(e) => { + setApiBaseUrl(e.target.value); + setApiModelsReady(false); + setGuideApiModelsFetched(false); + setGuideApiSaved(false); + }} placeholder="https://..." />
@@ -933,7 +959,12 @@ export function EasyModePage({ type="password" className="text-input" value={apiKey} - onChange={(e) => { setApiKey(e.target.value); setGuideApiSaved(false); }} + onChange={(e) => { + setApiKey(e.target.value); + setApiModelsReady(false); + setGuideApiModelsFetched(false); + setGuideApiSaved(false); + }} placeholder="sk-..." /> @@ -963,11 +994,24 @@ export function EasyModePage({ <>
{t("easyMode.api.modelListTitle")} +
- {t("easyMode.api.modelListHint")} + + {selectedApiSection === "deepseek" && !apiModelsReady + ? t("easyMode.api.modelListStale") + : t("easyMode.api.modelListHint")} +
{apiTestedModels.map((model) => { @@ -983,6 +1027,7 @@ export function EasyModePage({ handleToggleApiModel(model)} /> {model.name} @@ -996,7 +1041,13 @@ export function EasyModePage({