新增: F-01模型拉取IPC+Settings拉取UI(FR-S1密钥内存解析闭环)
This commit is contained in:
@@ -6,6 +6,8 @@ use serde::Serialize;
|
|||||||
use tauri::{AppHandle, Emitter, State};
|
use tauri::{AppHandle, Emitter, State};
|
||||||
|
|
||||||
use df_ai::provider::ChatMessage;
|
use df_ai::provider::ChatMessage;
|
||||||
|
// df-ai 重导出 df_ai_core(供下游直接引用 trait/类型);src-tauri 不直接依赖 df-ai-core crate。
|
||||||
|
use df_ai::df_ai_core::model::ModelConfig;
|
||||||
use df_core::types::new_id;
|
use df_core::types::new_id;
|
||||||
use df_storage::models::AiProviderRecord;
|
use df_storage::models::AiProviderRecord;
|
||||||
|
|
||||||
@@ -827,6 +829,78 @@ pub async fn ai_delete_provider(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 模型列表拉取 + 单模型探测(F-01 阶段5 IPC)
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
/// 将前端 provider_type 规范化为 fetch_and_probe 接受的类型。
|
||||||
|
///
|
||||||
|
/// Settings.vue 存的 provider_type 是 "openai_compat" / "anthropic"(对齐 build_provider 工厂),
|
||||||
|
/// 而 model_fetch::fetch_and_probe 分派用 "openai_compat" / "anthropic_compat"。
|
||||||
|
/// 两个工厂入口类型语义一致(anthropic 协议),仅命名不同,这里收敛归一。
|
||||||
|
fn normalize_provider_type_for_fetch(provider_type: &str) -> String {
|
||||||
|
match provider_type {
|
||||||
|
"anthropic" => "anthropic_compat".to_string(),
|
||||||
|
other => other.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 测试连接并拉取厂商模型列表(F-01 阶段5)
|
||||||
|
///
|
||||||
|
/// 流程:
|
||||||
|
/// 1. DB 取 AiProviderRecord(get_by_id)
|
||||||
|
/// 2. FR-S1:经 resolve_provider_secret 内存解析真实 api_key(keyring 优先 fallback DB,
|
||||||
|
/// 绝不进日志/返回值/错误信息)
|
||||||
|
/// 3. provider_type 归一(anthropic→anthropic_compat)+ base_url + api_key 调
|
||||||
|
/// df_ai::model_fetch::fetch_and_probe → Vec<ModelConfig>(每个模型名已探测出 4 维度)
|
||||||
|
/// 4. 写回 AiProviderRecord.model_configs(update_full)→ 返回 Vec<ModelConfig>
|
||||||
|
///
|
||||||
|
/// ModelConfig 本就无 api_key 字段,返回值天然不含密钥(FR-S1 闭环)。
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn ai_fetch_models(
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
provider_id: String,
|
||||||
|
) -> Result<Vec<ModelConfig>, String> {
|
||||||
|
let provider = state
|
||||||
|
.ai_providers
|
||||||
|
.get_by_id(&provider_id)
|
||||||
|
.await
|
||||||
|
.map_err(err_str)?
|
||||||
|
.ok_or_else(|| format!("提供商不存在: {}", provider_id))?;
|
||||||
|
|
||||||
|
// FR-S1:内存解析密钥,绝不外泄(不入日志/返回值/错误信息)
|
||||||
|
let api_key = df_storage::secret::resolve_provider_secret(&provider);
|
||||||
|
let fetch_type = normalize_provider_type_for_fetch(&provider.provider_type);
|
||||||
|
|
||||||
|
let configs = df_ai::model_fetch::fetch_and_probe(&fetch_type, &provider.base_url, &api_key)
|
||||||
|
.await
|
||||||
|
.map_err(err_str)?;
|
||||||
|
|
||||||
|
// 写回 model_configs(更新 updated_at)
|
||||||
|
let mut updated = provider.clone();
|
||||||
|
updated.model_configs = configs.clone();
|
||||||
|
updated.updated_at = now_millis();
|
||||||
|
state
|
||||||
|
.ai_providers
|
||||||
|
.update_full(&updated)
|
||||||
|
.await
|
||||||
|
.map_err(err_str)?;
|
||||||
|
|
||||||
|
Ok(configs)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 单模型探测(F-01 阶段5):纯 CPU 启发式 + 预设表,无网络。
|
||||||
|
///
|
||||||
|
/// 用途:拉取后用户手动补一个模型名、或想重探某模型的能力维度。
|
||||||
|
/// 直接调 df_ai::model_probe::probe(&model_id),返回填充了 probe_source 的 ModelConfig。
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn ai_probe_model(
|
||||||
|
_state: State<'_, AppState>,
|
||||||
|
model_id: String,
|
||||||
|
) -> Result<ModelConfig, String> {
|
||||||
|
Ok(df_ai::model_probe::probe(&model_id))
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 对话管理
|
// 对话管理
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|||||||
@@ -122,6 +122,9 @@ pub fn run() {
|
|||||||
commands::ai::ai_save_provider,
|
commands::ai::ai_save_provider,
|
||||||
commands::ai::ai_set_provider,
|
commands::ai::ai_set_provider,
|
||||||
commands::ai::ai_delete_provider,
|
commands::ai::ai_delete_provider,
|
||||||
|
// F-01 阶段5:测试连接拉取模型列表 + 单模型探测
|
||||||
|
commands::ai::ai_fetch_models,
|
||||||
|
commands::ai::ai_probe_model,
|
||||||
// AI 对话管理
|
// AI 对话管理
|
||||||
commands::ai::ai_conversation_create,
|
commands::ai::ai_conversation_create,
|
||||||
commands::ai::ai_conversation_list,
|
commands::ai::ai_conversation_list,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { invoke } from '@tauri-apps/api/core'
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
import { listen, type UnlistenFn } from '@tauri-apps/api/event'
|
import { listen, type UnlistenFn } from '@tauri-apps/api/event'
|
||||||
import type { AiChatEvent, AiConversationDetail, AiConversationSummary, AiProviderConfig, SkillInfo } from './types'
|
import type { AiChatEvent, AiConversationDetail, AiConversationSummary, AiProviderConfig, ModelConfig, SkillInfo } from './types'
|
||||||
|
|
||||||
export const aiApi = {
|
export const aiApi = {
|
||||||
/** 发送消息(非阻塞,通过 ai-chat-event 流式返回);skill 传技能名则注入其 SKILL.md */
|
/** 发送消息(非阻塞,通过 ai-chat-event 流式返回);skill 传技能名则注入其 SKILL.md */
|
||||||
@@ -105,6 +105,25 @@ export const aiApi = {
|
|||||||
return invoke('ai_delete_provider', { providerId })
|
return invoke('ai_delete_provider', { providerId })
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试连接并拉取厂商模型列表(F-01 阶段5)。
|
||||||
|
* 后端:DB 取 provider → FR-S1 内存解析 api_key → fetch_and_probe
|
||||||
|
* (网络拉模型名 + 探测 4 维度)→ 写回 model_configs → 返回。
|
||||||
|
* 返回值 ModelConfig 不含 api_key(FR-S1 闭环)。
|
||||||
|
* 需已落库的 providerId(新建态先保存再拉取)。
|
||||||
|
*/
|
||||||
|
fetchModels(providerId: string): Promise<ModelConfig[]> {
|
||||||
|
return invoke('ai_fetch_models', { providerId })
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单模型探测(F-01 阶段5):纯 CPU 启发式 + 预设表,无网络。
|
||||||
|
* 返回填充了 probe_source 的 ModelConfig。用途:手动补模型名后探测能力维度。
|
||||||
|
*/
|
||||||
|
probeModel(modelId: string): Promise<ModelConfig> {
|
||||||
|
return invoke('ai_probe_model', { modelId })
|
||||||
|
},
|
||||||
|
|
||||||
/** 设置 LLM 调用并发上限(全局 / 单对话,运行时即时生效;软收敛不中断进行中调用) */
|
/** 设置 LLM 调用并发上限(全局 / 单对话,运行时即时生效;软收敛不中断进行中调用) */
|
||||||
setConcurrencyConfig(globalLimit: number, perConvLimit: number): Promise<void> {
|
setConcurrencyConfig(globalLimit: number, perConvLimit: number): Promise<void> {
|
||||||
return invoke('ai_set_concurrency_config', { globalLimit, perConvLimit })
|
return invoke('ai_set_concurrency_config', { globalLimit, perConvLimit })
|
||||||
|
|||||||
@@ -181,12 +181,57 @@ export interface AiProviderConfig {
|
|||||||
base_url: string
|
base_url: string
|
||||||
default_model: string
|
default_model: string
|
||||||
models: string | null
|
models: string | null
|
||||||
|
/** 模型能力配置数组(F-01:4 维度 + 路由控制;DB TEXT 列 JSON 解析,老库可能缺失) */
|
||||||
|
model_configs?: ModelConfig[]
|
||||||
is_default: boolean
|
is_default: boolean
|
||||||
config: string | null
|
config: string | null
|
||||||
created_at: string
|
created_at: string
|
||||||
updated_at: string
|
updated_at: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// F-01 模型能力系统(ModelConfig,与 crates/df-ai-core/src/model.rs 严格对齐)
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
/** 维度 1 — 模态(模型能接收什么输入)。serde snake_case 字面量。 */
|
||||||
|
export type Modality = 'text' | 'vision' | 'audio' | 'video'
|
||||||
|
|
||||||
|
/** 维度 2 — 能力(模型能做什么)。serde snake_case 字面量。 */
|
||||||
|
export type Capability = 'tool_use' | 'embedding' | 'code_gen'
|
||||||
|
|
||||||
|
/** 维度 3 — 价格分级(成本)。serde snake_case 字面量。 */
|
||||||
|
export type CostTier = 'free' | 'low' | 'medium' | 'high'
|
||||||
|
|
||||||
|
/** 维度 4 — 智力分级。serde snake_case 字面量。 */
|
||||||
|
export type IntelligenceTier = 'lite' | 'standard' | 'plus' | 'ultra'
|
||||||
|
|
||||||
|
/** 探测来源(只读,由后端探测器填充)。serde snake_case 字面量。 */
|
||||||
|
export type ProbeSource = 'user_set' | 'preset_table' | 'heuristic' | 'default'
|
||||||
|
|
||||||
|
/** 单模型完整配置(4 维度 + 路由控制 + 探测元数据)。 */
|
||||||
|
export interface ModelConfig {
|
||||||
|
/** 模型名(如 "glm-4v-flash") */
|
||||||
|
model_id: string
|
||||||
|
/** 启用/禁用(false=配了但不参与路由) */
|
||||||
|
enabled: boolean
|
||||||
|
/** 用户自定义别名(可选) */
|
||||||
|
label?: string
|
||||||
|
/** 模态集合 */
|
||||||
|
modalities: Modality[]
|
||||||
|
/** 能力集合 */
|
||||||
|
capabilities: Capability[]
|
||||||
|
/** 价格分级 */
|
||||||
|
cost_tier: CostTier
|
||||||
|
/** 智力分级 */
|
||||||
|
intelligence: IntelligenceTier
|
||||||
|
/** 权重(0-100,同能力候选中优先选权重高的) */
|
||||||
|
weight: number
|
||||||
|
/** 上下文窗口大小(tokens) */
|
||||||
|
context_window: number
|
||||||
|
/** 探测来源(可选) */
|
||||||
|
probe_source?: ProbeSource
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AI 错误分类字面量集(B-260615-42)。
|
* AI 错误分类字面量集(B-260615-42)。
|
||||||
* 与后端 `commands::ai::ErrorType` 的 serde(rename_all = "snake_case") 严格对齐。
|
* 与后端 `commands::ai::ErrorType` 的 serde(rename_all = "snake_case") 严格对齐。
|
||||||
|
|||||||
@@ -29,6 +29,28 @@ export default {
|
|||||||
providerTypeAnthropic: 'Anthropic-compatible (GLM subscription / Claude)',
|
providerTypeAnthropic: 'Anthropic-compatible (GLM subscription / Claude)',
|
||||||
saving: 'Saving...',
|
saving: 'Saving...',
|
||||||
|
|
||||||
|
// Model fetch (F-01 phase 6)
|
||||||
|
testConnection: 'Test connection & fetch models',
|
||||||
|
fetchingModels: 'Fetching...',
|
||||||
|
fetchHintSaveFirst: 'Save the provider first, then fetch models',
|
||||||
|
labelWeight: 'Weight',
|
||||||
|
modelListTitle: 'Fetched models ({count})',
|
||||||
|
modelListHint: 'Enabled / weight are session-local edits; the next "Test connection" overwrites with fresh probe results',
|
||||||
|
saveModels: 'Save model config',
|
||||||
|
toastFetchOk: 'Fetched {count} models',
|
||||||
|
toastFetchFail: 'Fetch failed: {msg}',
|
||||||
|
fetchFailedTimeout: 'Fetch timed out, check that the Base URL is reachable: {msg}',
|
||||||
|
fetchFailedAuth: 'Authentication failed, check your API Key: {msg}',
|
||||||
|
fetchFailedNotFound: 'Endpoint not found, this vendor may not support model listing: {msg}',
|
||||||
|
toastModelEditLocalOnly: 'Enabled / weight are local edits, overwritten on next fetch',
|
||||||
|
|
||||||
|
// Model trait tags (4 dimensions + probe source)
|
||||||
|
tagModality: { text: 'Text', vision: 'Vision', audio: 'Audio', video: 'Video' },
|
||||||
|
tagCapability: { tool_use: 'Tool use', embedding: 'Embedding', code_gen: 'Code' },
|
||||||
|
tagCost: { free: 'Free', low: 'Low', medium: 'Medium', high: 'High' },
|
||||||
|
tagIntel: { lite: 'Lite', standard: 'Standard', plus: 'Plus', ultra: 'Ultra' },
|
||||||
|
tagSource: { user_set: 'Manual', preset_table: 'Preset', heuristic: 'Heuristic', default: 'Default' },
|
||||||
|
|
||||||
// ===== Connection panel =====
|
// ===== Connection panel =====
|
||||||
panelConnection: '🔗 Connections',
|
panelConnection: '🔗 Connections',
|
||||||
addConnection: '+ Add connection',
|
addConnection: '+ Add connection',
|
||||||
|
|||||||
@@ -29,6 +29,28 @@ export default {
|
|||||||
providerTypeAnthropic: 'Anthropic 兼容(GLM 订阅 / Claude)',
|
providerTypeAnthropic: 'Anthropic 兼容(GLM 订阅 / Claude)',
|
||||||
saving: '保存中...',
|
saving: '保存中...',
|
||||||
|
|
||||||
|
// 模型拉取(F-01 阶段6)
|
||||||
|
testConnection: '测试连接并拉取模型',
|
||||||
|
fetchingModels: '拉取中...',
|
||||||
|
fetchHintSaveFirst: '请先保存 Provider 后再拉取模型',
|
||||||
|
labelWeight: '权重',
|
||||||
|
modelListTitle: '已拉取模型({count} 个)',
|
||||||
|
modelListHint: '已启用 / 权重为本次会话内调整,下次「测试连接」会以最新探测结果覆盖',
|
||||||
|
saveModels: '保存模型配置',
|
||||||
|
toastFetchOk: '已拉取 {count} 个模型',
|
||||||
|
toastFetchFail: '拉取模型失败:{msg}',
|
||||||
|
fetchFailedTimeout: '拉取超时,请检查 Base URL 是否可达:{msg}',
|
||||||
|
fetchFailedAuth: '鉴权失败,请检查 API Key:{msg}',
|
||||||
|
fetchFailedNotFound: '端点不存在,该厂商可能不支持模型列表拉取:{msg}',
|
||||||
|
toastModelEditLocalOnly: '已启用 / 权重为本地编辑,下次拉取时覆盖',
|
||||||
|
|
||||||
|
// 模型特征标签(4 维度 + 探测来源)
|
||||||
|
tagModality: { text: '文本', vision: '视觉', audio: '音频', video: '视频' },
|
||||||
|
tagCapability: { tool_use: '工具调用', embedding: '向量', code_gen: '代码' },
|
||||||
|
tagCost: { free: '免费', low: '低价', medium: '中价', high: '高价' },
|
||||||
|
tagIntel: { lite: '轻量', standard: '标准', plus: '增强', ultra: '旗舰' },
|
||||||
|
tagSource: { user_set: '手动', preset_table: '预设', heuristic: '推断', default: '默认' },
|
||||||
|
|
||||||
// ===== 连接管理面板 =====
|
// ===== 连接管理面板 =====
|
||||||
panelConnection: '🔗 连接管理',
|
panelConnection: '🔗 连接管理',
|
||||||
addConnection: '+ 添加连接',
|
addConnection: '+ 添加连接',
|
||||||
|
|||||||
@@ -92,6 +92,38 @@
|
|||||||
<button class="btn btn-primary" @click="saveProvider" :disabled="providerForm.saving">
|
<button class="btn btn-primary" @click="saveProvider" :disabled="providerForm.saving">
|
||||||
{{ providerForm.saving ? $t('settings.saving') : $t('common.save') }}
|
{{ providerForm.saving ? $t('settings.saving') : $t('common.save') }}
|
||||||
</button>
|
</button>
|
||||||
|
<button class="btn btn-ghost" @click="fetchModels" :disabled="providerForm.fetching || !providerForm.editId">
|
||||||
|
{{ providerForm.fetching ? $t('settings.fetchingModels') : $t('settings.testConnection') }}
|
||||||
|
</button>
|
||||||
|
<span v-if="!providerForm.editId" class="form-hint">{{ $t('settings.fetchHintSaveFirst') }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ═══ 拉取到的模型列表(F-01 阶段6)═══ -->
|
||||||
|
<div class="form-field form-field--full" v-if="providerForm.models.length > 0">
|
||||||
|
<label class="form-label">{{ $t('settings.modelListTitle', { count: providerForm.models.length }) }}</label>
|
||||||
|
<div class="model-list">
|
||||||
|
<div class="model-row" v-for="m in providerForm.models" :key="m.model_id">
|
||||||
|
<div class="model-row-main">
|
||||||
|
<label class="toggle toggle--sm">
|
||||||
|
<input type="checkbox" v-model="m.enabled" />
|
||||||
|
<span class="toggle-slider"></span>
|
||||||
|
</label>
|
||||||
|
<span class="model-id">{{ m.model_id }}</span>
|
||||||
|
<div class="model-tags">
|
||||||
|
<span v-for="md in m.modalities" :key="'md-' + md" class="tag tag-modality">{{ $t('settings.tagModality.' + md) }}</span>
|
||||||
|
<span v-for="cap in m.capabilities" :key="'cap-' + cap" class="tag tag-capability">{{ $t('settings.tagCapability.' + cap) }}</span>
|
||||||
|
<span class="tag tag-cost" :class="'tag-cost--' + m.cost_tier">{{ $t('settings.tagCost.' + m.cost_tier) }}</span>
|
||||||
|
<span class="tag tag-intel" :class="'tag-intel--' + m.intelligence">{{ $t('settings.tagIntel.' + m.intelligence) }}</span>
|
||||||
|
<span v-if="m.probe_source" class="tag tag-source">{{ $t('settings.tagSource.' + m.probe_source) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="model-row-side">
|
||||||
|
<label class="model-weight-label">{{ $t('settings.labelWeight') }}</label>
|
||||||
|
<input type="number" class="setting-input setting-input--sm" min="0" max="100" v-model.number="m.weight" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="model-list-hint">{{ $t('settings.modelListHint') }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -397,7 +429,7 @@ import { aiApi, knowledgeApi } from '@/api'
|
|||||||
import { useAppSettingsStore } from '@/stores/appSettings'
|
import { useAppSettingsStore } from '@/stores/appSettings'
|
||||||
import { useAiStore } from '@/stores/ai'
|
import { useAiStore } from '@/stores/ai'
|
||||||
import { useConfirm } from '@/composables/useConfirm'
|
import { useConfirm } from '@/composables/useConfirm'
|
||||||
import type { AiProviderConfig } from '@/api/types'
|
import type { AiProviderConfig, ModelConfig } from '@/api/types'
|
||||||
import i18n from '@/i18n'
|
import i18n from '@/i18n'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@@ -437,6 +469,9 @@ const providerForm = reactive({
|
|||||||
apiKey: '',
|
apiKey: '',
|
||||||
defaultModel: '',
|
defaultModel: '',
|
||||||
saving: false,
|
saving: false,
|
||||||
|
// F-01 阶段6:模型拉取 + 列表
|
||||||
|
fetching: false,
|
||||||
|
models: [] as ModelConfig[],
|
||||||
})
|
})
|
||||||
|
|
||||||
async function loadProviders() {
|
async function loadProviders() {
|
||||||
@@ -458,6 +493,8 @@ function openProviderForm(p?: AiProviderConfig) {
|
|||||||
providerForm.apiKey = '' // 编辑不回填 api_key(list 返回 mask,留空=不改,FR-S1)
|
providerForm.apiKey = '' // 编辑不回填 api_key(list 返回 mask,留空=不改,FR-S1)
|
||||||
providerForm.defaultModel = p.default_model
|
providerForm.defaultModel = p.default_model
|
||||||
providerForm.providerType = p.provider_type || 'openai_compat'
|
providerForm.providerType = p.provider_type || 'openai_compat'
|
||||||
|
// F-01 阶段6:回填已保存的 model_configs(深拷贝避免双向绑定污染列表源)
|
||||||
|
providerForm.models = (p.model_configs ?? []).map(m => ({ ...m, modalities: [...m.modalities], capabilities: [...m.capabilities] }))
|
||||||
} else {
|
} else {
|
||||||
providerForm.editId = ''
|
providerForm.editId = ''
|
||||||
providerForm.name = ''
|
providerForm.name = ''
|
||||||
@@ -465,10 +502,43 @@ function openProviderForm(p?: AiProviderConfig) {
|
|||||||
providerForm.baseUrl = ''
|
providerForm.baseUrl = ''
|
||||||
providerForm.apiKey = ''
|
providerForm.apiKey = ''
|
||||||
providerForm.defaultModel = ''
|
providerForm.defaultModel = ''
|
||||||
|
providerForm.models = []
|
||||||
}
|
}
|
||||||
|
providerForm.fetching = false
|
||||||
providerForm.visible = true
|
providerForm.visible = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试连接并拉取模型列表(F-01 阶段6)。
|
||||||
|
* 后端 ai_fetch_models:DB 取 provider + 内存解析 api_key(FR-S1)+ 网络拉取 + 探测 4 维度 + 写回。
|
||||||
|
* 新建态(editId 空)无 provider_id → 按钮已 disabled,提示先保存。
|
||||||
|
*
|
||||||
|
* enabled toggle / weight input 为本地会话内编辑(仅本批展示交互,持久化另见 issues);
|
||||||
|
* 下次「测试连接」会以最新探测结果覆盖 model_configs 落库。
|
||||||
|
*/
|
||||||
|
async function fetchModels() {
|
||||||
|
if (!providerForm.editId) {
|
||||||
|
showToast(t('settings.fetchHintSaveFirst'), 'warning')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
providerForm.fetching = true
|
||||||
|
try {
|
||||||
|
const models = await aiApi.fetchModels(providerForm.editId)
|
||||||
|
providerForm.models = models.map(m => ({ ...m, modalities: [...m.modalities], capabilities: [...m.capabilities] }))
|
||||||
|
showToast(t('settings.toastFetchOk', { count: models.length }), 'info')
|
||||||
|
} catch (e) {
|
||||||
|
const msg = errMsg(e)
|
||||||
|
// 按 model_fetch 错误映射友好提示:超时 / 鉴权失败 / 端点不存在 / 厂商服务异常 / 不支持
|
||||||
|
let key = 'settings.toastFetchFail'
|
||||||
|
if (msg.includes('超时')) key = 'settings.fetchFailedTimeout'
|
||||||
|
else if (msg.includes('鉴权失败')) key = 'settings.fetchFailedAuth'
|
||||||
|
else if (msg.includes('端点不存在') || msg.includes('404')) key = 'settings.fetchFailedNotFound'
|
||||||
|
showToast(t(key, { msg }), 'error')
|
||||||
|
} finally {
|
||||||
|
providerForm.fetching = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function saveProvider() {
|
async function saveProvider() {
|
||||||
// apiKey:新建必填;编辑可留空(不改,FR-S1 后端保留原 DB 值)
|
// apiKey:新建必填;编辑可留空(不改,FR-S1 后端保留原 DB 值)
|
||||||
const needKey = !providerForm.editId
|
const needKey = !providerForm.editId
|
||||||
|
|||||||
Reference in New Issue
Block a user