fix(models): 获取模型列表 404 改为友好提示 + 助手侧走后端绕 CORS

场景:部分服务商(如某些厂商的 Anthropic 兼容接口)不提供 /models 列表接口,
之前会直接显示 HTTP 404 Not Found / Failed to fetch 等技术错误,用户体验差。

- Rust list_remote_models:识别 404/405/501 作为"不支持自动获取"场景,
  返回带 [NOT_SUPPORTED] 前缀的友好错误,而非裸 HTTP 状态码
- 模型配置页「获取列表」:识别 [NOT_SUPPORTED] 后弹出引导对话框,
  点击「模型」按钮直接进入手动添加流程
- 助手设置页「获取列表」:改为走 Rust 后端 api.listRemoteModels,
  原先直接用前端 fetch 会被 WebView CORS 拦截(provider 不返回 CORS 头),
  改走后端既绕开 CORS 又能获得一致的友好提示
- Web 模式 dev-api.js 同步 404 识别逻辑,保证桌面 / Web 行为一致
- 补齐 models.fetchNotSupported / assistant.fetchNotSupported 多语言文案
This commit is contained in:
晴天
2026-04-20 16:01:24 +08:00
parent 12cdc72d2b
commit 1ef9ca8ede
6 changed files with 38 additions and 45 deletions

View File

@@ -3976,49 +3976,10 @@ function showSettings() {
btn.textContent = t('assistant.fetching')
resultEl.innerHTML = '<span style="color:var(--text-tertiary)">' + t('assistant.fetchingModels') + '</span>'
try {
const base = cleanBaseUrl(baseUrl, selApiType)
const hdrs = authHeaders(selApiType, apiKey)
let models = []
if (selApiType === 'anthropic-messages') {
// Anthropic: GET /v1/models
const resp = await fetch(base + '/models', { headers: hdrs, signal: AbortSignal.timeout(10000) })
if (!resp.ok) {
const text = await resp.text().catch(() => '')
let msg = 'HTTP ' + resp.status
try { msg = JSON.parse(text).error?.message || msg } catch {}
resultEl.innerHTML = '<span style="color:var(--error)">✗ ' + escHtml(msg) + '</span>'
return
}
const data = await resp.json()
models = (data.data || []).map(m => m.id).filter(Boolean).sort()
} else if (selApiType === 'google-gemini') {
// Gemini: GET /models?key=xxx
const resp = await fetch(base + '/models?key=' + apiKey, { signal: AbortSignal.timeout(10000) })
if (!resp.ok) {
const text = await resp.text().catch(() => '')
let msg = 'HTTP ' + resp.status
try { msg = JSON.parse(text).error?.message || msg } catch {}
resultEl.innerHTML = '<span style="color:var(--error)">✗ ' + escHtml(msg) + '</span>'
return
}
const data = await resp.json()
models = (data.models || []).map(m => m.name?.replace('models/', '') || m.name).filter(Boolean).sort()
} else {
// OpenAI: GET /v1/models
const resp = await fetch(base + '/models', { headers: hdrs, signal: AbortSignal.timeout(10000) })
if (!resp.ok) {
const text = await resp.text().catch(() => '')
let msg = 'HTTP ' + resp.status
try { msg = JSON.parse(text).error?.message || msg } catch {}
resultEl.innerHTML = '<span style="color:var(--error)">✗ ' + escHtml(msg) + '</span>'
return
}
const data = await resp.json()
models = (data.data || []).map(m => m.id).filter(Boolean).sort()
}
if (models.length === 0) {
// 走 Rust 后端(桌面 & Web 模式统一),绕过 WebView CORS 限制
// 部分 provider 不返回 CORS 头,直接前端 fetch 会报 Failed to fetch
const models = await api.listRemoteModels(baseUrl, apiKey, selApiType)
if (!models || models.length === 0) {
resultEl.innerHTML = '<span style="color:var(--warning)">' + t('assistant.noModelsFound') + '</span>'
return
}
@@ -4028,7 +3989,13 @@ function showSettings() {
).join('')
dropdown.style.display = 'block'
} catch (err) {
resultEl.innerHTML = '<span style="color:var(--error)">✗ ' + escHtml(err.message) + '</span>'
const errStr = String(err?.message || err)
// 服务商不支持 /models 接口 → 友好提示引导手动填写
if (errStr.includes('[NOT_SUPPORTED]') || errStr.includes('不支持自动获取')) {
resultEl.innerHTML = '<span style="color:var(--warning);line-height:1.5">⚠ ' + escHtml(t('assistant.fetchNotSupported')) + '</span>'
} else {
resultEl.innerHTML = '<span style="color:var(--error)">✗ ' + escHtml(errStr) + '</span>'
}
} finally {
btn.disabled = false
btn.textContent = t('assistant.fetchBtn')

View File

@@ -1378,7 +1378,20 @@ async function fetchRemoteModels(btn, page, state, providerKey) {
} catch (e) {
btn.disabled = false
btn.textContent = t('models.fetchList')
toast(t('models.fetchFailed', { error: e }), 'error')
const errStr = String(e?.message || e)
// 服务商不支持 /models 接口 → 友好弹窗引导手动添加
if (errStr.includes('[NOT_SUPPORTED]') || errStr.includes('不支持自动获取')) {
const msg = errStr.replace('[NOT_SUPPORTED] ', '').replace('获取模型列表失败: ', '')
showConfirm(t('models.fetchNotSupported', { error: msg }), {
title: t('models.fetchNotSupportedTitle'),
confirmText: t('models.addModel').replace('+ ', ''),
cancelText: t('common.close'),
}).then(yes => {
if (yes) addModel(btn.closest('.page') || document.querySelector('.page'), { config: state.config, save: state.save }, providerKey)
})
} else {
toast(t('models.fetchFailed', { error: errStr }), 'error')
}
}
}