新增: F-01模型拉取IPC+Settings拉取UI(FR-S1密钥内存解析闭环)

This commit is contained in:
2026-06-17 00:14:38 +08:00
parent 8a142c212c
commit a26b86e003
7 changed files with 257 additions and 2 deletions

View File

@@ -92,6 +92,38 @@
<button class="btn btn-primary" @click="saveProvider" :disabled="providerForm.saving">
{{ providerForm.saving ? $t('settings.saving') : $t('common.save') }}
</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>
</section>
@@ -397,7 +429,7 @@ import { aiApi, knowledgeApi } from '@/api'
import { useAppSettingsStore } from '@/stores/appSettings'
import { useAiStore } from '@/stores/ai'
import { useConfirm } from '@/composables/useConfirm'
import type { AiProviderConfig } from '@/api/types'
import type { AiProviderConfig, ModelConfig } from '@/api/types'
import i18n from '@/i18n'
const { t } = useI18n()
@@ -437,6 +469,9 @@ const providerForm = reactive({
apiKey: '',
defaultModel: '',
saving: false,
// F-01 阶段6:模型拉取 + 列表
fetching: false,
models: [] as ModelConfig[],
})
async function loadProviders() {
@@ -458,6 +493,8 @@ function openProviderForm(p?: AiProviderConfig) {
providerForm.apiKey = '' // 编辑不回填 api_key(list 返回 mask,留空=不改,FR-S1)
providerForm.defaultModel = p.default_model
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 {
providerForm.editId = ''
providerForm.name = ''
@@ -465,10 +502,43 @@ function openProviderForm(p?: AiProviderConfig) {
providerForm.baseUrl = ''
providerForm.apiKey = ''
providerForm.defaultModel = ''
providerForm.models = []
}
providerForm.fetching = false
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() {
// apiKey:新建必填;编辑可留空(不改,FR-S1 后端保留原 DB 值)
const needKey = !providerForm.editId