fix: auto-detect deepseek profile in settings

This commit is contained in:
temp
2026-08-25 18:40:49 +08:00
parent d6d48c97c0
commit abc00c1fef
3 changed files with 55 additions and 5 deletions
@@ -51,3 +51,12 @@ test('Eino 模型 retry/failover 设置项有中英文文案', () => {
assert.ok(en.settingsBasic[key].length > 0, `en ${key} is empty`); assert.ok(en.settingsBasic[key].length > 0, `en ${key} is empty`);
}); });
}); });
test('AI 通道保存前会自动识别 DeepSeek 官方线路', () => {
assert.match(settings, /function\s+isOfficialDeepSeekBaseURL/);
assert.match(settings, /api\.deepseek\.com/);
assert.match(settings, /profile:\s*'deepseek'/);
assert.match(settings, /normalizeAIChannelProviderProfile\(\{/);
assert.match(settings, /normalizeAIConfigProviderProfiles\(currentConfig\.ai\)/);
assert.match(template, /<option value="deepseek">deepseek<\/option>/);
});
+45 -5
View File
@@ -1985,6 +1985,7 @@ async function applySettings() {
const activeChannelId = normalizeAIChannelId(selectedAIChannelId || currentConfig.ai.default_channel || 'default'); const activeChannelId = normalizeAIChannelId(selectedAIChannelId || currentConfig.ai.default_channel || 'default');
currentConfig.ai.channels[activeChannelId] = readAIChannelFromMainForm(activeChannelId); currentConfig.ai.channels[activeChannelId] = readAIChannelFromMainForm(activeChannelId);
currentConfig.ai.default_channel = activeChannelId; currentConfig.ai.default_channel = activeChannelId;
currentConfig.ai = normalizeAIConfigProviderProfiles(currentConfig.ai);
renderAIChannelSelect(); renderAIChannelSelect();
const activeChannel = currentConfig.ai.channels[activeChannelId] || {}; const activeChannel = currentConfig.ai.channels[activeChannelId] || {};
const prevOpenai = activeChannel; const prevOpenai = activeChannel;
@@ -1999,7 +2000,7 @@ async function applySettings() {
return String(s || '').split(/[\n,]/).map(v => v.trim()).filter(Boolean); return String(s || '').split(/[\n,]/).map(v => v.trim()).filter(Boolean);
}; };
const config = { const config = {
ai: currentConfig.ai, ai: normalizeAIConfigProviderProfiles(currentConfig.ai),
vision: visionPayload, vision: visionPayload,
fofa: { fofa: {
api_key: document.getElementById('fofa-api-key')?.value.trim() || '', api_key: document.getElementById('fofa-api-key')?.value.trim() || '',
@@ -2533,6 +2534,43 @@ function normalizeAIChannelId(name) {
return id || 'default'; return id || 'default';
} }
function aiChannelBaseURLHost(baseUrl) {
const raw = String(baseUrl || '').trim();
if (!raw) return '';
try {
return new URL(raw).hostname.toLowerCase().replace(/^www\./, '');
} catch (e) {
try {
return new URL(`https://${raw.replace(/^\/+/, '')}`).hostname.toLowerCase().replace(/^www\./, '');
} catch (_) {
return '';
}
}
}
function isOfficialDeepSeekBaseURL(baseUrl) {
return aiChannelBaseURLHost(baseUrl) === 'api.deepseek.com';
}
function normalizeAIChannelProviderProfile(channel) {
if (!channel || typeof channel !== 'object') return channel;
if (isOfficialDeepSeekBaseURL(channel.base_url)) {
channel.reasoning = {
...(channel.reasoning || {}),
profile: 'deepseek'
};
}
return channel;
}
function normalizeAIConfigProviderProfiles(ai) {
if (!ai || typeof ai !== 'object' || !ai.channels || typeof ai.channels !== 'object') return ai;
Object.keys(ai.channels).forEach((id) => {
ai.channels[id] = normalizeAIChannelProviderProfile(ai.channels[id] || {});
});
return ai;
}
function escapeAIChannelHtml(value) { function escapeAIChannelHtml(value) {
return String(value == null ? '' : value) return String(value == null ? '' : value)
.replace(/&/g, '&amp;') .replace(/&/g, '&amp;')
@@ -2559,13 +2597,13 @@ function ensureAIConfigShape(cfg) {
reasoning: oa.reasoning || {} reasoning: oa.reasoning || {}
}; };
} }
return { default_channel: def, channels }; return normalizeAIConfigProviderProfiles({ default_channel: def, channels });
} }
function readAIChannelFromMainForm(id) { function readAIChannelFromMainForm(id) {
const prev = currentConfig?.ai?.channels?.[id] || {}; const prev = currentConfig?.ai?.channels?.[id] || {};
const maxCompletionTokens = parseInt(document.getElementById('openai-max-completion-tokens')?.value, 10) || 32768; const maxCompletionTokens = parseInt(document.getElementById('openai-max-completion-tokens')?.value, 10) || 32768;
return { return normalizeAIChannelProviderProfile({
...prev, ...prev,
name: (document.getElementById('ai-channel-name')?.value || '').trim() || prev.name || id, name: (document.getElementById('ai-channel-name')?.value || '').trim() || prev.name || id,
provider: document.getElementById('openai-provider')?.value || 'openai', provider: document.getElementById('openai-provider')?.value || 'openai',
@@ -2581,7 +2619,7 @@ function readAIChannelFromMainForm(id) {
profile: document.getElementById('openai-reasoning-profile')?.value || 'auto', profile: document.getElementById('openai-reasoning-profile')?.value || 'auto',
allow_client_reasoning: document.getElementById('openai-reasoning-allow-client')?.checked !== false allow_client_reasoning: document.getElementById('openai-reasoning-allow-client')?.checked !== false
} }
}; });
} }
function writeAIChannelToMainForm(id) { function writeAIChannelToMainForm(id) {
@@ -2611,7 +2649,7 @@ function writeAIChannelToMainForm(id) {
const effEl = document.getElementById('openai-reasoning-effort'); const effEl = document.getElementById('openai-reasoning-effort');
if (effEl) effEl.value = ['', 'low', 'medium', 'high', 'max', 'xhigh'].includes(String(r.effort || '').toLowerCase()) ? String(r.effort || '').toLowerCase() : ''; if (effEl) effEl.value = ['', 'low', 'medium', 'high', 'max', 'xhigh'].includes(String(r.effort || '').toLowerCase()) ? String(r.effort || '').toLowerCase() : '';
const profileEl = document.getElementById('openai-reasoning-profile'); const profileEl = document.getElementById('openai-reasoning-profile');
if (profileEl) profileEl.value = ['auto', 'deepseek_compat', 'openai_compat', 'output_config_effort'].includes(String(r.profile || '').toLowerCase()) ? String(r.profile || '').toLowerCase() : 'auto'; if (profileEl) profileEl.value = ['auto', 'deepseek', 'deepseek_compat', 'openai_compat', 'output_config_effort'].includes(String(r.profile || '').toLowerCase()) ? String(r.profile || '').toLowerCase() : 'auto';
const allowEl = document.getElementById('openai-reasoning-allow-client'); const allowEl = document.getElementById('openai-reasoning-allow-client');
if (allowEl) allowEl.checked = r.allow_client_reasoning !== false; if (allowEl) allowEl.checked = r.allow_client_reasoning !== false;
syncModelListFetchButtons(); syncModelListFetchButtons();
@@ -2943,6 +2981,7 @@ async function persistAIChannelsToServer(successMessage, options = {}) {
currentConfig.ai.default_channel = latestAI.default_channel || id; currentConfig.ai.default_channel = latestAI.default_channel || id;
} }
} }
currentConfig.ai = normalizeAIConfigProviderProfiles(currentConfig.ai);
const updateResponse = await apiFetch('/api/config', { const updateResponse = await apiFetch('/api/config', {
method: 'PUT', method: 'PUT',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
@@ -2971,6 +3010,7 @@ async function persistAIConfigOnlyToServer(successMessage) {
if (typeof requirePermission === 'function' && !requirePermission('config:write')) return false; if (typeof requirePermission === 'function' && !requirePermission('config:write')) return false;
if (!currentConfig) return false; if (!currentConfig) return false;
currentConfig.ai = ensureAIConfigShape(currentConfig); currentConfig.ai = ensureAIConfigShape(currentConfig);
currentConfig.ai = normalizeAIConfigProviderProfiles(currentConfig.ai);
showAIChannelSaveHint(settingsT('settingsBasic.aiChannelSaving', '正在保存通道...'), true); showAIChannelSaveHint(settingsT('settingsBasic.aiChannelSaving', '正在保存通道...'), true);
try { try {
const updateResponse = await apiFetch('/api/config', { const updateResponse = await apiFetch('/api/config', {
+1
View File
@@ -3695,6 +3695,7 @@
<label for="openai-reasoning-profile" data-i18n="settingsBasic.openaiReasoningProfile">线路</label> <label for="openai-reasoning-profile" data-i18n="settingsBasic.openaiReasoningProfile">线路</label>
<select id="openai-reasoning-profile"> <select id="openai-reasoning-profile">
<option value="auto">auto</option> <option value="auto">auto</option>
<option value="deepseek">deepseek</option>
<option value="deepseek_compat">deepseek_compat</option> <option value="deepseek_compat">deepseek_compat</option>
<option value="openai_compat">openai_compat</option> <option value="openai_compat">openai_compat</option>
<option value="output_config_effort">output_config_effort</option> <option value="output_config_effort">output_config_effort</option>