feat:模型列表显示模型可用渠道

This commit is contained in:
qian committed 2026-07-23 00:32:44 +08:00
1 parent 4b13477a83
commit 52ef686202
4 files changed
+107 -1

No files matched your search

+9
View File
@@ -21,6 +21,13 @@ type ModelListRes struct {
PageSize int `json:"page_size"`
}
// ModelChannelInfo 模型关联的渠道信息
type ModelChannelInfo struct {
ChannelID int64 `json:"channel_id"`
ChannelName string `json:"channel_name"`
Type int `json:"type"`
}
// ModelItem 模型信息
type ModelItem struct {
ID int64 `json:"id"`
@@ -43,6 +50,8 @@ type ModelItem struct {
InputPrice float64 `json:"input_price"` // $/1M tokens
OutputPrice float64 `json:"output_price"` // $/1M tokens
PerRequestPrice float64 `json:"per_request_price"` // $/request(按次计费模式)
// 可用渠道列表(来自 chn_abilities + chn_channels)
Channels []ModelChannelInfo `json:"channels"`
}
// ModelCreateReq 创建模型请求
+37
View File
@@ -137,6 +137,39 @@ func (s *sAdmin) ListModels(ctx context.Context, req *v1.ModelListReq) (*v1.Mode
pricingMap[p.ModelId] = p
}
// 批量查询渠道支持情况
type channelAbilityRow struct {
ModelName string `json:"model_name"`
ChannelID int64 `json:"channel_id"`
ChannelName string `json:"channel_name"`
Type int `json:"type"`
}
modelIdStrs := make([]string, 0, len(models))
for _, m := range models {
modelIdStrs = append(modelIdStrs, m.ModelId)
}
var channelRows []channelAbilityRow
if len(modelIdStrs) > 0 {
err = dao.ChnAbilities.Ctx(ctx).
LeftJoin("chn_channels", "chn_channels.id = chn_abilities.channel_id").
Fields("chn_abilities.model_name, chn_abilities.channel_id, chn_channels.name AS channel_name, chn_channels.type").
Where("chn_abilities.model_name IN (?)", modelIdStrs).
Where("chn_abilities.enabled", true).
Where("chn_channels.status", "active").
Scan(&channelRows)
if err = common.IgnoreScanNoRows(err); err != nil {
return nil, err
}
}
channelMap := make(map[string][]v1.ModelChannelInfo, len(channelRows))
for _, r := range channelRows {
channelMap[r.ModelName] = append(channelMap[r.ModelName], v1.ModelChannelInfo{
ChannelID: r.ChannelID,
ChannelName: r.ChannelName,
Type: r.Type,
})
}
list := make([]v1.ModelItem, 0, len(models))
for _, m := range models {
item := v1.ModelItem{
@@ -171,6 +204,10 @@ func (s *sAdmin) ListModels(ctx context.Context, req *v1.ModelListReq) (*v1.Mode
item.PerRequestPrice = *p.PerRequestPrice
}
}
// 填充可用渠道
if chs, ok := channelMap[m.ModelId]; ok {
item.Channels = chs
}
list = append(list, item)
}
+39
View File
@@ -297,6 +297,8 @@ async function handleOAuthExchange() {
finally { oauthLoading.value = false }
}
const showSchedulingGuide = ref(false)
const { exporting, exportFile } = useExport({
url: '/admin/channels/export',
getFilters: () => ({
@@ -323,6 +325,29 @@ const { exporting, exportFile } = useExport({
</template>
</PageHeader>
<!-- 渠道调度说明(点击弹窗) -->
<div class="scheduling-hint" @click="showSchedulingGuide = true">
<icon-info-circle /> 了解优先级和权重如何影响渠道调度 →
</div>
<!-- 调度说明弹窗 -->
<AModal v-model:visible="showSchedulingGuide" title="渠道调度逻辑说明" :width="640" :footer="false">
<div style="font-size:14px;line-height:2;color:var(--color-text-1)">
<p>请求到达时,调度器按以下规则从启用渠道中<strong>选择一个</strong>处理:</p>
<ol style="margin:8px 0 16px 20px;padding:0">
<li style="margin-bottom:8px"><strong>排除</strong>健康度 &lt; 20 的渠道(如果全部不健康则降级使用全部)</li>
<li style="margin-bottom:8px">按<strong>优先级</strong>分组,<strong>只取最高优先级组</strong>的渠道(数字<strong>越大越优先</strong>)</li>
<li style="margin-bottom:8px">组内按<strong>权重加权随机选择</strong>(权重越高被选中的概率越大)</li>
<li style="margin-bottom:8px">同时根据<strong>健康度对权重降权</strong>:健康度 50-79 权重减半,20-49 权重降至 1/4</li>
</ol>
<div style="background:var(--color-fill-2);border-radius:8px;padding:16px;margin-top:8px">
<div style="font-weight:600;margin-bottom:8px">💡 配置示例</div>
<p style="margin:0 0 4px">两个同供应商渠道分别设 <code>优先级=100</code>(主力)和 <code>优先级=50</code>(备用),调度器始终优先使用主力渠道,仅在主力不可用时切换到备用。</p>
<p style="margin:8px 0 0">同一优先级内两个渠道分别设 <code>权重=70</code> 和 <code>权重=30</code>,则流量按 7:3 比例分发。</p>
</div>
</div>
</AModal>
<!-- Filters -->
<ACard :bordered="false" class="mb-4">
<ASpace>
@@ -474,4 +499,18 @@ const { exporting, exportFile } = useExport({
padding-top: 16px;
border-top: 1px solid var(--ta-border-light);
}
.scheduling-hint {
display: inline-flex;
align-items: center;
gap: 4px;
margin-bottom: 16px;
font-size: 13px;
color: var(--color-text-3);
cursor: pointer;
transition: color 0.2s;
user-select: none;
}
.scheduling-hint:hover {
color: rgb(var(--arcoblue-6));
}
</style>
+22 -1
View File
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { ref, reactive, onMounted, h } from 'vue'
import {
Tag, Button, Space, Popconfirm, Message,
Tag, Button, Space, Popconfirm, Message, Tooltip,
} from '@arco-design/web-vue'
import type { TableColumnData, FormInstance } from '@arco-design/web-vue'
import PageHeader from '@/components/PageHeader.vue'
@@ -214,6 +214,27 @@ const columns: TableColumnData[] = [
},
},
{
title: '可用渠道',
dataIndex: 'channels',
width: 180,
render({ record }) {
const channels = record.channels
if (!channels || channels.length === 0) {
return h('span', { style: 'color:var(--color-text-4);font-size:12px' }, '—')
}
const display = channels.slice(0, 2).map((c: any) => c.channel_name)
if (channels.length > 2) {
display.push(`+${channels.length - 2}`)
}
const trigger = h('span', { style: 'cursor:default;font-size:13px' }, display.join(', '))
if (channels.length <= 2) return trigger
const tooltip = h('div', { style: 'line-height:2;padding:4px 0' },
channels.map((c: any) => h('div', null, c.channel_name)),
)
return h(Tooltip, null, { default: () => trigger, content: () => tooltip })
},
},
{ title: '更新时间', dataIndex: 'updated_at', width: 180 },
{
title: '操作',