- 远程模型候选只排除本渠道已允许的模型,同名模型可被多个渠道各自允许 - 渠道 key 掩码统一 xxxxxxx******Mq4Y(保留前 7 位与后 4 位) - 移除已弃用的批量导入端点 POST /channels/:id/models/import Co-Authored-By: Claude <noreply@anthropic.com>
179 lines
5.8 KiB
Vue
179 lines
5.8 KiB
Vue
<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 fetched = 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
|
||
selected.value = []
|
||
fetched.value = true
|
||
} catch (e) {
|
||
toast.err(errMsg(e))
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
async function addSelected() {
|
||
let added = 0
|
||
for (const name of selected.value) {
|
||
try {
|
||
await http.post(`/admin/channels/${props.channel.id}/models`, { upstream_model: name })
|
||
added++
|
||
} catch {
|
||
/* 单个失败不中断 */
|
||
}
|
||
}
|
||
selected.value = []
|
||
toast.ok(added ? `已添加 ${added} 个模型` : '所选均已添加')
|
||
await load()
|
||
// 已添加的模型已被渠道允许,从拉取候选中移除
|
||
await fetchRemote()
|
||
}
|
||
|
||
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 px-2 py-1 font-mono text-[11px] text-muted transition select-none"
|
||
:class="selected.includes(m) ? 'border-accent bg-accent-soft text-ink' : 'border-edge2 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">
|
||
{{ remote.length === 0 && fetched ? '接口返回的模型均已允许,无新增候选' : '点「拉取」获取渠道接口返回的新模型,勾选需要的加入' }}
|
||
</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>
|