diff --git a/desktop-tauri/i18n/i18n-src/en/models.json b/desktop-tauri/i18n/i18n-src/en/models.json index 180239953..c8dfd8fda 100644 --- a/desktop-tauri/i18n/i18n-src/en/models.json +++ b/desktop-tauri/i18n/i18n-src/en/models.json @@ -325,6 +325,7 @@ "这家的清单是你自己登记的,不填的项下游按未声明处理。": "This provider's catalog is what you registered yourself; fields left empty are treated as not declared downstream.", "这家还没有可用账号,刷新会使用默认登录态": "This provider has no usable accounts yet; the refresh will use the default login session", "这家还没有模型:点「添加模型」登记,或「获取上游模型」从上游拉取": "No models for this provider yet: click “Add model” to register one, or “Fetch models” to pull from upstream", + "自定义提供商还没有模型:选中具体某一家后点「添加模型」登记,或「获取上游模型」从上游拉取": "No models on custom providers yet: pick a specific provider, then click “Add model” to register one or “Fetch models” to pull from upstream", "这是模型自己能配的档位(来自上游目录),只读;要改某一档请到映射弹窗里选。": "These are the levels the model itself supports (from the upstream catalog); they're read-only. To change a level, select it in the mapping dialog.", "这条登记(不像内置模型那样只是隐藏),因为它的存在完全由这次登记决定。": "this registration (unlike built-in models, which are merely hidden), because its existence is decided entirely by this registration.", "远程": "Remote", diff --git a/desktop-tauri/i18n/i18n-src/ja/models.json b/desktop-tauri/i18n/i18n-src/ja/models.json index 00045264f..b1516b31c 100644 --- a/desktop-tauri/i18n/i18n-src/ja/models.json +++ b/desktop-tauri/i18n/i18n-src/ja/models.json @@ -325,6 +325,7 @@ "这家的清单是你自己登记的,不填的项下游按未声明处理。": "このプロバイダーのカタログは自分で登録したものです。未入力の項目はダウンストリームでは未宣言として処理されます。", "这家还没有可用账号,刷新会使用默认登录态": "このプロバイダーにはまだ利用可能なアカウントがありません。更新時はデフォルトのログイン状態を使用します", "这家还没有模型:点「添加模型」登记,或「获取上游模型」从上游拉取": "このプロバイダーにはまだモデルがありません:「モデルを追加」で登録するか、「アップストリームモデルを取得」でアップストリームから取得してください", + "自定义提供商还没有模型:选中具体某一家后点「添加模型」登记,或「获取上游模型」从上游拉取": "カスタムプロバイダーにはまだモデルがありません:先に具体的なプロバイダーを選んでから、「モデルを追加」で登録するか「アップストリームモデルを取得」で取得してください", "这是模型自己能配的档位(来自上游目录),只读;要改某一档请到映射弹窗里选。": "これはモデル自身が設定できるレベルです(アップストリームディレクトリ由来)。読み取り専用です;特定のレベルを変更するにはマッピングダイアログで選択してください。", "这条登记(不像内置模型那样只是隐藏),因为它的存在完全由这次登记决定。": "することを意味します(内蔵モデルのように非表示にするだけではありません)。その存在はこの登録だけで決まるためです。", "远程": "リモート", diff --git a/desktop-tauri/i18n/i18n-src/ko/models.json b/desktop-tauri/i18n/i18n-src/ko/models.json index 182df9a56..ab03fb134 100644 --- a/desktop-tauri/i18n/i18n-src/ko/models.json +++ b/desktop-tauri/i18n/i18n-src/ko/models.json @@ -325,6 +325,7 @@ "这家的清单是你自己登记的,不填的项下游按未声明处理。": "이 제공자의 카탈로그는 직접 등록한 것이며, 비워 둔 항목은 다운스트림에서 미선언으로 처리합니다.", "这家还没有可用账号,刷新会使用默认登录态": "이 제공자에는 아직 사용 가능한 계정이 없어 새로 고침 시 기본 로그인 세션을 사용합니다", "这家还没有模型:点「添加模型」登记,或「获取上游模型」从上游拉取": "이 제공자에는 아직 모델이 없습니다: '모델 추가'를 눌러 등록하거나 '업스트림 모델 가져오기'로 업스트림에서 가져오세요", + "自定义提供商还没有模型:选中具体某一家后点「添加模型」登记,或「获取上游模型」从上游拉取": "커스텀 제공자에는 아직 모델이 없습니다: 먼저 특정 제공자를 선택한 후 '모델 추가'로 등록하거나 '업스트림 모델 가져오기'로 가져오세요", "这是模型自己能配的档位(来自上游目录),只读;要改某一档请到映射弹窗里选。": "이것은 모델 자체가 설정할 수 있는 레벨(업스트림 디렉터리에서 제공)이며 읽기 전용입니다; 특정 레벨을 바꾸려면 매핑 대화상자에서 선택하세요.", "这条登记(不像内置模型那样只是隐藏),因为它的存在完全由这次登记决定。": "하는 것입니다(내장 모델처럼 단순히 숨기는 것이 아닙니다). 이 등록의 존재 여부가 전적으로 이 등록에 달려 있기 때문입니다.", "远程": "원격", diff --git a/desktop-tauri/i18n/i18n-src/pt-BR/models.json b/desktop-tauri/i18n/i18n-src/pt-BR/models.json index a32de8bed..adda1e7a7 100644 --- a/desktop-tauri/i18n/i18n-src/pt-BR/models.json +++ b/desktop-tauri/i18n/i18n-src/pt-BR/models.json @@ -325,6 +325,7 @@ "这家的清单是你自己登记的,不填的项下游按未声明处理。": "O catálogo deste provedor é registrado por você; itens não preenchidos são tratados como não declarados pelo downstream.", "这家还没有可用账号,刷新会使用默认登录态": "Este provedor ainda não tem contas disponíveis; a atualização usará a sessão padrão", "这家还没有模型:点「添加模型」登记,或「获取上游模型」从上游拉取": "Este provedor ainda não tem modelos: clique em “Adicionar modelo” para registrar ou em “Buscar modelos do upstream” para buscá-los", + "自定义提供商还没有模型:选中具体某一家后点「添加模型」登记,或「获取上游模型」从上游拉取": "Os provedores personalizados ainda não têm modelos: selecione um provedor específico e clique em “Adicionar modelo” para registrar ou em “Buscar modelos do upstream” para buscá-los", "这是模型自己能配的档位(来自上游目录),只读;要改某一档请到映射弹窗里选。": "São os níveis que o próprio modelo pode configurar (vêm do catálogo upstream); somente leitura. Para alterar algum nível, escolha no diálogo de mapeamento.", "这条登记(不像内置模型那样只是隐藏),因为它的存在完全由这次登记决定。": " este registro (não apenas ocultá-lo como no caso dos modelos integrados), pois sua existência é determinada inteiramente por este registro.", "远程": "Remoto", diff --git a/desktop-tauri/ui-islands/src/islands/models-page.tsx b/desktop-tauri/ui-islands/src/islands/models-page.tsx index 0396ccbb8..634f61d00 100644 --- a/desktop-tauri/ui-islands/src/islands/models-page.tsx +++ b/desktop-tauri/ui-islands/src/islands/models-page.tsx @@ -122,7 +122,7 @@ import type { ManageModel, ManageView } from './models-custom-source' // 内置家的图标映射(与添加账号弹窗、签到中心同一份,别处不要照抄这份映射) import { PROVIDER_ICONS } from './add-provider-pick' import { - GROUP_LIMIT, MODEL_SORT_OPTIONS, MODEL_STATE_OPTIONS, accept, bindingKeyOf, bindingsOf, builtinRailItems, + CUSTOM_ALL, GROUP_LIMIT, MODEL_SORT_OPTIONS, MODEL_STATE_OPTIONS, accept, bindingKeyOf, bindingsOf, builtinRailItems, closeCapability, closeCustomModel, closeMapping, collapseGroup, currentProvider, customProviderOptions, directoryReady, esc, errorMessage, expandGroup, formatTime, getSnapshot, levelOf, load, models, openAddCustomProvider, openCapability, openCustomModel, openMapping, providerLabelOf, providerOptions, @@ -310,7 +310,9 @@ function matches(model: ManageModel, keyword: string, provider: string, stateFil if (stateFilter === 'enabled' && !rowEnabled(model)) return false if (stateFilter === 'disabled' && rowEnabled(model)) return false if (stateFilter === 'mapped' && !(model.aliases || []).length) return false - if (provider !== 'all' && (model.provider || '') !== provider) return false + // 「自定义 · 全部」视图:数据已经只含自定义家(viewData 合并而来),这里不再按家过滤; + // 选中具体某家(含内置与自定义)时按行上的 provider 精确匹配,与旧实现同口径。 + if (provider !== 'all' && provider !== CUSTOM_ALL && (model.provider || '') !== provider) return false if (!keyword) return true const hay = [model.id, model.name, ...(model.aliases || [])].join(' ').toLowerCase() return hay.includes(keyword) @@ -319,7 +321,9 @@ function matches(model: ManageModel, keyword: string, provider: string, stateFil function ModelsPage() { const state = React.useSyncExternalStore(subscribe, getSnapshot) const provider = resolveProvider(state) - const custom = customSource.isCustom(provider) + /** 「自定义 · 全部」聚合视图(只读浏览,写入仍按行上的 provider 落到具体某一家) */ + const customAll = provider === CUSTOM_ALL + const custom = customAll || customSource.isCustom(provider) /** * 「测试」弹窗的目标行。这一份状态刻意**不放进模块级快照**(与三个弹窗的 context 不同): * 它只被本页用、也没有从 React 之外打开的调用点(入口就是表格里那颗按钮), @@ -337,7 +341,7 @@ function ModelsPage() { */ const [selection, setSelection] = React.useState>(new Set()) /** 选中了一个已被删除的自定义家(目录缓存里确实没有这条记录,而不是「还没加载完」) */ - const customMissing = custom && directoryReady() && !customSource.record(provider) + const customMissing = !customAll && customSource.isCustom(provider) && directoryReady() && !customSource.record(provider) /** * 首屏一次性副作用:登记列设置(并注入按钮)→ 同步静态表头 → 补读筛选记忆 → 按需自拉。 @@ -382,9 +386,11 @@ function ModelsPage() { */ const titleLabel = provider === 'all' ? t('全部') - : (custom - ? String(customSource.record(provider)?.name || provider) - : (builtinRailItems(state.data).get(provider)?.label || providerLabelOf(provider))) + : (customAll + ? t('全部') + : (custom + ? String(customSource.record(provider)?.name || provider) + : (builtinRailItems(state.data).get(provider)?.label || providerLabelOf(provider)))) const enabledCount = all.filter(rowEnabled).length const mappedCount = all.filter(model => (model.aliases || []).length > 0).length /** @@ -424,7 +430,7 @@ function ModelsPage() { }} /> ) - /** 左栏:内置提供商(全部 + 各家)+ 自定义提供商(每家 + 新建)。 + /** 左栏:内置提供商(全部 + 各家)+ 自定义提供商(全部 + 每家 + 新建)。 条目区(.rail-scroll)自己滚,「+ 新建」沉在栏底(.rail-foot)不跟着滚 —— 它是动作不是选项,家多了也不该被推出视野。 */ function rail() { @@ -432,6 +438,11 @@ function ModelsPage() { const customs = customSource.list() const total = [...counts.values()].reduce((sum, item) => sum + item.n, 0) const allLabel = t('全部({n} 家)', { n: counts.size }) + // 「自定义 · 全部」的计数与家数:模型总数是各家 models 长度之和(目录缓存的权威口径, + // 与单家条目上那颗计数同源);只有一家时这项没意义,不渲染(与内置区恒显「全部」不同 —— + // 内置区那份还承担「回到全局视图」的入口,自定义区的「全部」只是多家的聚合浏览) + const customTotal = customs.reduce((sum, item) => sum + (Array.isArray(item.models) ? item.models.length : 0), 0) + const customAllLabel = t('全部({n} 家)', { n: customs.length }) return (