mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-06 20:02:49 +08:00
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:
@@ -5119,6 +5119,11 @@ const handlers = {
|
||||
}
|
||||
clearTimeout(timeout)
|
||||
if (!resp.ok) {
|
||||
// 404/405/501 = 服务商不支持 /models 接口,给用户友好提示
|
||||
const code = resp.status
|
||||
if (code === 404 || code === 405 || code === 501) {
|
||||
throw new Error('[NOT_SUPPORTED] 该服务商不支持自动获取模型列表,请手动输入模型 ID')
|
||||
}
|
||||
const text = await resp.text().catch(() => '')
|
||||
let msg = `HTTP ${resp.status}`
|
||||
try {
|
||||
|
||||
@@ -5654,6 +5654,11 @@ pub async fn list_remote_models(
|
||||
let text = resp.text().await.unwrap_or_default();
|
||||
|
||||
if !status.is_success() {
|
||||
// 404/405/501 = 服务商不支持 /models 接口,给用户友好提示而非技术错误
|
||||
let code = status.as_u16();
|
||||
if code == 404 || code == 405 || code == 501 {
|
||||
return Err("[NOT_SUPPORTED] 该服务商不支持自动获取模型列表,请手动输入模型 ID".to_string());
|
||||
}
|
||||
let msg = extract_error_message(&text, status);
|
||||
return Err(format!("获取模型列表失败: {msg}"));
|
||||
}
|
||||
|
||||
@@ -331,6 +331,7 @@ export default {
|
||||
fetchBtn: _('获取列表', 'Fetch List', '取得列表', 'リスト取得', '목록 가져오기'),
|
||||
noModelsFound: _('未找到可用模型', 'No available models found', '', '利用可能なモデルが見つかりません', '사용 가능한 모델 없음'),
|
||||
modelsFound: _('找到 {count} 个模型', 'Found {count} models', '找到 {count} 個模型', '{count} モデルが見つかりました', '{count}개 모델 발견'),
|
||||
fetchNotSupported: _('该服务商不支持自动获取模型列表,请在下方「模型」输入框手动填写模型 ID', 'This provider does not support auto-fetching models. Please manually enter the model ID below.', '該服務商不支援自動取得模型列表,請在下方「模型」輸入框手動填写模型 ID'),
|
||||
personaSource: _('人设来源', 'Persona Source', '人設來源', 'ペルソナソース', '페르소나 소스'),
|
||||
personaDefault: _('默认', 'Default', '預設', 'デフォルト', '기본'),
|
||||
personaOpenClaw: _('OpenClaw Agent', 'OpenClaw Agent'),
|
||||
|
||||
@@ -139,6 +139,8 @@ export default {
|
||||
addSelected: _('添加选中', 'Add Selected', '新增選中'),
|
||||
selectAtLeast: _('请至少选择一个模型', 'Please select at least one model', '請至少選擇一個模型'),
|
||||
fetchFailed: _('获取模型列表失败: {error}', 'Failed to fetch model list: {error}', '取得模型列表失敗: {error}', 'モデルリスト取得失敗', '모델 목록 가져오기 실패'),
|
||||
fetchNotSupportedTitle: _('该服务商不支持自动获取', 'Auto-fetch not supported', '該服務商不支援自動取得', 'モデルリスト自動取得非対応', '자동 가져오기 미지원'),
|
||||
fetchNotSupported: _('该服务商的接口不支持自动获取模型列表。\n\n请点击「模型」按钮手动填写模型 ID。\n模型 ID 通常可在服务商的官网文档中找到。', 'This provider does not support auto-fetching model list.\n\nPlease click "Model" to manually enter the model ID.\nModel IDs can usually be found in the provider\'s documentation.', '該服務商的介面不支援自動取得模型列表。\n\n請点擊「模型」按鈕手動填写模型 ID。\n模型 ID 通常可在服務商的官網文件中找到。'),
|
||||
configNotReady: _('配置未加载完成,请稍候', 'Config not loaded yet, please wait', '設定未載入完成,請稍候'),
|
||||
fetchRemoteFailed: _('无法获取模型列表,请检查网络或稍后重试', 'Cannot fetch model list. Check network or try later.', '無法取得模型列表,請檢查網路或稍后重試'),
|
||||
configLoadFailed: _('加载配置失败', 'Failed to load config', '載入設定失敗'),
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user