2026-04-13 12:15:16 +08:00
|
|
|
|
<script setup lang="ts">
|
2026-04-19 20:59:25 +08:00
|
|
|
|
import { ref, watch, computed, onMounted } from 'vue'
|
2026-04-26 22:51:35 +08:00
|
|
|
|
import { NModal, NForm, NFormItem, NInput, NInputNumber, NButton, NSelect, NRadioGroup, NRadioButton, useMessage, useDialog } from 'naive-ui'
|
2026-04-16 08:38:18 +08:00
|
|
|
|
import { useModelsStore } from '@/stores/hermes/models'
|
2026-04-13 15:15:14 +08:00
|
|
|
|
import { useI18n } from 'vue-i18n'
|
2026-04-17 23:11:57 +08:00
|
|
|
|
import CodexLoginModal from './CodexLoginModal.vue'
|
2026-04-23 08:39:19 +08:00
|
|
|
|
import NousLoginModal from './NousLoginModal.vue'
|
2026-04-26 22:51:35 +08:00
|
|
|
|
import CopilotLoginModal from './CopilotLoginModal.vue'
|
|
|
|
|
|
import { checkCopilotToken, enableCopilot, type CopilotTokenSource } from '@/api/hermes/copilot-auth'
|
2026-04-13 15:15:14 +08:00
|
|
|
|
|
|
|
|
|
|
const { t } = useI18n()
|
2026-04-13 12:15:16 +08:00
|
|
|
|
|
|
|
|
|
|
const emit = defineEmits<{
|
|
|
|
|
|
close: []
|
|
|
|
|
|
saved: []
|
|
|
|
|
|
}>()
|
|
|
|
|
|
|
|
|
|
|
|
const modelsStore = useModelsStore()
|
|
|
|
|
|
const message = useMessage()
|
2026-04-26 22:51:35 +08:00
|
|
|
|
const dialog = useDialog()
|
2026-04-13 12:15:16 +08:00
|
|
|
|
|
|
|
|
|
|
const showModal = ref(true)
|
|
|
|
|
|
const loading = ref(false)
|
|
|
|
|
|
const fetchingModels = ref(false)
|
2026-04-17 23:11:57 +08:00
|
|
|
|
const showCodexLogin = ref(false)
|
2026-04-23 08:39:19 +08:00
|
|
|
|
const showNousLogin = ref(false)
|
2026-04-26 22:51:35 +08:00
|
|
|
|
const showCopilotLogin = ref(false)
|
|
|
|
|
|
const copilotChecking = ref(false)
|
2026-04-13 12:15:16 +08:00
|
|
|
|
|
|
|
|
|
|
const providerType = ref<'preset' | 'custom'>('preset')
|
|
|
|
|
|
const selectedPreset = ref<string | null>(null)
|
|
|
|
|
|
const formData = ref({
|
|
|
|
|
|
name: '',
|
|
|
|
|
|
base_url: '',
|
|
|
|
|
|
api_key: '',
|
|
|
|
|
|
model: '',
|
2026-04-24 11:18:11 +08:00
|
|
|
|
context_length: null as number | null,
|
2026-04-13 12:15:16 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const modelOptions = ref<Array<{ label: string; value: string }>>([])
|
|
|
|
|
|
|
2026-04-17 23:11:57 +08:00
|
|
|
|
const CODEX_KEY = 'openai-codex'
|
2026-04-23 08:39:19 +08:00
|
|
|
|
const NOUS_KEY = 'nous'
|
2026-04-26 22:51:35 +08:00
|
|
|
|
const COPILOT_KEY = 'copilot'
|
2026-04-25 14:00:07 +08:00
|
|
|
|
const ALIBABA_CODING_KEY = 'alibaba-coding-plan'
|
|
|
|
|
|
const ALIBABA_CODING_REGIONS = {
|
|
|
|
|
|
intl: 'https://coding-intl.dashscope.aliyuncs.com/v1',
|
|
|
|
|
|
cn: 'https://coding.dashscope.aliyuncs.com/v1',
|
|
|
|
|
|
} as const
|
2026-04-17 23:11:57 +08:00
|
|
|
|
|
|
|
|
|
|
const isCodex = computed(() => selectedPreset.value === CODEX_KEY)
|
2026-04-23 08:39:19 +08:00
|
|
|
|
const isNous = computed(() => selectedPreset.value === NOUS_KEY)
|
2026-04-26 22:51:35 +08:00
|
|
|
|
const isCopilot = computed(() => selectedPreset.value === COPILOT_KEY)
|
2026-04-25 14:00:07 +08:00
|
|
|
|
const isAlibabaCoding = computed(() => selectedPreset.value === ALIBABA_CODING_KEY)
|
|
|
|
|
|
const alibabaCodingRegion = ref<'intl' | 'cn'>('intl')
|
2026-04-17 23:11:57 +08:00
|
|
|
|
|
2026-04-19 20:59:25 +08:00
|
|
|
|
const presetOptions = computed(() =>
|
|
|
|
|
|
modelsStore.allProviders.map(g => ({ label: g.label, value: g.provider })),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-04-13 12:15:16 +08:00
|
|
|
|
function autoGenerateName(url: string): string {
|
|
|
|
|
|
const clean = url.replace(/^https?:\/\//, '').replace(/\/v1\/?$/, '')
|
|
|
|
|
|
const host = clean.split('/')[0]
|
|
|
|
|
|
if (host.includes('localhost') || host.includes('127.0.0.1')) {
|
2026-04-15 16:36:04 +08:00
|
|
|
|
return t('models.local', { host })
|
2026-04-13 12:15:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
return host.charAt(0).toUpperCase() + host.slice(1)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
watch(selectedPreset, (val) => {
|
|
|
|
|
|
formData.value.model = ''
|
2026-04-25 14:00:07 +08:00
|
|
|
|
alibabaCodingRegion.value = 'intl'
|
2026-04-13 12:15:16 +08:00
|
|
|
|
if (val) {
|
2026-04-19 20:59:25 +08:00
|
|
|
|
const group = modelsStore.allProviders.find(g => g.provider === val)
|
|
|
|
|
|
if (group) {
|
|
|
|
|
|
formData.value.name = group.label
|
|
|
|
|
|
formData.value.base_url = group.base_url
|
|
|
|
|
|
modelOptions.value = group.models.map((m: string) => ({ label: m, value: m }))
|
|
|
|
|
|
if (group.models.length > 0) {
|
|
|
|
|
|
formData.value.model = group.models[0]
|
2026-04-13 12:15:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-26 22:51:35 +08:00
|
|
|
|
if (val === COPILOT_KEY) {
|
|
|
|
|
|
// 判断是否已能解析到 token:有 → 弹简单确认;无 → 走 in-app device flow
|
|
|
|
|
|
void triggerCopilotAdd()
|
|
|
|
|
|
}
|
2026-04-13 12:15:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-04-25 14:00:07 +08:00
|
|
|
|
watch(alibabaCodingRegion, (region) => {
|
|
|
|
|
|
if (isAlibabaCoding.value) {
|
|
|
|
|
|
formData.value.base_url = ALIBABA_CODING_REGIONS[region]
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-04-13 12:15:16 +08:00
|
|
|
|
watch(() => formData.value.base_url, (url) => {
|
2026-04-21 12:35:48 +08:00
|
|
|
|
if (providerType.value === 'custom' && url.trim() && !formData.value.name) {
|
2026-04-13 12:15:16 +08:00
|
|
|
|
formData.value.name = autoGenerateName(url.trim())
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
watch(providerType, () => {
|
|
|
|
|
|
modelOptions.value = []
|
2026-04-24 11:18:11 +08:00
|
|
|
|
formData.value = { name: '', base_url: '', api_key: '', model: '', context_length: null }
|
2026-04-13 12:15:16 +08:00
|
|
|
|
selectedPreset.value = null
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-04-19 20:59:25 +08:00
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
if (modelsStore.providers.length === 0) {
|
|
|
|
|
|
modelsStore.fetchProviders()
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-04-13 12:15:16 +08:00
|
|
|
|
async function fetchModels() {
|
|
|
|
|
|
const { base_url } = formData.value
|
|
|
|
|
|
if (!base_url.trim()) {
|
2026-04-13 15:15:14 +08:00
|
|
|
|
message.warning(t('models.enterBaseUrl'))
|
2026-04-13 12:15:16 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fetchingModels.value = true
|
|
|
|
|
|
try {
|
2026-04-21 12:35:48 +08:00
|
|
|
|
const base = base_url.replace(/\/+$/, '')
|
2026-04-23 12:57:42 +08:00
|
|
|
|
const url = /\/v\d+\/?$/.test(base) ? `${base}/models` : `${base}/v1/models`
|
2026-04-13 12:15:16 +08:00
|
|
|
|
const headers: Record<string, string> = {}
|
|
|
|
|
|
if (formData.value.api_key.trim()) {
|
|
|
|
|
|
headers['Authorization'] = `Bearer ${formData.value.api_key.trim()}`
|
|
|
|
|
|
}
|
|
|
|
|
|
const res = await fetch(url, { headers, signal: AbortSignal.timeout(8000) })
|
|
|
|
|
|
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
|
|
|
|
const data = await res.json() as { data?: Array<{ id: string }> }
|
2026-04-13 15:15:14 +08:00
|
|
|
|
if (!Array.isArray(data.data)) throw new Error(t('models.unexpectedFormat'))
|
2026-04-13 12:15:16 +08:00
|
|
|
|
|
|
|
|
|
|
modelOptions.value = data.data.map(m => ({ label: m.id, value: m.id }))
|
|
|
|
|
|
if (modelOptions.value.length > 0 && !formData.value.model) {
|
|
|
|
|
|
formData.value.model = modelOptions.value[0].value
|
|
|
|
|
|
}
|
2026-04-13 15:15:14 +08:00
|
|
|
|
message.success(t('models.foundModels', { count: modelOptions.value.length }))
|
2026-04-13 12:15:16 +08:00
|
|
|
|
} catch (e: any) {
|
2026-04-13 15:15:14 +08:00
|
|
|
|
message.error(t('models.fetchFailed') + ': ' + e.message)
|
2026-04-13 12:15:16 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
fetchingModels.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function handleSave() {
|
|
|
|
|
|
if (providerType.value === 'preset' && !selectedPreset.value) {
|
2026-04-13 15:15:14 +08:00
|
|
|
|
message.warning(t('models.selectProviderRequired'))
|
2026-04-13 12:15:16 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-04-17 23:11:57 +08:00
|
|
|
|
|
|
|
|
|
|
// Codex: 弹出授权码弹窗
|
|
|
|
|
|
if (isCodex.value) {
|
|
|
|
|
|
showCodexLogin.value = true
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-23 08:39:19 +08:00
|
|
|
|
// Nous: 弹出 OAuth 设备码弹窗
|
|
|
|
|
|
if (isNous.value) {
|
|
|
|
|
|
showNousLogin.value = true
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-26 22:51:35 +08:00
|
|
|
|
// Copilot: 走 token-aware 的添加流程(已有 token → 确认窗;否则 device flow)
|
|
|
|
|
|
if (isCopilot.value) {
|
|
|
|
|
|
void triggerCopilotAdd()
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-13 12:15:16 +08:00
|
|
|
|
if (!formData.value.base_url.trim()) {
|
2026-04-13 15:15:14 +08:00
|
|
|
|
message.warning(t('models.baseUrlRequired'))
|
2026-04-13 12:15:16 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!formData.value.api_key.trim()) {
|
2026-04-13 15:15:14 +08:00
|
|
|
|
message.warning(t('models.apiKeyRequired'))
|
2026-04-13 12:15:16 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!formData.value.model) {
|
2026-04-13 15:15:14 +08:00
|
|
|
|
message.warning(t('models.modelRequired'))
|
2026-04-13 12:15:16 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
loading.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const providerKey = providerType.value === 'preset'
|
2026-04-19 20:59:25 +08:00
|
|
|
|
? selectedPreset.value
|
2026-04-13 12:15:16 +08:00
|
|
|
|
: null
|
|
|
|
|
|
|
2026-04-24 11:18:11 +08:00
|
|
|
|
const contextLength = formData.value.context_length ?? undefined
|
2026-04-13 12:15:16 +08:00
|
|
|
|
await modelsStore.addProvider({
|
|
|
|
|
|
name: formData.value.name.trim(),
|
|
|
|
|
|
base_url: formData.value.base_url.trim(),
|
|
|
|
|
|
api_key: formData.value.api_key.trim(),
|
|
|
|
|
|
model: formData.value.model,
|
2026-04-24 11:18:11 +08:00
|
|
|
|
context_length: contextLength,
|
2026-04-13 12:15:16 +08:00
|
|
|
|
providerKey,
|
|
|
|
|
|
})
|
2026-04-13 15:15:14 +08:00
|
|
|
|
message.success(t('models.providerAdded'))
|
2026-04-13 12:15:16 +08:00
|
|
|
|
emit('saved')
|
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
message.error(e.message)
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
loading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-17 23:11:57 +08:00
|
|
|
|
async function handleCodexSuccess() {
|
|
|
|
|
|
showCodexLogin.value = false
|
|
|
|
|
|
message.success(t('models.providerAdded'))
|
|
|
|
|
|
emit('saved')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-23 08:39:19 +08:00
|
|
|
|
async function handleNousSuccess() {
|
|
|
|
|
|
showNousLogin.value = false
|
|
|
|
|
|
message.success(t('models.providerAdded'))
|
|
|
|
|
|
emit('saved')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-26 22:51:35 +08:00
|
|
|
|
async function handleCopilotSuccess() {
|
|
|
|
|
|
showCopilotLogin.value = false
|
|
|
|
|
|
message.success(t('models.providerAdded'))
|
|
|
|
|
|
emit('saved')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function copilotSourceLabel(source: CopilotTokenSource): string {
|
|
|
|
|
|
if (source === 'env') return t('models.copilotAddSourceEnv')
|
|
|
|
|
|
if (source === 'gh-cli') return t('models.copilotAddSourceGhCli')
|
|
|
|
|
|
if (source === 'apps-json') return t('models.copilotAddSourceAppsJson')
|
|
|
|
|
|
return ''
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function triggerCopilotAdd() {
|
|
|
|
|
|
if (copilotChecking.value) return
|
|
|
|
|
|
copilotChecking.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const status = await checkCopilotToken()
|
|
|
|
|
|
if (status.has_token) {
|
|
|
|
|
|
// 已能解析到 token:弹确认窗,用户点 [添加] → enable + saved
|
|
|
|
|
|
const sourceText = copilotSourceLabel(status.source)
|
|
|
|
|
|
dialog.success({
|
|
|
|
|
|
title: t('models.copilotAddDetectedTitle'),
|
|
|
|
|
|
content: sourceText
|
|
|
|
|
|
? `${t('models.copilotAddDetected')}\n\n${sourceText}`
|
|
|
|
|
|
: t('models.copilotAddDetected'),
|
|
|
|
|
|
positiveText: t('common.add'),
|
|
|
|
|
|
negativeText: t('common.cancel'),
|
|
|
|
|
|
onPositiveClick: async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await enableCopilot()
|
|
|
|
|
|
message.success(t('models.providerAdded'))
|
|
|
|
|
|
emit('saved')
|
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
message.error(e?.message ?? String(e))
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
onNegativeClick: () => {
|
|
|
|
|
|
selectedPreset.value = null
|
|
|
|
|
|
},
|
|
|
|
|
|
onClose: () => {
|
|
|
|
|
|
selectedPreset.value = null
|
|
|
|
|
|
},
|
|
|
|
|
|
})
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 无 token:device flow
|
|
|
|
|
|
showCopilotLogin.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
message.error(e?.message ?? String(e))
|
|
|
|
|
|
selectedPreset.value = null
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
copilotChecking.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleCopilotClose() {
|
|
|
|
|
|
showCopilotLogin.value = false
|
|
|
|
|
|
// 用户取消 Copilot 引导时,清空选择避免卡在无 api_key 状态
|
|
|
|
|
|
selectedPreset.value = null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-13 12:15:16 +08:00
|
|
|
|
function handleClose() {
|
|
|
|
|
|
showModal.value = false
|
|
|
|
|
|
setTimeout(() => emit('close'), 200)
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
|
<NModal
|
|
|
|
|
|
v-model:show="showModal"
|
|
|
|
|
|
preset="card"
|
2026-04-13 15:15:14 +08:00
|
|
|
|
:title="t('models.addProvider')"
|
2026-04-15 09:12:54 +08:00
|
|
|
|
:style="{ width: 'min(520px, calc(100vw - 32px))' }"
|
2026-04-26 22:51:35 +08:00
|
|
|
|
:mask-closable="!loading && !showCodexLogin && !showNousLogin && !showCopilotLogin"
|
2026-04-13 12:15:16 +08:00
|
|
|
|
@after-leave="emit('close')"
|
|
|
|
|
|
>
|
|
|
|
|
|
<NForm label-placement="top">
|
2026-04-13 15:15:14 +08:00
|
|
|
|
<NFormItem :label="t('models.providerType')">
|
2026-04-13 12:15:16 +08:00
|
|
|
|
<div style="display: flex; gap: 12px">
|
|
|
|
|
|
<NButton
|
|
|
|
|
|
:type="providerType === 'preset' ? 'primary' : 'default'"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
@click="providerType = 'preset'"
|
|
|
|
|
|
>
|
2026-04-13 15:15:14 +08:00
|
|
|
|
{{ t('models.preset') }}
|
2026-04-13 12:15:16 +08:00
|
|
|
|
</NButton>
|
|
|
|
|
|
<NButton
|
|
|
|
|
|
:type="providerType === 'custom' ? 'primary' : 'default'"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
@click="providerType = 'custom'"
|
|
|
|
|
|
>
|
2026-04-13 15:15:14 +08:00
|
|
|
|
{{ t('models.custom') }}
|
2026-04-13 12:15:16 +08:00
|
|
|
|
</NButton>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</NFormItem>
|
|
|
|
|
|
|
2026-04-13 15:15:14 +08:00
|
|
|
|
<NFormItem v-if="providerType === 'preset'" :label="t('models.selectProvider')" required>
|
2026-04-13 12:15:16 +08:00
|
|
|
|
<NSelect
|
|
|
|
|
|
v-model:value="selectedPreset"
|
2026-04-19 20:59:25 +08:00
|
|
|
|
:options="presetOptions"
|
2026-04-13 15:15:14 +08:00
|
|
|
|
:placeholder="t('models.chooseProvider')"
|
2026-04-13 12:15:16 +08:00
|
|
|
|
filterable
|
|
|
|
|
|
/>
|
|
|
|
|
|
</NFormItem>
|
|
|
|
|
|
|
2026-04-13 15:15:14 +08:00
|
|
|
|
<NFormItem v-if="providerType === 'custom'" :label="t('models.name')">
|
2026-04-13 12:15:16 +08:00
|
|
|
|
<NInput
|
|
|
|
|
|
v-model:value="formData.name"
|
2026-04-13 15:15:14 +08:00
|
|
|
|
:placeholder="t('models.autoGeneratedName')"
|
2026-04-13 12:15:16 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</NFormItem>
|
|
|
|
|
|
|
2026-04-25 14:00:07 +08:00
|
|
|
|
<NFormItem v-if="isAlibabaCoding" :label="t('models.region')">
|
|
|
|
|
|
<NRadioGroup v-model:value="alibabaCodingRegion">
|
|
|
|
|
|
<NRadioButton value="intl">{{ t('models.regionIntl') }}</NRadioButton>
|
|
|
|
|
|
<NRadioButton value="cn">{{ t('models.regionCn') }}</NRadioButton>
|
|
|
|
|
|
</NRadioGroup>
|
|
|
|
|
|
</NFormItem>
|
|
|
|
|
|
|
2026-04-23 08:39:19 +08:00
|
|
|
|
<NFormItem v-if="!isCodex && !isNous" :label="t('models.baseUrl')" required>
|
2026-04-13 12:15:16 +08:00
|
|
|
|
<NInput
|
|
|
|
|
|
v-model:value="formData.base_url"
|
2026-04-13 15:15:14 +08:00
|
|
|
|
:placeholder="t('models.baseUrlPlaceholder')"
|
2026-04-13 12:15:16 +08:00
|
|
|
|
:disabled="providerType === 'preset'"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</NFormItem>
|
|
|
|
|
|
|
2026-04-23 08:39:19 +08:00
|
|
|
|
<NFormItem v-if="!isCodex && !isNous" :label="t('models.apiKey')" required>
|
2026-04-13 12:15:16 +08:00
|
|
|
|
<NInput
|
|
|
|
|
|
v-model:value="formData.api_key"
|
|
|
|
|
|
type="password"
|
|
|
|
|
|
show-password-on="click"
|
2026-04-13 15:15:14 +08:00
|
|
|
|
:placeholder="t('models.apiKeyPlaceholder')"
|
2026-04-17 23:11:57 +08:00
|
|
|
|
autocomplete="off"
|
2026-04-13 12:15:16 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</NFormItem>
|
|
|
|
|
|
|
2026-04-13 15:15:14 +08:00
|
|
|
|
<NFormItem :label="t('models.defaultModel')" required>
|
2026-04-13 12:15:16 +08:00
|
|
|
|
<div style="display: flex; gap: 8px; width: 100%">
|
|
|
|
|
|
<NSelect
|
|
|
|
|
|
v-model:value="formData.model"
|
|
|
|
|
|
:options="modelOptions"
|
|
|
|
|
|
filterable
|
2026-04-17 22:05:06 +08:00
|
|
|
|
tag
|
|
|
|
|
|
:placeholder="t('models.selectOrInput')"
|
2026-04-13 12:15:16 +08:00
|
|
|
|
style="flex: 1"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<NButton
|
|
|
|
|
|
v-if="providerType === 'custom' || (providerType === 'preset' && modelOptions.length === 0)"
|
|
|
|
|
|
:loading="fetchingModels"
|
|
|
|
|
|
@click="fetchModels"
|
|
|
|
|
|
>
|
2026-04-13 15:15:14 +08:00
|
|
|
|
{{ t('common.fetch') }}
|
2026-04-13 12:15:16 +08:00
|
|
|
|
</NButton>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</NFormItem>
|
2026-04-24 11:18:11 +08:00
|
|
|
|
|
|
|
|
|
|
<NFormItem v-if="providerType === 'custom'" :label="t('models.contextLength')">
|
|
|
|
|
|
<NInputNumber
|
|
|
|
|
|
v-model:value="formData.context_length as number | null"
|
|
|
|
|
|
:placeholder="t('models.contextLengthPlaceholder')"
|
|
|
|
|
|
:min="0"
|
|
|
|
|
|
clearable
|
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</NFormItem>
|
2026-04-13 12:15:16 +08:00
|
|
|
|
</NForm>
|
|
|
|
|
|
|
|
|
|
|
|
<template #footer>
|
|
|
|
|
|
<div class="modal-footer">
|
2026-04-13 15:15:14 +08:00
|
|
|
|
<NButton @click="handleClose">{{ t('common.cancel') }}</NButton>
|
2026-04-13 12:15:16 +08:00
|
|
|
|
<NButton type="primary" :loading="loading" @click="handleSave">
|
2026-04-13 15:15:14 +08:00
|
|
|
|
{{ t('common.add') }}
|
2026-04-13 12:15:16 +08:00
|
|
|
|
</NButton>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
2026-04-17 23:11:57 +08:00
|
|
|
|
|
|
|
|
|
|
<CodexLoginModal
|
|
|
|
|
|
v-if="showCodexLogin"
|
|
|
|
|
|
@close="showCodexLogin = false"
|
|
|
|
|
|
@success="handleCodexSuccess"
|
|
|
|
|
|
/>
|
2026-04-23 08:39:19 +08:00
|
|
|
|
|
|
|
|
|
|
<NousLoginModal
|
|
|
|
|
|
v-if="showNousLogin"
|
|
|
|
|
|
@close="showNousLogin = false"
|
|
|
|
|
|
@success="handleNousSuccess"
|
|
|
|
|
|
/>
|
2026-04-26 22:51:35 +08:00
|
|
|
|
|
|
|
|
|
|
<CopilotLoginModal
|
|
|
|
|
|
v-if="showCopilotLogin"
|
|
|
|
|
|
@close="handleCopilotClose"
|
|
|
|
|
|
@success="handleCopilotSuccess"
|
|
|
|
|
|
/>
|
2026-04-13 12:15:16 +08:00
|
|
|
|
</NModal>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
|
|
|
.modal-footer {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|