diff --git a/src/locales/modules/assistant.js b/src/locales/modules/assistant.js index 8d98e76..7527a18 100644 --- a/src/locales/modules/assistant.js +++ b/src/locales/modules/assistant.js @@ -163,6 +163,16 @@ export default { 'Noch keine Fallback-Modelle, klicken Sie unten auf Hinzufügen', ), fallbackAdd: _('添加备用模型', 'Add Fallback Model', '新增備用模型', 'フォールバックモデルを追加', '대체 모델 추가', 'Thêm mô hình dự phòng', 'Agregar modelo de respaldo', 'Adicionar modelo de fallback', 'Добавить резервную модель', 'Ajouter un modèle de secours', 'Fallback-Modell hinzufügen'), + fallbackPrimaryRow: _('主模型(当前)', 'Primary Model (current)', '主模型(當前)', 'メインモデル(現在)', '기본 모델 (현재)', 'Mô hình chính (hiện tại)'), + fallbackPickProviderHint: _('选择服务商快速添加:', 'Pick a provider to quickly add:', '選擇服務商快速新增:', 'プロバイダーを選択して素早く追加:', '서비스 제공자를 선택하여 빠르게 추가:', 'Chọn nhà cung cấp để thêm nhanh:'), + fallbackAddCopyPrimary: _('从主模型复制', 'Copy from Primary', '從主模型複製', 'メインモデルからコピー', '기본 모델에서 복사', 'Sao chép từ mô hình chính'), + fallbackAddCustom: _('自定义 / 自建', 'Custom / Self-hosted', '自訂 / 自建', 'カスタム / セルフホスト', '사용자 정의 / 자체 호스팅', 'Tùy chỉnh / Tự host'), + fallbackMoreProviders: _('更多服务商…', 'More providers…', '更多服務商…', 'その他のプロバイダー…', '더 많은 제공자…', 'Nhiều nhà cung cấp hơn…'), + fallbackEditAdvanced: _('编辑', 'Edit', '編輯', '編集', '편집', 'Chỉnh sửa'), + fallbackHideAdvanced: _('收起', 'Collapse', '收合', '折りたたむ', '접기', 'Thu gọn'), + fallbackShowAdvanced: _('高级选项(Base URL / API 类型)', 'Advanced (Base URL / API type)', '進階選項(Base URL / API 類型)', '詳細(Base URL / API タイプ)', '고급 (Base URL / API 유형)', 'Nâng cao (Base URL / Loại API)'), + fallbackUnnamedModel: _('未选模型', 'No model', '未選模型', 'モデル未選択', '모델 미선택', 'Chưa chọn mô hình'), + fallbackPickProviderTitle: _('所有服务商', 'All Providers', '所有服務商', 'すべてのプロバイダー', '모든 제공자', 'Tất cả nhà cung cấp'), fallbackRemove: _('删除此备用模型', 'Remove this fallback model', '刪除此備用模型', 'このフォールバックモデルを削除', '이 대체 모델 삭제', 'Xóa mô hình dự phòng này', 'Eliminar este modelo de respaldo', 'Remover este modelo de fallback', 'Удалить эту резервную модель', 'Supprimer ce modèle de secours', 'Dieses Fallback-Modell entfernen'), fallbackEnabled: _('启用', 'Enabled', '啟用', '有効', '활성', 'Bật', 'Activo', 'Ativo', 'Активно', 'Activé', 'Aktiv'), fallbackLabelPlaceholder: _('显示名称(选填,如 DeepSeek 备用)', 'Display name (optional, e.g. DeepSeek Backup)', '顯示名稱(選填,如 DeepSeek 備用)', '表示名(任意、例:DeepSeek バックアップ)', '표시 이름(선택, 예: DeepSeek 백업)', 'Tên hiển thị (tuỳ chọn, ví dụ: DeepSeek dự phòng)', 'Nombre para mostrar (opcional)', 'Nome de exibição (opcional)', 'Отображаемое имя (необязательно)', 'Nom d\'affichage (facultatif)', 'Anzeigename (optional)'), diff --git a/src/pages/assistant.js b/src/pages/assistant.js index 52c0f99..36bcf71 100644 --- a/src/pages/assistant.js +++ b/src/pages/assistant.js @@ -3082,7 +3082,7 @@ function showSettings() { - +
@@ -3093,8 +3093,20 @@ function showSettings() {
${t('assistant.fallbackModelsDesc')}
-
- + +
+ 📌 + ${t('assistant.fallbackPrimaryRow')} + + +
+ +
+ +
+
${t('assistant.fallbackPickProviderHint')}
+
+
@@ -3224,76 +3236,201 @@ function showSettings() { const renderFallbackList = () => { if (!fallbackListEl) return if (fallbackDrafts.length === 0) { - fallbackListEl.innerHTML = `
${t('assistant.fallbackEmpty')}
` + fallbackListEl.innerHTML = `
${t('assistant.fallbackEmpty')}
` updateFallbackCount() return } - fallbackListEl.innerHTML = fallbackDrafts.map((fb, idx) => ` -
-
- #${idx + 2} - - - + fallbackListEl.innerHTML = fallbackDrafts.map((fb, idx) => { + const expanded = fb._editing === true || (!fb.baseUrl && !fb.model) + const modelText = fb.model || t('assistant.fallbackUnnamedModel') + const hostText = fb.baseUrl ? hostOf(fb.baseUrl) : '' + const brandText = fb._brandLabel ? `${escHtml(fb._brandLabel)}` : '' + return ` +
+
+ ⋮⋮ + #${idx + 2} +
+ ${brandText} + ${escHtml(modelText)} + ${hostText ? `· ${escHtml(hostText)}` : ''} +
+ +
-
- - -
-
- - +
+
+ + +
+
at.value === normalizeApiType(fb.apiType)) ? 'open' : ''} style="margin-top:4px"> + ▸ ${t('assistant.fallbackShowAdvanced')} +
+ + +
+
- `).join('') + `}).join('') - // 绑定每张卡片的事件 - fallbackListEl.querySelectorAll('.ast-fallback-card').forEach(card => { - const idx = parseInt(card.dataset.fbIdx, 10) + // 绑定事件 + fallbackListEl.querySelectorAll('.ast-fallback-row').forEach(row => { + const idx = parseInt(row.dataset.fbIdx, 10) const sync = () => { fallbackDrafts[idx] = { ...fallbackDrafts[idx], - label: card.querySelector('.ast-fb-label').value.trim(), - baseUrl: card.querySelector('.ast-fb-url').value.trim(), - apiKey: card.querySelector('.ast-fb-key').value.trim(), - model: card.querySelector('.ast-fb-model').value.trim(), - apiType: normalizeApiType(card.querySelector('.ast-fb-apitype').value), - enabled: card.querySelector('.ast-fb-enabled').checked, + baseUrl: (row.querySelector('.ast-fb-url')?.value || fallbackDrafts[idx].baseUrl || '').trim(), + apiKey: (row.querySelector('.ast-fb-key')?.value || '').trim(), + model: (row.querySelector('.ast-fb-model')?.value || '').trim(), + apiType: normalizeApiType(row.querySelector('.ast-fb-apitype')?.value || fallbackDrafts[idx].apiType), } - // 仅更新透明度和计数,不重渲染(避免输入框丢焦点) - card.style.opacity = fallbackDrafts[idx].enabled === false ? '0.55' : '1' updateFallbackCount() + // 实时更新折叠态显示的 model / hostname + const headerModel = row.querySelector('div > span[style*="var(--font-mono)"]') + if (headerModel) { + const m = fallbackDrafts[idx].model + headerModel.textContent = m || t('assistant.fallbackUnnamedModel') + headerModel.style.color = m ? 'var(--text-primary)' : 'var(--text-tertiary)' + } } - card.querySelectorAll('.ast-fb-label, .ast-fb-url, .ast-fb-key, .ast-fb-model, .ast-fb-apitype, .ast-fb-enabled').forEach(el => { + row.querySelectorAll('.ast-fb-url, .ast-fb-key, .ast-fb-model, .ast-fb-apitype').forEach(el => { el.addEventListener('input', sync) el.addEventListener('change', sync) }) - card.querySelector('.ast-fb-remove').onclick = () => { + // 展开 / 收起 + row.querySelector('.ast-fb-toggle').onclick = () => { + fallbackDrafts[idx]._editing = !(fallbackDrafts[idx]._editing === true || (!fallbackDrafts[idx].baseUrl && !fallbackDrafts[idx].model)) + renderFallbackList() + } + // 删除 + row.querySelector('.ast-fb-remove').onclick = () => { fallbackDrafts.splice(idx, 1) renderFallbackList() } + // HTML5 拖拽排序 + row.ondragstart = (e) => { + e.dataTransfer.setData('text/plain', String(idx)) + e.dataTransfer.effectAllowed = 'move' + row.style.opacity = '0.4' + } + row.ondragend = () => { row.style.opacity = '' } + row.ondragover = (e) => { e.preventDefault(); row.style.borderColor = 'var(--primary)' } + row.ondragleave = () => { row.style.borderColor = 'var(--border-primary)' } + row.ondrop = (e) => { + e.preventDefault() + row.style.borderColor = 'var(--border-primary)' + const from = parseInt(e.dataTransfer.getData('text/plain'), 10) + if (isNaN(from) || from === idx) return + const [moved] = fallbackDrafts.splice(from, 1) + fallbackDrafts.splice(idx, 0, moved) + renderFallbackList() + } }) updateFallbackCount() } - renderFallbackList() - overlay.querySelector('#ast-fallback-add')?.addEventListener('click', () => { - // 默认从主模型推断 apiType,便于快速配同类备用 - const mainApi = overlay.querySelector('#ast-apitype')?.value || _config.apiType || 'openai-completions' + // 添加一个厂商预设备用(点击快捷按钮触发) + const addFallbackFromPreset = (preset) => { fallbackDrafts.push({ - label: '', - baseUrl: '', + baseUrl: preset.baseUrl, apiKey: '', model: '', - apiType: normalizeApiType(mainApi), - enabled: true, + apiType: normalizeApiType(preset.api), + _editing: true, + _brandLabel: preset.label, }) renderFallbackList() + // 聚焦到刚加的 apiKey 输入 + setTimeout(() => { + const last = fallbackListEl.querySelector('.ast-fallback-row:last-child .ast-fb-key') + last?.focus() + }, 30) + } + // 从主模型复制 + const addFallbackFromPrimary = () => { + const baseUrl = overlay.querySelector('#ast-baseurl')?.value?.trim() || '' + const apiKey = overlay.querySelector('#ast-apikey')?.value?.trim() || '' + const model = overlay.querySelector('#ast-model')?.value?.trim() || '' + const apiType = overlay.querySelector('#ast-apitype')?.value || 'openai-completions' + fallbackDrafts.push({ + baseUrl, + apiKey, + model, + apiType: normalizeApiType(apiType), + _editing: true, + }) + renderFallbackList() + setTimeout(() => { + const last = fallbackListEl.querySelector('.ast-fallback-row:last-child .ast-fb-model') + last?.focus() + last?.select() + }, 30) + } + // 渲染厂商预设按钮(6 个最常用 + 从主模型复制 + 自定义 + 更多) + const TOP_PRESETS = ['qtcool', 'openai', 'anthropic', 'deepseek', 'google', 'ollama'] + let showAllPresets = false + const renderPresetButtons = () => { + const shown = showAllPresets + ? PROVIDER_PRESETS + : PROVIDER_PRESETS.filter(p => TOP_PRESETS.includes(p.key)) + const presetBtnHtml = shown.map(p => ` + + `).join('') + const extraBtnHtml = ` + + + ${!showAllPresets ? `` : ''} + ` + fallbackPresetsEl.innerHTML = presetBtnHtml + extraBtnHtml + + // 绑定每个预设按钮 + fallbackPresetsEl.querySelectorAll('.ast-fb-preset-btn').forEach(btn => { + btn.onclick = () => { + const preset = PROVIDER_PRESETS.find(p => p.key === btn.dataset.presetKey) + if (preset) addFallbackFromPreset(preset) + } + }) + // 从主模型复制 + fallbackPresetsEl.querySelector('#ast-fb-copy-primary').onclick = addFallbackFromPrimary + // 自定义(空白) + fallbackPresetsEl.querySelector('#ast-fb-custom').onclick = () => { + const mainApi = overlay.querySelector('#ast-apitype')?.value || 'openai-completions' + fallbackDrafts.push({ + baseUrl: '', + apiKey: '', + model: '', + apiType: normalizeApiType(mainApi), + _editing: true, + }) + renderFallbackList() + setTimeout(() => { + const last = fallbackListEl.querySelector('.ast-fallback-row:last-child .ast-fb-key') + last?.focus() + }, 30) + } + // 更多服务商(展开全部) + fallbackPresetsEl.querySelector('#ast-fb-more')?.addEventListener('click', () => { + showAllPresets = true + renderPresetButtons() + }) + } + // 主模型表单任一字段变更时,同步主模型只读行 + ;['#ast-baseurl', '#ast-model', '#ast-apikey', '#ast-apitype'].forEach(sel => { + overlay.querySelector(sel)?.addEventListener('input', renderPrimaryRow) + overlay.querySelector(sel)?.addEventListener('change', renderPrimaryRow) }) + renderPrimaryRow() + renderFallbackList() + renderPresetButtons() // Tab 切换 overlay.querySelectorAll('.ast-tab').forEach(tab => { @@ -3988,16 +4125,19 @@ function showSettings() { } // 知识库 _config.knowledgeFiles = kbFiles - // #Compat-3: 备用模型组(过滤掉空卡片) + // #Compat-3: 备用模型组(过滤空卡 + 迁移清理老的禁用条目) + // 新 UI 不再暴露 enabled 开关(删除即停用),保存时: + // - 过滤掉空卡(baseUrl 或 model 为空) + // - 过滤掉老数据里 enabled===false 的条目(隐式迁移,用户以后看到的都是启用状态) + // - strip 掉 _editing / _brandLabel 等临时字段(只挑 5 个字段 map 出来) _config.fallbackModels = fallbackDrafts - .filter(f => f && f.baseUrl && f.model) + .filter(f => f && f.baseUrl && f.model && f.enabled !== false) .map(f => ({ label: f.label || f.model, baseUrl: f.baseUrl, apiKey: f.apiKey || '', model: f.model, apiType: normalizeApiType(f.apiType), - enabled: f.enabled !== false, })) saveConfig() overlay.remove()