feat:模型列表显示模型可用渠道
This commit is contained in:
1 parent
4b13477a83
commit
52ef686202
4 files changed
+107
-1
No files matched your search
@@ -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 创建模型请求
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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>健康度 < 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>
|
||||
@@ -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: '操作',
|
||||
|
||||
Reference in new issue
Block a user