渠道: 移除供应商下拉, 格式多选为唯一配置
- 前端渠道表单去掉"供应商"下拉, 只保留"支持的 API 格式"多选(默认 chat) - 后端 provider 改为可选字段: 为空时按 formats 推断(仅 messages→anthropic, 含 responses→openai, 否则 compatible), 兼容旧数据 - 保存时校验至少选择一种格式 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -45,8 +45,8 @@ func (h *Handler) AdminChannels(c *gin.Context) {
|
|||||||
|
|
||||||
type channelBody struct {
|
type channelBody struct {
|
||||||
Name string `json:"name" binding:"required,min=1,max=64"`
|
Name string `json:"name" binding:"required,min=1,max=64"`
|
||||||
Provider string `json:"provider" binding:"required"`
|
Provider string `json:"provider"` // 可选:为空时按 formats 推断(兼容旧数据)
|
||||||
Formats []string `json:"formats"` // 原生支持的协议 chat|responses|messages,空则按 provider 推断
|
Formats []string `json:"formats"` // 原生支持的协议 chat|responses|messages(主配置)
|
||||||
BaseURL string `json:"base_url" binding:"required"`
|
BaseURL string `json:"base_url" binding:"required"`
|
||||||
APIKey string `json:"api_key"`
|
APIKey string `json:"api_key"`
|
||||||
Weight *int `json:"weight"`
|
Weight *int `json:"weight"`
|
||||||
@@ -64,6 +64,29 @@ var validFormats = map[string]bool{
|
|||||||
store.FormatChat: true, store.FormatResponses: true, store.FormatMessages: true,
|
store.FormatChat: true, store.FormatResponses: true, store.FormatMessages: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// deriveProvider 按格式推断供应商(仅作内部字段/兼容用途,不参与路由)。
|
||||||
|
func deriveProvider(formats []string) string {
|
||||||
|
if len(formats) == 0 {
|
||||||
|
return store.ChannelProviderCompatible
|
||||||
|
}
|
||||||
|
messagesOnly, hasResponses := true, false
|
||||||
|
for _, f := range formats {
|
||||||
|
if f != store.FormatMessages {
|
||||||
|
messagesOnly = false
|
||||||
|
}
|
||||||
|
if f == store.FormatResponses {
|
||||||
|
hasResponses = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if messagesOnly {
|
||||||
|
return store.ChannelProviderAnthropic
|
||||||
|
}
|
||||||
|
if hasResponses {
|
||||||
|
return store.ChannelProviderOpenAI
|
||||||
|
}
|
||||||
|
return store.ChannelProviderCompatible
|
||||||
|
}
|
||||||
|
|
||||||
// resolveFormats 渠道协议格式:显式给出则校验去重;空则按 provider 推断默认。
|
// resolveFormats 渠道协议格式:显式给出则校验去重;空则按 provider 推断默认。
|
||||||
func resolveFormats(provider string, formats []string) ([]string, error) {
|
func resolveFormats(provider string, formats []string) ([]string, error) {
|
||||||
if len(formats) == 0 {
|
if len(formats) == 0 {
|
||||||
@@ -97,6 +120,9 @@ func (h *Handler) AdminCreateChannel(c *gin.Context) {
|
|||||||
resp.Fail(c, http.StatusBadRequest, "invalid input: "+err.Error())
|
resp.Fail(c, http.StatusBadRequest, "invalid input: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if req.Provider == "" {
|
||||||
|
req.Provider = deriveProvider(req.Formats)
|
||||||
|
}
|
||||||
if !validateProvider(req.Provider) {
|
if !validateProvider(req.Provider) {
|
||||||
resp.Fail(c, http.StatusBadRequest, "provider must be openai, anthropic or compatible")
|
resp.Fail(c, http.StatusBadRequest, "provider must be openai, anthropic or compatible")
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -18,8 +18,7 @@ const busyId = ref<number | null>(null)
|
|||||||
|
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
name: '',
|
name: '',
|
||||||
provider: 'openai' as 'openai' | 'anthropic' | 'compatible',
|
formats: ['chat'] as string[],
|
||||||
formats: [] as string[],
|
|
||||||
base_url: '',
|
base_url: '',
|
||||||
api_key: '',
|
api_key: '',
|
||||||
weight: 1,
|
weight: 1,
|
||||||
@@ -29,17 +28,6 @@ const form = reactive({
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
// 按供应商推断默认支持的协议格式
|
|
||||||
function defaultFormats(p: string): string[] {
|
|
||||||
if (p === 'anthropic') return ['messages']
|
|
||||||
if (p === 'openai') return ['chat', 'responses']
|
|
||||||
return ['chat']
|
|
||||||
}
|
|
||||||
|
|
||||||
function onProviderChange() {
|
|
||||||
form.formats = defaultFormats(form.provider)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
try {
|
try {
|
||||||
const { data } = await http.get('/admin/channels')
|
const { data } = await http.get('/admin/channels')
|
||||||
@@ -52,7 +40,7 @@ async function load() {
|
|||||||
function openCreate() {
|
function openCreate() {
|
||||||
editing.value = null
|
editing.value = null
|
||||||
Object.assign(form, {
|
Object.assign(form, {
|
||||||
name: '', provider: 'openai', formats: defaultFormats('openai'), base_url: '', api_key: '',
|
name: '', formats: ['chat'], base_url: '', api_key: '',
|
||||||
weight: 1, priority: 0, timeout_ms: 120000, max_concurrency: 16, enabled: true,
|
weight: 1, priority: 0, timeout_ms: 120000, max_concurrency: 16, enabled: true,
|
||||||
})
|
})
|
||||||
editOpen.value = true
|
editOpen.value = true
|
||||||
@@ -61,7 +49,7 @@ function openCreate() {
|
|||||||
function openEdit(ch: Channel) {
|
function openEdit(ch: Channel) {
|
||||||
editing.value = ch
|
editing.value = ch
|
||||||
Object.assign(form, {
|
Object.assign(form, {
|
||||||
name: ch.name, provider: ch.provider, formats: [...(ch.formats || defaultFormats(ch.provider))],
|
name: ch.name, formats: [...(ch.formats?.length ? ch.formats : ['chat'])],
|
||||||
base_url: ch.base_url, api_key: '',
|
base_url: ch.base_url, api_key: '',
|
||||||
weight: ch.weight, priority: ch.priority, timeout_ms: ch.timeout_ms,
|
weight: ch.weight, priority: ch.priority, timeout_ms: ch.timeout_ms,
|
||||||
max_concurrency: ch.max_concurrency, enabled: ch.enabled,
|
max_concurrency: ch.max_concurrency, enabled: ch.enabled,
|
||||||
@@ -70,6 +58,10 @@ function openEdit(ch: Channel) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function save() {
|
async function save() {
|
||||||
|
if (form.formats.length === 0) {
|
||||||
|
toast.err('请至少选择一种 API 格式')
|
||||||
|
return
|
||||||
|
}
|
||||||
saving.value = true
|
saving.value = true
|
||||||
const payload = {
|
const payload = {
|
||||||
...form,
|
...form,
|
||||||
@@ -199,21 +191,7 @@ onMounted(load)
|
|||||||
|
|
||||||
<Modal :open="editOpen" :title="editing ? '编辑渠道' : '添加渠道'" @close="editOpen = false">
|
<Modal :open="editOpen" :title="editing ? '编辑渠道' : '添加渠道'" @close="editOpen = false">
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div class="grid grid-cols-2 gap-4">
|
|
||||||
<Input v-model="form.name" label="名称" placeholder="openai" />
|
<Input v-model="form.name" label="名称" placeholder="openai" />
|
||||||
<label class="block">
|
|
||||||
<span class="mb-1.5 block text-xs font-medium text-muted">供应商</span>
|
|
||||||
<select
|
|
||||||
v-model="form.provider"
|
|
||||||
class="h-10 w-full rounded-md border border-edge2 bg-surface px-3 text-sm text-ink outline-none focus:border-accent"
|
|
||||||
@change="onProviderChange"
|
|
||||||
>
|
|
||||||
<option value="openai">OpenAI</option>
|
|
||||||
<option value="anthropic">Anthropic</option>
|
|
||||||
<option value="compatible">兼容</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<span class="mb-1.5 block text-xs font-medium text-muted">支持的 API 格式</span>
|
<span class="mb-1.5 block text-xs font-medium text-muted">支持的 API 格式</span>
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
|
|||||||
Reference in New Issue
Block a user