优化 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
+18 -8
View File
@@ -30,18 +30,28 @@ async function waitIdle(pid, timeout = 30000) {
}
const step = msg => console.log(`✔ ${msg}`)
// 1. 提供方与模型
const provider = await api('/api/providers', 'POST', { name: 'Mock', type: 'openai-compatible', baseUrl: mock, apiKey: 'test-key' })
// 1. API 连接与模型
const mockRoot = mock.replace(/\/v1$/, '')
const pulled = await api('/api/providers/remote-models', 'POST', { type: 'openai-compatible', baseUrl: mockRoot, apiKey: 'test-key' })
assert.deepEqual(pulled.models, ['mock-editor', 'mock-writer-trunc'])
assert.equal(pulled.baseUrl, mockRoot + '/v1', '漏写 /v1 时应自动补全')
const provider = await api('/api/providers', 'POST', { type: 'openai-compatible', baseUrl: pulled.baseUrl, apiKey: 'test-key', models: [...pulled.models, 'extra-model'] })
const providers = await api('/api/providers')
assert.ok(providers.find(p => p.id === provider.id).apiKeyMasked.length > 0)
const savedProvider = providers.find(p => p.id === provider.id)
assert.equal(savedProvider.name, new URL(mock).host, '未填名称时使用域名')
assert.ok(!JSON.stringify(providers).includes('test-key'), '接口不应返回明文 Key')
const remote = await api(`/api/providers/${provider.id}/remote-models`)
assert.deepEqual(remote, ['mock-editor', 'mock-writer-trunc'])
const editorModel = await api('/api/models', 'POST', { providerId: provider.id, modelId: 'mock-editor', label: '模拟主编' })
const writerModel = await api('/api/models', 'POST', { providerId: provider.id, modelId: 'mock-writer-trunc', label: '模拟作家', maxOutputTokens: 4096 })
const pulledAgain = await api('/api/providers/remote-models', 'POST', { type: 'openai-compatible', baseUrl: pulled.baseUrl, providerId: provider.id })
assert.equal(pulledAgain.models.length, 2, '编辑时不填 Key 应使用已保存的 Key')
const synced = await api(`/api/providers/${provider.id}`, 'PUT', { models: pulled.models })
assert.equal(synced.removed, 1)
let allModels = (await api('/api/models')).filter(m => m.providerId === provider.id)
assert.deepEqual(allModels.map(m => m.modelId).sort(), ['mock-editor', 'mock-writer-trunc'])
const editorModel = allModels.find(m => m.modelId === 'mock-editor')
const writerModel = allModels.find(m => m.modelId === 'mock-writer-trunc')
await api(`/api/models/${writerModel.id}`, 'PUT', { label: '模拟作家', maxOutputTokens: 4096 })
const test = await api(`/api/models/${editorModel.id}/test`, 'POST', {})
assert.equal(test.ok, true, JSON.stringify(test))
step('提供方/模型/连接测试')
step('API 连接:拉取模型(自动补全 /v1)、批量保存、同步、连接测试')
// 2. 项目与设定
const { id: pid } = await api('/api/projects', 'POST', { title: 'E2E 测试作品' })