优化 API 配置:一个弹窗完成接口、Key 与模型选择

- 去掉预设供应商,直接选择接口类型并填写 Base URL、API Key
- 可在保存前从接口拉取模型,搜索并勾选多个;也可手动输入模型名
- 适配中转站:漏写 /v1(Gemini 为 /v1beta)时自动补全,同时发送 Bearer 与官方鉴权头
- 保存时服务端同步连接下的模型列表;编辑时不填 Key 也可拉取
- 单模型测试与参数设置;手机端布局优化

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jocay
2026-10-18 02:23:09 +08:00
parent a472a86069
commit 283f40892a
10 changed files with 409 additions and 250 deletions
+235 -187
View File
@@ -4,140 +4,156 @@ import type { ProviderType } from '#shared/types'
const api = useApi()
const { data: providers, refresh: refreshProviders } = await useAsyncData('providers', () => api.get<ProviderItem[]>('/api/providers'), { default: () => [] })
const { data: models, refresh: refreshModels } = await useAsyncData('models', () => api.get<ModelItem[]>('/api/models'), { default: () => [] })
const refreshAll = () => Promise.all([refreshProviders(), refreshModels()])
const TYPE_LABEL: Record<ProviderType, string> = {
'openai-compatible': 'OpenAI 兼容',
'anthropic': 'Anthropic (Claude)',
'google': 'Google (Gemini)'
}
const typeItems = Object.entries(TYPE_LABEL).map(([value, label]) => ({ value, label }))
const PRESETS: { label: string, name: string, type: ProviderType, baseUrl: string }[] = [
{ label: 'Anthropic Claude', name: 'Claude', type: 'anthropic', baseUrl: '' },
{ label: 'Google Gemini', name: 'Gemini', type: 'google', baseUrl: '' },
{ label: 'OpenAI', name: 'OpenAI', type: 'openai-compatible', baseUrl: 'https://api.openai.com/v1' },
{ label: 'DeepSeek', name: 'DeepSeek', type: 'openai-compatible', baseUrl: 'https://api.deepseek.com/v1' },
{ label: 'OpenRouter', name: 'OpenRouter', type: 'openai-compatible', baseUrl: 'https://openrouter.ai/api/v1' },
{ label: '硅基流动', name: '硅基流动', type: 'openai-compatible', baseUrl: 'https://api.siliconflow.cn/v1' },
{ label: 'Ollama(本地)', name: 'Ollama', type: 'openai-compatible', baseUrl: 'http://localhost:11434/v1' }
const TYPE_ITEMS: { value: ProviderType, label: string, hint: string }[] = [
{ value: 'openai-compatible', label: 'OpenAI 兼容', hint: '绝大多数中转站、OpenAI、DeepSeek、OpenRouter、Ollama 等' },
{ value: 'anthropic', label: 'Anthropic(Claude 原生)', hint: 'Claude 官方接口,或提供 Claude 原生格式的中转站' },
{ value: 'google', label: 'Google Gemini(原生)', hint: 'Gemini 官方接口,或提供 Gemini 原生格式的中转站' }
]
const DEFAULT_URL: Record<ProviderType, string> = {
'openai-compatible': 'https://api.openai.com/v1',
const TYPE_LABEL = Object.fromEntries(TYPE_ITEMS.map(t => [t.value, t.label])) as Record<ProviderType, string>
const URL_PLACEHOLDER: Record<ProviderType, string> = {
'openai-compatible': 'https://你的中转站域名/v1',
'anthropic': 'https://api.anthropic.com/v1',
'google': 'https://generativelanguage.googleapis.com/v1beta'
}
/* ---------- 提供方 ---------- */
const providerOpen = ref(false)
const providerForm = reactive({ id: '', name: '', type: 'openai-compatible' as ProviderType, baseUrl: '', apiKey: '', hasKey: false })
const savingProvider = ref(false)
const modelsOf = (providerId: string) => models.value.filter(m => m.providerId === providerId)
function openProvider(p?: ProviderItem) {
Object.assign(providerForm, p
? { id: p.id, name: p.name, type: p.type, baseUrl: p.baseUrl, apiKey: '', hasKey: p.hasKey }
: { id: '', name: '', type: 'openai-compatible', baseUrl: '', apiKey: '', hasKey: false })
providerOpen.value = true
}
function applyPreset(label: string) {
const p = PRESETS.find(x => x.label === label)
if (!p) return
providerForm.name = p.name
providerForm.type = p.type
providerForm.baseUrl = p.baseUrl
}
async function saveProvider() {
savingProvider.value = true
try {
const body = { name: providerForm.name, type: providerForm.type, baseUrl: providerForm.baseUrl, apiKey: providerForm.apiKey }
if (providerForm.id) await api.put(`/api/providers/${providerForm.id}`, body)
else await api.post('/api/providers', body)
providerOpen.value = false
await refreshProviders()
}
finally {
savingProvider.value = false
}
}
const deletingProvider = ref<ProviderItem | null>(null)
async function deleteProvider() {
await api.del(`/api/providers/${deletingProvider.value!.id}`)
deletingProvider.value = null
await Promise.all([refreshProviders(), refreshModels()])
}
/* ---------- 模型 ---------- */
const modelOpen = ref(false)
const modelForm = reactive({
id: '', providerId: '', label: '', modelId: '',
temperature: null as number | null, topP: null as number | null, maxOutputTokens: null as number | null
/* ================= 连接编辑(含模型) ================= */
const open = ref(false)
const saving = ref(false)
const form = reactive({
id: '',
name: '',
type: 'openai-compatible' as ProviderType,
baseUrl: '',
apiKey: '',
hasKey: false,
models: [] as string[]
})
const remoteModels = ref<string[]>([])
const loadingRemote = ref(false)
const savingModel = ref(false)
const manualModel = ref('')
const remote = ref<string[]>([])
const remoteQuery = ref('')
const fetching = ref(false)
const fetchError = ref('')
function openModel(providerId: string, m?: ModelItem) {
Object.assign(modelForm, m
? { id: m.id, providerId, label: m.label, modelId: m.modelId, temperature: m.temperature, topP: m.topP, maxOutputTokens: m.maxOutputTokens }
: { id: '', providerId, label: '', modelId: '', temperature: null, topP: null, maxOutputTokens: null })
remoteModels.value = []
modelOpen.value = true
function openEditor(p?: ProviderItem) {
Object.assign(form, p
? { id: p.id, name: p.name, type: p.type, baseUrl: p.baseUrl, apiKey: '', hasKey: p.hasKey, models: modelsOf(p.id).map(m => m.modelId) }
: { id: '', name: '', type: 'openai-compatible', baseUrl: '', apiKey: '', hasKey: false, models: [] })
manualModel.value = ''
remote.value = []
remoteQuery.value = ''
fetchError.value = ''
open.value = true
}
function addManual() {
const names = manualModel.value.split(/[,,\s]+/).map(s => s.trim()).filter(Boolean)
for (const n of names) if (!form.models.includes(n)) form.models.push(n)
manualModel.value = ''
}
function removeModel(id: string) {
form.models = form.models.filter(m => m !== id)
}
function toggleModel(id: string, on: boolean) {
if (on && !form.models.includes(id)) form.models.push(id)
if (!on) removeModel(id)
}
const filteredRemote = computed(() => {
const q = remoteQuery.value.trim().toLowerCase()
return q ? remote.value.filter(m => m.toLowerCase().includes(q)) : remote.value
})
function selectAllVisible() {
for (const m of filteredRemote.value) if (!form.models.includes(m)) form.models.push(m)
}
function clearVisible() {
const visible = new Set(filteredRemote.value)
form.models = form.models.filter(m => !visible.has(m))
}
async function fetchRemote() {
loadingRemote.value = true
fetching.value = true
fetchError.value = ''
try {
remoteModels.value = await api.get<string[]>(`/api/providers/${modelForm.providerId}/remote-models`)
if (!remoteModels.value.length) api.toast.add({ title: '没有获取到模型', color: 'warning' })
}
finally {
loadingRemote.value = false
}
}
async function saveModel() {
savingModel.value = true
try {
const body = {
providerId: modelForm.providerId,
label: modelForm.label.trim() || modelForm.modelId.trim(),
modelId: modelForm.modelId.trim(),
temperature: modelForm.temperature,
topP: modelForm.topP,
maxOutputTokens: modelForm.maxOutputTokens
const res = await api.post<{ models: string[], baseUrl: string }>('/api/providers/remote-models', {
type: form.type,
baseUrl: form.baseUrl,
apiKey: form.apiKey,
providerId: form.id || undefined
}, { silent: true })
remote.value = res.models
if (res.baseUrl && res.baseUrl !== form.baseUrl.trim().replace(/\/+$/, '')) {
form.baseUrl = res.baseUrl
api.toast.add({ title: 'Base URL 已自动补全', description: res.baseUrl, color: 'info', icon: 'i-lucide-wand-sparkles' })
}
if (modelForm.id) await api.put(`/api/models/${modelForm.id}`, body)
else await api.post('/api/models', body)
modelOpen.value = false
await refreshModels()
if (!res.models.length) fetchError.value = '接口返回了空列表,可以手动填写模型名'
}
catch (err) {
fetchError.value = apiErrorMessage(err)
}
finally {
savingModel.value = false
fetching.value = false
}
}
async function deleteModel(m: ModelItem) {
await api.del(`/api/models/${m.id}`)
await refreshModels()
const canSave = computed(() => (form.apiKey.trim() || form.hasKey) && (form.type !== 'openai-compatible' || form.baseUrl.trim()))
async function save() {
if (manualModel.value.trim()) addManual()
saving.value = true
try {
const body = { name: form.name, type: form.type, baseUrl: form.baseUrl, apiKey: form.apiKey, models: form.models }
if (form.id) await api.put(`/api/providers/${form.id}`, body)
else await api.post('/api/providers', body)
open.value = false
await refreshAll()
api.toast.add({ title: '已保存', description: `${form.models.length} 个模型`, color: 'success', icon: 'i-lucide-circle-check' })
}
finally {
saving.value = false
}
}
const deleting = ref<ProviderItem | null>(null)
async function confirmDelete() {
await api.del(`/api/providers/${deleting.value!.id}`)
deleting.value = null
await refreshAll()
}
/* ================= 单个模型:测试与参数 ================= */
const testing = ref<string | null>(null)
async function testModel(m: ModelItem) {
testing.value = m.id
try {
const res = await api.post<{ ok: boolean, text?: string, error?: string, ms: number }>(`/api/models/${m.id}/test`)
if (res.ok) api.toast.add({ title: `${m.label} 连接成功`, description: `${res.ms} ms:${res.text}`, color: 'success', icon: 'i-lucide-circle-check' })
else api.toast.add({ title: `${m.label} 连接失败`, description: res.error, color: 'error', duration: 10000 })
if (res.ok) api.toast.add({ title: `${m.label} 可用`, description: `${res.ms} ms:${res.text}`, color: 'success', icon: 'i-lucide-circle-check' })
else api.toast.add({ title: `${m.label} 调用失败`, description: res.error, color: 'error', duration: 12000 })
}
finally {
testing.value = null
}
}
const modelsOf = (providerId: string) => models.value.filter(m => m.providerId === providerId)
const paramOpen = ref(false)
const paramForm = reactive({ id: '', modelId: '', label: '', temperature: null as number | null, topP: null as number | null, maxOutputTokens: null as number | null })
function openParams(m: ModelItem) {
Object.assign(paramForm, { id: m.id, modelId: m.modelId, label: m.label, temperature: m.temperature, topP: m.topP, maxOutputTokens: m.maxOutputTokens })
paramOpen.value = true
}
async function saveParams() {
await api.put(`/api/models/${paramForm.id}`, {
label: paramForm.label.trim() || paramForm.modelId,
temperature: paramForm.temperature,
topP: paramForm.topP,
maxOutputTokens: paramForm.maxOutputTokens
})
paramOpen.value = false
await refreshModels()
}
</script>
<template>
@@ -145,73 +161,65 @@ const modelsOf = (providerId: string) => models.value.filter(m => m.providerId =
<template #header>
<UDashboardNavbar title="模型与 API">
<template #right>
<UButton icon="i-lucide-plus" label="添加 API 提供方" @click="openProvider()" />
<UButton icon="i-lucide-plus" label="添加 API" @click="openEditor()" />
</template>
</UDashboardNavbar>
</template>
<template #body>
<p class="text-sm text-muted mb-4">
API Key 使用 AES-256-GCM 加密后保存在本地 <code>data/app.db</code>。添加提供方后,在其下添加具体模型,然后就可以在作品的「AI 团队」中把模型分配给主编和作家。
每个 API 连接填写接口类型、地址和 Key,再从接口拉取或手动填写要用的模型。之后在作品的「AI 团队」中给主编和作家选择模型。API Key 加密保存在服务器上。
</p>
<div v-if="!providers.length" class="flex flex-col items-center py-20 gap-3 text-center">
<UIcon name="i-lucide-plug" class="size-12 text-muted" />
<p class="text-muted">还没有 API 提供方</p>
<UButton icon="i-lucide-plus" label="添加 API 提供方" @click="openProvider()" />
<p class="text-muted">
还没有 API 连接
</p>
<UButton icon="i-lucide-plus" label="添加 API" @click="openEditor()" />
</div>
<div class="space-y-4">
<UCard v-for="p in providers" :key="p.id">
<UCard v-for="p in providers" :key="p.id" :ui="{ body: 'p-0 sm:p-0' }">
<template #header>
<div class="flex items-center gap-3">
<UIcon name="i-lucide-server" class="size-5 text-primary" />
<div class="min-w-0">
<div class="font-semibold">
<div class="flex items-start gap-3">
<UIcon name="i-lucide-server" class="size-5 text-primary mt-0.5 shrink-0" />
<div class="min-w-0 flex-1">
<div class="font-semibold flex flex-wrap items-center gap-2">
{{ p.name }}
<UBadge variant="subtle" color="neutral" size="sm" class="ms-2">
<UBadge variant="subtle" color="neutral" size="sm">
{{ TYPE_LABEL[p.type] }}
</UBadge>
</div>
<div class="text-xs text-muted truncate">
{{ p.baseUrl || DEFAULT_URL[p.type] }} · Key:{{ p.apiKeyMasked || '未设置' }}
<div class="text-xs text-muted break-all">
{{ p.baseUrl || '官方地址' }} · <span class="whitespace-nowrap">Key:{{ p.apiKeyMasked || '未设置' }}</span>
</div>
</div>
<div class="ms-auto flex gap-1">
<UButton icon="i-lucide-plus" label="添加模型" size="sm" variant="soft" @click="openModel(p.id)" />
<UButton icon="i-lucide-pencil" size="sm" color="neutral" variant="ghost" @click="openProvider(p)" />
<UButton icon="i-lucide-trash-2" size="sm" color="error" variant="ghost" @click="deletingProvider = p" />
<div class="flex gap-1 shrink-0">
<UButton icon="i-lucide-pencil" label="编辑" size="sm" color="neutral" variant="outline" @click="openEditor(p)" />
<UButton icon="i-lucide-trash-2" size="sm" color="error" variant="ghost" @click="deleting = p" />
</div>
</div>
</template>
<div v-if="!modelsOf(p.id).length" class="text-sm text-muted">
还没有模型,点击「添加模型」。
<div v-if="!modelsOf(p.id).length" class="px-4 py-3 text-sm text-muted">
还没有模型,点击「编辑」从接口拉取或手动添加。
</div>
<div v-else class="divide-y divide-default">
<div v-for="m in modelsOf(p.id)" :key="m.id" class="flex flex-wrap items-center gap-3 py-2">
<div class="min-w-0">
<div class="font-medium">
<div v-for="m in modelsOf(p.id)" :key="m.id" class="flex flex-wrap items-center gap-2 px-4 py-2.5">
<div class="min-w-0 flex-1">
<div class="font-medium text-sm break-all">
{{ m.label }}
</div>
<div class="text-xs text-muted">
{{ m.modelId }}
<template v-if="m.temperature != null">
· 温度 {{ m.temperature }}
</template>
<template v-if="m.topP != null">
· top_p {{ m.topP }}
</template>
<template v-if="m.maxOutputTokens">
· 最大输出 {{ m.maxOutputTokens }}
</template>
<div v-if="m.label !== m.modelId || m.temperature != null || m.topP != null || m.maxOutputTokens" class="text-xs text-muted break-all">
<span v-if="m.label !== m.modelId">{{ m.modelId }}</span>
<span v-if="m.temperature != null"> · 温度 {{ m.temperature }}</span>
<span v-if="m.topP != null"> · top_p {{ m.topP }}</span>
<span v-if="m.maxOutputTokens"> · 最大输出 {{ m.maxOutputTokens }}</span>
</div>
</div>
<div class="ms-auto flex gap-1">
<UButton icon="i-lucide-activity" label="测试" size="xs" color="neutral" variant="outline" :loading="testing === m.id" @click="testModel(m)" />
<UButton icon="i-lucide-pencil" size="xs" color="neutral" variant="ghost" @click="openModel(p.id, m)" />
<UButton icon="i-lucide-trash-2" size="xs" color="error" variant="ghost" @click="deleteModel(m)" />
</div>
<UButton icon="i-lucide-activity" label="测试" size="xs" color="neutral" variant="outline" :loading="testing === m.id" @click="testModel(m)" />
<UButton icon="i-lucide-sliders-horizontal" label="参数" size="xs" color="neutral" variant="ghost" @click="openParams(m)" />
</div>
</div>
</UCard>
@@ -219,91 +227,131 @@ const modelsOf = (providerId: string) => models.value.filter(m => m.providerId =
</template>
</UDashboardPanel>
<!-- 提供方编辑 -->
<UModal v-model:open="providerOpen" :title="providerForm.id ? '编辑 API 提供方' : '添加 API 提供方'">
<!-- API 连接 + 模型 -->
<UModal v-model:open="open" :title="form.id ? '编辑 API' : '添加 API'" :ui="{ content: 'max-w-2xl' }">
<template #body>
<div class="space-y-4">
<UFormField v-if="!providerForm.id" label="快速填写">
<div class="flex flex-wrap gap-1.5">
<UButton v-for="pr in PRESETS" :key="pr.label" :label="pr.label" size="xs" color="neutral" variant="outline" @click="applyPreset(pr.label)" />
<UFormField label="接口类型" required :help="TYPE_ITEMS.find(t => t.value === form.type)?.hint">
<USelect v-model="form.type" :items="TYPE_ITEMS" value-key="value" label-key="label" class="w-full" />
</UFormField>
<UFormField
label="Base URL"
:required="form.type === 'openai-compatible'"
:help="form.type === 'openai-compatible' ? '中转站一般要带 /v1;漏写的话,拉取模型时会自动补全' : '留空使用官方地址;中转站请填写其提供的地址'"
>
<UInput v-model="form.baseUrl" :placeholder="URL_PLACEHOLDER[form.type]" class="w-full" autocomplete="off" />
</UFormField>
<UFormField label="API Key" required :help="form.hasKey ? '已保存 Key,留空则保持不变' : undefined">
<UInput v-model="form.apiKey" type="password" :placeholder="form.hasKey ? '••••••(不修改)' : 'sk-...'" class="w-full" autocomplete="new-password" />
</UFormField>
<UFormField label="名称" hint="可选,默认使用域名">
<UInput v-model="form.name" class="w-full" />
</UFormField>
<USeparator label="模型" />
<div class="flex flex-wrap gap-2">
<UButton
icon="i-lucide-cloud-download"
label="从接口拉取模型"
:loading="fetching"
:disabled="!form.apiKey.trim() && !form.hasKey"
@click="fetchRemote"
/>
<div class="flex flex-1 min-w-56 gap-2">
<UInput v-model="manualModel" placeholder="或手动输入模型名,回车添加" class="flex-1" @keydown.enter.prevent="addManual" />
<UButton icon="i-lucide-plus" color="neutral" variant="outline" :disabled="!manualModel.trim()" @click="addManual" />
</div>
</UFormField>
<UFormField label="名称" required>
<UInput v-model="providerForm.name" class="w-full" />
</UFormField>
<UFormField label="接口类型" required>
<USelect v-model="providerForm.type" :items="typeItems" class="w-full" />
</UFormField>
<UFormField label="Base URL" :hint="`留空使用默认:${DEFAULT_URL[providerForm.type]}`">
<UInput v-model="providerForm.baseUrl" :placeholder="DEFAULT_URL[providerForm.type]" class="w-full" />
</UFormField>
<UFormField label="API Key" :hint="providerForm.hasKey ? '留空则保留原有 Key' : ''">
<UInput v-model="providerForm.apiKey" type="password" autocomplete="off" class="w-full" />
</UFormField>
</div>
<UAlert v-if="fetchError" color="warning" variant="subtle" icon="i-lucide-triangle-alert" :description="fetchError" :ui="{ description: 'whitespace-pre-wrap break-all' }" />
<!-- 已选模型 -->
<div>
<div class="text-sm font-medium mb-1.5">
已选 {{ form.models.length }} 个模型
</div>
<div v-if="form.models.length" class="flex flex-wrap gap-1.5">
<UBadge v-for="m in form.models" :key="m" color="primary" variant="soft" class="gap-1 pe-1 max-w-full">
<span class="truncate">{{ m }}</span>
<UButton icon="i-lucide-x" size="xs" color="primary" variant="link" class="p-0" @click="removeModel(m)" />
</UBadge>
</div>
<p v-else class="text-xs text-muted">
从下方列表勾选,或手动输入模型名
</p>
</div>
<!-- 拉取到的模型列表 -->
<div v-if="remote.length" class="rounded-lg border border-default">
<div class="flex items-center gap-2 p-2 border-b border-default">
<UInput v-model="remoteQuery" icon="i-lucide-search" placeholder="搜索模型" size="sm" class="flex-1" />
<span class="text-xs text-muted shrink-0">{{ filteredRemote.length }}/{{ remote.length }}</span>
<UButton label="全选" size="xs" color="neutral" variant="ghost" @click="selectAllVisible" />
<UButton label="清空" size="xs" color="neutral" variant="ghost" @click="clearVisible" />
</div>
<div class="max-h-72 overflow-y-auto p-1">
<label
v-for="m in filteredRemote"
:key="m"
class="flex items-center gap-2 px-2 py-1.5 rounded-md hover:bg-elevated cursor-pointer text-sm"
>
<UCheckbox :model-value="form.models.includes(m)" @update:model-value="(v: boolean | 'indeterminate') => toggleModel(m, v === true)" />
<span class="break-all">{{ m }}</span>
</label>
<p v-if="!filteredRemote.length" class="text-xs text-muted text-center py-4">
没有匹配的模型
</p>
</div>
</div>
</div>
</template>
<template #footer="{ close }">
<div class="flex justify-end gap-2 w-full">
<UButton label="取消" color="neutral" variant="outline" @click="close" />
<UButton label="保存" :loading="savingProvider" :disabled="!providerForm.name" @click="saveProvider" />
<UButton label="保存" icon="i-lucide-save" :loading="saving" :disabled="!canSave" @click="save" />
</div>
</template>
</UModal>
<!-- 模型编辑 -->
<UModal v-model:open="modelOpen" :title="modelForm.id ? '编辑模型' : '添加模型'">
<!-- 模型参数 -->
<UModal v-model:open="paramOpen" title="模型参数" :description="paramForm.modelId">
<template #body>
<div class="space-y-4">
<UFormField label="模型 ID" required hint="与 API 中的 model 参数一致">
<div class="flex gap-2">
<UInput v-model="modelForm.modelId" placeholder="例如 claude-opus-5-5 / gemini-3-pro" class="flex-1" />
<UButton icon="i-lucide-download" label="获取列表" color="neutral" variant="outline" :loading="loadingRemote" @click="fetchRemote" />
</div>
<UFormField label="显示名称">
<UInput v-model="paramForm.label" :placeholder="paramForm.modelId" class="w-full" />
</UFormField>
<UFormField v-if="remoteModels.length" label="从列表选择">
<USelectMenu
:items="remoteModels"
:model-value="remoteModels.includes(modelForm.modelId) ? modelForm.modelId : undefined"
placeholder="搜索模型…"
class="w-full"
@update:model-value="(v: string) => (modelForm.modelId = v)"
/>
</UFormField>
<UFormField label="显示名称" hint="可选">
<UInput v-model="modelForm.label" :placeholder="modelForm.modelId" class="w-full" />
</UFormField>
<div class="grid grid-cols-3 gap-3">
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3">
<UFormField label="温度" hint="留空=默认">
<UInputNumber v-model="modelForm.temperature" :min="0" :max="2" :step="0.1" placeholder="默认" />
<UInputNumber v-model="paramForm.temperature" :min="0" :max="2" :step="0.1" placeholder="默认" class="w-full" />
</UFormField>
<UFormField label="top_p">
<UInputNumber v-model="modelForm.topP" :min="0" :max="1" :step="0.05" placeholder="默认" />
<UInputNumber v-model="paramForm.topP" :min="0" :max="1" :step="0.05" placeholder="默认" class="w-full" />
</UFormField>
<UFormField label="最大输出 tokens">
<UInputNumber v-model="modelForm.maxOutputTokens" :min="256" :step="1024" placeholder="默认" />
<UInputNumber v-model="paramForm.maxOutputTokens" :min="256" :step="1024" placeholder="默认" class="w-full" />
</UFormField>
</div>
<p class="text-xs text-muted">
写长章节时建议把「最大输出 tokens」调大(例如 16000 以上);如果输出仍被截断,系统会自动续写。
写长章节时建议把「最大输出 tokens」调大(例如 16000 以上);输出被截断时系统会自动续写。Claude 模型不要同时设置温度和 top_p。
</p>
</div>
</template>
<template #footer="{ close }">
<div class="flex justify-end gap-2 w-full">
<UButton label="取消" color="neutral" variant="outline" @click="close" />
<UButton label="保存" :loading="savingModel" :disabled="!modelForm.modelId.trim()" @click="saveModel" />
<UButton label="保存" @click="saveParams" />
</div>
</template>
</UModal>
<UModal :open="!!deletingProvider" title="删除 API 提供方" @update:open="v => !v && (deletingProvider = null)">
<UModal :open="!!deleting" title="删除 API" @update:open="v => !v && (deleting = null)">
<template #body>
<p>删除「{{ deletingProvider?.name }}」会同时删除它下面的所有模型,已分配这些模型的 AI 成员需要重新选择模型。</p>
<p>删除「{{ deleting?.name }}」会同时删除它下面的所有模型,使用这些模型的 AI 成员需要重新选择模型。</p>
</template>
<template #footer>
<div class="flex justify-end gap-2 w-full">
<UButton label="取消" color="neutral" variant="outline" @click="deletingProvider = null" />
<UButton label="删除" color="error" @click="deleteProvider" />
<UButton label="取消" color="neutral" variant="outline" @click="deleting = null" />
<UButton label="删除" color="error" @click="confirmDelete" />
</div>
</template>
</UModal>