渠道: 分协议 Base URL + 模型抽屉/远程拉取/操作图标
- 渠道支持分协议 base_url(chat/responses/messages 各一), 网关按协议选 base 直通 (如智谱三种格式不同 base, 一个渠道即可), UpstreamURL 按 proto 拼接 - 渠道模型改为下方抽屉: 当前绑定列表(内联改上游/解除)、从接口拉取(remote 预览+勾选添加)、手动添加 - 新增 /channels/:id/models/remote 预览接口; 操作按钮加 Phosphor 图标 - 修复 formats jsonb 更新未序列化问题; 手机端渠道卡片化 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { PhArrowsClockwise, PhPlus, PhX } from '@phosphor-icons/vue'
|
||||
import { http, errMsg } from '@/api/client'
|
||||
import { useToastStore } from '@/stores/toast'
|
||||
import Button from '@/components/ui/Button.vue'
|
||||
import type { Channel, ChannelModelMapping } from '@/types'
|
||||
|
||||
const props = defineProps<{ channel: Channel }>()
|
||||
const toast = useToastStore()
|
||||
|
||||
const mappings = ref<ChannelModelMapping[]>([])
|
||||
const remote = ref<string[]>([])
|
||||
const selected = ref<string[]>([])
|
||||
const loading = ref(false)
|
||||
const addForm = reactive({ custom_name: '', upstream_model: '' })
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
const { data } = await http.get(`/admin/channels/${props.channel.id}/models`)
|
||||
mappings.value = data.data.items
|
||||
} catch (e) {
|
||||
toast.err(errMsg(e))
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchRemote() {
|
||||
loading.value = true
|
||||
try {
|
||||
const { data } = await http.get(`/admin/channels/${props.channel.id}/models/remote`)
|
||||
remote.value = data.data.items
|
||||
const bound = new Set(mappings.value.map((m) => m.upstream_model))
|
||||
selected.value = remote.value.filter((r) => bound.has(r))
|
||||
} catch (e) {
|
||||
toast.err(errMsg(e))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function addSelected() {
|
||||
const bound = new Set(mappings.value.map((m) => m.upstream_model))
|
||||
let added = 0
|
||||
for (const name of selected.value) {
|
||||
if (bound.has(name)) continue
|
||||
try {
|
||||
await http.post(`/admin/channels/${props.channel.id}/models`, { upstream_model: name })
|
||||
added++
|
||||
} catch {
|
||||
/* 单个失败不中断 */
|
||||
}
|
||||
}
|
||||
toast.ok(added ? `已添加 ${added} 个模型` : '所选均已添加')
|
||||
await load()
|
||||
}
|
||||
|
||||
async function addManual() {
|
||||
if (!addForm.upstream_model.trim()) return
|
||||
try {
|
||||
await http.post(`/admin/channels/${props.channel.id}/models`, {
|
||||
upstream_model: addForm.upstream_model.trim(),
|
||||
custom_name: addForm.custom_name.trim(),
|
||||
})
|
||||
toast.ok('已添加')
|
||||
addForm.custom_name = ''
|
||||
addForm.upstream_model = ''
|
||||
await load()
|
||||
} catch (e) {
|
||||
toast.err(errMsg(e))
|
||||
}
|
||||
}
|
||||
|
||||
async function saveUpstream(b: ChannelModelMapping) {
|
||||
try {
|
||||
await http.patch(`/admin/channels/${props.channel.id}/models/${b.id}`, {
|
||||
upstream_model: b.upstream_model,
|
||||
})
|
||||
toast.ok('已更新')
|
||||
await load()
|
||||
} catch (e) {
|
||||
toast.err(errMsg(e))
|
||||
}
|
||||
}
|
||||
|
||||
async function remove(b: ChannelModelMapping) {
|
||||
if (!confirm(`解除模型 ${b.model_name} 的绑定?`)) return
|
||||
try {
|
||||
await http.delete(`/admin/channels/${props.channel.id}/models/${b.id}`)
|
||||
toast.ok('已解除')
|
||||
await load()
|
||||
} catch (e) {
|
||||
toast.err(errMsg(e))
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-3">
|
||||
<!-- 当前支持的模型 -->
|
||||
<div>
|
||||
<p class="mb-1.5 text-xs font-medium text-muted">当前支持的模型({{ mappings.length }})</p>
|
||||
<div v-if="mappings.length" class="flex flex-wrap gap-2">
|
||||
<div
|
||||
v-for="b in mappings"
|
||||
:key="b.id"
|
||||
class="inline-flex items-center gap-1.5 rounded-md border border-edge bg-surface px-2 py-1 font-mono text-[11px] text-muted"
|
||||
>
|
||||
<span class="text-ink">{{ b.model_name }}</span>
|
||||
<span class="opacity-60">→</span>
|
||||
<input
|
||||
v-model="b.upstream_model"
|
||||
class="w-28 rounded border border-transparent bg-transparent px-1 text-[11px] text-accent outline-none transition focus:border-accent/50 focus:bg-surface2"
|
||||
@change="saveUpstream(b)"
|
||||
/>
|
||||
<button class="text-muted hover:text-err" aria-label="解除" @click="remove(b)">
|
||||
<PhX :size="12" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p v-else class="text-xs text-muted">尚未添加模型</p>
|
||||
</div>
|
||||
|
||||
<!-- 从接口拉取 + 勾选 -->
|
||||
<div class="border-t border-edge pt-3">
|
||||
<div class="mb-1.5 flex items-center justify-between">
|
||||
<p class="text-xs font-medium text-muted">从接口拉取模型</p>
|
||||
<Button size="sm" variant="ghost" :loading="loading" @click="fetchRemote">
|
||||
<PhArrowsClockwise :size="13" />
|
||||
拉取
|
||||
</Button>
|
||||
</div>
|
||||
<div v-if="remote.length" class="flex max-h-36 flex-wrap gap-2 overflow-y-auto">
|
||||
<label
|
||||
v-for="m in remote"
|
||||
:key="m"
|
||||
class="flex cursor-pointer items-center gap-1.5 rounded-md border border-edge2 px-2 py-1 font-mono text-[11px] text-muted transition select-none"
|
||||
:class="selected.includes(m) ? 'border-accent bg-accent-soft text-ink' : 'hover:border-edge'"
|
||||
>
|
||||
<input v-model="selected" type="checkbox" :value="m" class="size-3.5 accent-[var(--color-accent)]" />
|
||||
{{ m }}
|
||||
</label>
|
||||
</div>
|
||||
<div v-if="remote.length" class="mt-2">
|
||||
<Button size="sm" @click="addSelected">
|
||||
<PhPlus :size="13" />
|
||||
添加所选({{ selected.length }})
|
||||
</Button>
|
||||
</div>
|
||||
<p v-else-if="!loading" class="text-xs text-muted">点「拉取」获取渠道接口返回的模型,勾选需要的加入</p>
|
||||
</div>
|
||||
|
||||
<!-- 手动添加 -->
|
||||
<div class="flex items-center gap-2 border-t border-edge pt-3">
|
||||
<input
|
||||
v-model="addForm.custom_name"
|
||||
placeholder="自定义名称(可选)"
|
||||
class="h-8 min-w-0 flex-1 rounded-md border border-edge2 bg-surface px-2 font-mono text-xs outline-none focus:border-accent"
|
||||
@keyup.enter="addManual"
|
||||
/>
|
||||
<input
|
||||
v-model="addForm.upstream_model"
|
||||
placeholder="上游模型名"
|
||||
class="h-8 min-w-0 flex-1 rounded-md border border-edge2 bg-surface px-2 font-mono text-xs outline-none focus:border-accent"
|
||||
@keyup.enter="addManual"
|
||||
/>
|
||||
<Button size="sm" class="shrink-0" @click="addManual">
|
||||
<PhPlus :size="13" />
|
||||
添加
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user