优化 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
+19 -8
View File
@@ -2,21 +2,32 @@ import { eq } from 'drizzle-orm'
import { z } from 'zod'
const schema = z.object({
name: z.string().min(1).optional(),
name: z.string().optional(),
type: z.enum(['openai-compatible', 'anthropic', 'google']).optional(),
baseUrl: z.string().optional(),
// 不传或传空字符串表示保留原有密钥
apiKey: z.string().optional()
apiKey: z.string().optional(),
// 传入时把该连接下的模型同步为这个列表
models: z.array(z.string()).optional()
})
export default defineEventHandler(async (event) => {
const id = param(event)
const body = await readJson(event, schema)
const patch: Partial<typeof tables.providers.$inferInsert> = {}
if (body.name !== undefined) patch.name = body.name
if (body.type !== undefined) patch.type = body.type
if (body.baseUrl !== undefined) patch.baseUrl = body.baseUrl.trim()
const db = useDb()
const current = db.select().from(tables.providers).where(eq(tables.providers.id, id)).get()
if (!current) throw createError({ statusCode: 404, message: 'API 连接不存在' })
const type = body.type ?? current.type
const baseUrl = body.baseUrl !== undefined ? body.baseUrl.trim() : current.baseUrl
const patch: Partial<typeof tables.providers.$inferInsert> = { type, baseUrl }
if (body.name !== undefined) patch.name = body.name.trim() || defaultProviderName(type, baseUrl)
if (body.apiKey) patch.apiKeyEnc = encryptSecret(body.apiKey.trim())
useDb().update(tables.providers).set(patch).where(eq(tables.providers.id, id)).run()
return { ok: true }
let result = { added: 0, removed: 0 }
db.transaction(() => {
db.update(tables.providers).set(patch).where(eq(tables.providers.id, id)).run()
if (body.models) result = syncProviderModels(id, body.models)
})
return { ok: true, ...result }
})
@@ -1,13 +0,0 @@
import { eq } from 'drizzle-orm'
/** 拉取提供方的可用模型列表 */
export default defineEventHandler(async (event) => {
const p = useDb().select().from(tables.providers).where(eq(tables.providers.id, param(event))).get()
if (!p) throw createError({ statusCode: 404, message: '提供方不存在' })
try {
return await listRemoteModels(p.type, p.baseUrl, decryptSecret(p.apiKeyEnc))
}
catch (err) {
throw createError({ statusCode: 502, message: `获取模型列表失败:${describeError(err)}` })
}
})
+16 -10
View File
@@ -1,20 +1,26 @@
import { z } from 'zod'
const schema = z.object({
name: z.string().min(1),
name: z.string().default(''),
type: z.enum(['openai-compatible', 'anthropic', 'google']),
baseUrl: z.string().default(''),
apiKey: z.string().default('')
apiKey: z.string().default(''),
models: z.array(z.string()).default([])
})
/** 新建 API 连接,并一次性添加所选模型 */
export default defineEventHandler(async (event) => {
const body = await readJson(event, schema)
const row = useDb().insert(tables.providers).values({
id: newId(),
name: body.name,
type: body.type,
baseUrl: body.baseUrl.trim(),
apiKeyEnc: encryptSecret(body.apiKey.trim())
}).returning().get()
return { id: row.id }
const id = newId()
useDb().transaction(() => {
useDb().insert(tables.providers).values({
id,
name: body.name.trim() || defaultProviderName(body.type, body.baseUrl),
type: body.type,
baseUrl: body.baseUrl.trim(),
apiKeyEnc: encryptSecret(body.apiKey.trim())
}).run()
syncProviderModels(id, body.models)
})
return { id }
})
@@ -0,0 +1,27 @@
import { eq } from 'drizzle-orm'
import { z } from 'zod'
const schema = z.object({
type: z.enum(['openai-compatible', 'anthropic', 'google']),
baseUrl: z.string().default(''),
apiKey: z.string().default(''),
/** 编辑已有连接且未重新填写 Key 时,使用已保存的 Key */
providerId: z.string().optional()
})
/** 用(可能尚未保存的)连接配置拉取模型列表 */
export default defineEventHandler(async (event) => {
const body = await readJson(event, schema)
let apiKey = body.apiKey.trim()
if (!apiKey && body.providerId) {
const p = useDb().select().from(tables.providers).where(eq(tables.providers.id, body.providerId)).get()
if (p) apiKey = decryptSecret(p.apiKeyEnc)
}
if (!apiKey) throw createError({ statusCode: 400, message: '请先填写 API Key' })
try {
return await listRemoteModels(body.type, body.baseUrl, apiKey)
}
catch (err) {
throw createError({ statusCode: 502, message: `拉取模型列表失败:${describeError(err)}` })
}
})