From 07369f2f3002ce41966cd42c5990ce45802533fd Mon Sep 17 00:00:00 2001 From: qian Date: Mon, 3 Aug 2026 21:38:34 +0800 Subject: [PATCH] =?UTF-8?q?improve(channel):=E8=B0=83=E6=95=B4=E6=B8=A0?= =?UTF-8?q?=E9=81=93=E7=BC=96=E8=BE=91=E8=A1=A8=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/admin/v1/channel.go | 4 ++ internal/logic/admin/channel.go | 16 ++++- web/admin/src/views/ai/ChannelDetailPage.vue | 59 ++++++++++------ web/admin/src/views/ai/ChannelsPage.vue | 73 +++++++++++++------- 4 files changed, 102 insertions(+), 50 deletions(-) diff --git a/api/admin/v1/channel.go b/api/admin/v1/channel.go index ed5c10e..a51f580 100644 --- a/api/admin/v1/channel.go +++ b/api/admin/v1/channel.go @@ -30,6 +30,7 @@ type ChannelItem struct { Status string `json:"status"` Priority int `json:"priority"` Weight int `json:"weight"` + MaxConcurrency int `json:"max_concurrency"` Tier string `json:"tier"` StrictCapacity bool `json:"strict_capacity"` TestModel string `json:"test_model"` @@ -52,6 +53,7 @@ type ChannelCreateReq struct { ApiKey string `json:"api_key" v:"required#请输入 API Key" dc:"API Key"` Priority int `json:"priority" d:"0" dc:"优先级"` Weight int `json:"weight" d:"100" v:"between:0,100" dc:"权重"` + MaxConcurrency int `json:"max_concurrency" d:"100" v:"min:0" dc:"最大并发请求数(0=按上游 429 水位自动估算)"` Tier string `json:"tier" d:"primary" v:"in:primary,secondary,reserve" dc:"调度层级:primary=首选 secondary=备用 reserve=保底"` StrictCapacity bool `json:"strict_capacity" d:"false" dc:"严格容量:Redis 故障时实例级保守限额(fail-closed),用于高成本渠道"` TestModel string `json:"test_model" dc:"测试模型名"` @@ -78,6 +80,7 @@ type ChannelUpdateReq struct { ApiKey *string `json:"api_key" dc:"更新 API Key(留空不更新)"` Priority int `json:"priority" dc:"优先级"` Weight int `json:"weight" dc:"权重"` + MaxConcurrency *int `json:"max_concurrency" v:"min:0" dc:"最大并发请求数(0=按上游 429 水位自动估算,留空不更新)"` TestModel string `json:"test_model" dc:"测试模型名"` Remark string `json:"remark" dc:"备注"` Status string `json:"status" v:"in:active,disabled,testing" dc:"状态"` @@ -112,6 +115,7 @@ type ChannelDetailRes struct { Status string `json:"status"` Priority int `json:"priority"` Weight int `json:"weight"` + MaxConcurrency int `json:"max_concurrency"` Tier string `json:"tier"` StrictCapacity bool `json:"strict_capacity"` TestModel string `json:"test_model"` diff --git a/internal/logic/admin/channel.go b/internal/logic/admin/channel.go index f6e9b54..f30f304 100644 --- a/internal/logic/admin/channel.go +++ b/internal/logic/admin/channel.go @@ -68,6 +68,7 @@ func (s *sAdmin) ListChannels(ctx context.Context, req *v1.ChannelListReq) (*v1. Status string `json:"status"` Priority int `json:"priority"` Weight int `json:"weight"` + MaxConcurrency int `json:"max_concurrency"` Tier string `json:"tier"` StrictCapacity bool `json:"strict_capacity"` TestModel string `json:"test_model"` @@ -81,7 +82,7 @@ func (s *sAdmin) ListChannels(ctx context.Context, req *v1.ChannelListReq) (*v1. Settings string `json:"settings"` } - err := query.Fields("chn_channels.id, chn_channels.name, chn_channels.type, chn_channels.base_url, chn_channels.status, chn_channels.priority, chn_channels.weight, chn_channels.tier, chn_channels.strict_capacity, chn_channels.test_model, chn_channels.remark, chn_channels.is_vip, chn_channels.sharing_threshold, chn_channels.preemption_threshold, chn_channels.borrowing_cooldown_seconds, chn_channels.created_at, h.health_score, chn_channels.settings"). + err := query.Fields("chn_channels.id, chn_channels.name, chn_channels.type, chn_channels.base_url, chn_channels.status, chn_channels.priority, chn_channels.weight, chn_channels.max_concurrency, chn_channels.tier, chn_channels.strict_capacity, chn_channels.test_model, chn_channels.remark, chn_channels.is_vip, chn_channels.sharing_threshold, chn_channels.preemption_threshold, chn_channels.borrowing_cooldown_seconds, chn_channels.created_at, h.health_score, chn_channels.settings"). // 列表默认按 id 倒序:新建渠道排在最前,便于运营查看最新配置。 // 注意分页场景下排序必须在 DB 层完成,前端只在当前页排序会导致跨页错乱。 OrderDesc("chn_channels.id"). @@ -109,6 +110,7 @@ func (s *sAdmin) ListChannels(ctx context.Context, req *v1.ChannelListReq) (*v1. Status: ch.Status, Priority: ch.Priority, Weight: ch.Weight, + MaxConcurrency: ch.MaxConcurrency, Tier: ch.Tier, StrictCapacity: ch.StrictCapacity, TestModel: ch.TestModel, @@ -141,6 +143,7 @@ func (s *sAdmin) CloneChannel(ctx context.Context, req *v1.ChannelCloneReq) (*v1 BaseURL string `json:"base_url"` Priority int `json:"priority"` Weight int `json:"weight"` + MaxConcurrency int `json:"max_concurrency"` TestModel string `json:"test_model"` Remark string `json:"remark"` Settings string `json:"settings"` @@ -150,7 +153,7 @@ func (s *sAdmin) CloneChannel(ctx context.Context, req *v1.ChannelCloneReq) (*v1 BorrowingCooldownSeconds int `json:"borrowing_cooldown_seconds"` } err := dao.ChnChannels.Ctx(ctx). - Fields("id, name, type, base_url, priority, weight, test_model, remark, settings, is_vip, sharing_threshold, preemption_threshold, borrowing_cooldown_seconds"). + Fields("id, name, type, base_url, priority, weight, max_concurrency, test_model, remark, settings, is_vip, sharing_threshold, preemption_threshold, borrowing_cooldown_seconds"). Where("id", req.ID). Scan(&src) if err != nil { @@ -184,6 +187,7 @@ func (s *sAdmin) CloneChannel(ctx context.Context, req *v1.ChannelCloneReq) (*v1 Status: "active", Priority: src.Priority, Weight: src.Weight, + MaxConcurrency: src.MaxConcurrency, TestModel: src.TestModel, Remark: src.Remark, Settings: src.Settings, @@ -265,6 +269,7 @@ func (s *sAdmin) CreateChannel(ctx context.Context, req *v1.ChannelCreateReq) (* Status: "active", Priority: req.Priority, Weight: req.Weight, + MaxConcurrency: req.MaxConcurrency, Tier: tier, StrictCapacity: req.StrictCapacity, TestModel: req.TestModel, @@ -317,6 +322,9 @@ func (s *sAdmin) UpdateChannel(ctx context.Context, req *v1.ChannelUpdateReq) (* } data.Priority = req.Priority data.Weight = req.Weight + if req.MaxConcurrency != nil { + data.MaxConcurrency = *req.MaxConcurrency + } if req.TestModel != "" { data.TestModel = req.TestModel } @@ -426,6 +434,7 @@ func (s *sAdmin) GetChannelDetail(ctx context.Context, req *v1.ChannelDetailReq) Status string `json:"status"` Priority int `json:"priority"` Weight int `json:"weight"` + MaxConcurrency int `json:"max_concurrency"` Tier string `json:"tier"` StrictCapacity bool `json:"strict_capacity"` TestModel string `json:"test_model"` @@ -442,7 +451,7 @@ func (s *sAdmin) GetChannelDetail(ctx context.Context, req *v1.ChannelDetailReq) err := dao.ChnChannels.Ctx(ctx). LeftJoin("chn_health_scores h ON chn_channels.id = h.channel_id"). - Fields("chn_channels.id, chn_channels.name, chn_channels.type, chn_channels.base_url, chn_channels.status, chn_channels.priority, chn_channels.weight, chn_channels.tier, chn_channels.strict_capacity, chn_channels.test_model, chn_channels.remark, chn_channels.is_vip, chn_channels.settings, chn_channels.sharing_threshold, chn_channels.preemption_threshold, chn_channels.borrowing_cooldown_seconds, chn_channels.created_at, chn_channels.updated_at, h.health_score"). + Fields("chn_channels.id, chn_channels.name, chn_channels.type, chn_channels.base_url, chn_channels.status, chn_channels.priority, chn_channels.weight, chn_channels.max_concurrency, chn_channels.tier, chn_channels.strict_capacity, chn_channels.test_model, chn_channels.remark, chn_channels.is_vip, chn_channels.settings, chn_channels.sharing_threshold, chn_channels.preemption_threshold, chn_channels.borrowing_cooldown_seconds, chn_channels.created_at, chn_channels.updated_at, h.health_score"). Where("chn_channels.id", req.ID). Scan(&ch) if err != nil { @@ -486,6 +495,7 @@ func (s *sAdmin) GetChannelDetail(ctx context.Context, req *v1.ChannelDetailReq) Status: ch.Status, Priority: ch.Priority, Weight: ch.Weight, + MaxConcurrency: ch.MaxConcurrency, Tier: ch.Tier, StrictCapacity: ch.StrictCapacity, TestModel: ch.TestModel, diff --git a/web/admin/src/views/ai/ChannelDetailPage.vue b/web/admin/src/views/ai/ChannelDetailPage.vue index 2bc2a13..75072f0 100644 --- a/web/admin/src/views/ai/ChannelDetailPage.vue +++ b/web/admin/src/views/ai/ChannelDetailPage.vue @@ -58,6 +58,7 @@ const editForm = reactive({ base_url: '', priority: 0, weight: 100, + max_concurrency: 100, tier: 'primary', strict_capacity: false, test_model: '', @@ -78,6 +79,7 @@ function openEditModal() { base_url: detail.value.base_url || '', priority: detail.value.priority || 0, weight: detail.value.weight || 100, + max_concurrency: detail.value.max_concurrency ?? 100, tier: detail.value.tier || 'primary', strict_capacity: detail.value.strict_capacity || false, test_model: detail.value.test_model || '', @@ -511,6 +513,10 @@ function formatHeaders(headers: Record): string { {{ detail.priority }} {{ detail.weight }} + + {{ detail.max_concurrency }} + 自动 + {{ tierLabel[detail.tier] || detail.tier || '-' }} @@ -731,41 +737,50 @@ function formatHeaders(headers: Record): string { - -
调度与状态
+ +
调度与容量
- + - + - + + + + + + + + + + + + + + + + + - - - -
功能与测试
- - - - - - - - + + + + + @@ -775,16 +790,18 @@ function formatHeaders(headers: Record): string { - + - + - + + + - + diff --git a/web/admin/src/views/ai/ChannelsPage.vue b/web/admin/src/views/ai/ChannelsPage.vue index f8bba44..4fa6ed1 100644 --- a/web/admin/src/views/ai/ChannelsPage.vue +++ b/web/admin/src/views/ai/ChannelsPage.vue @@ -175,10 +175,10 @@ const providerDefaultURLs: Record = { 35: 'https://api.openai.com', } -const form = reactive({ name: '', type: 1, base_url: '', api_key: '', priority: 0, weight: 100, tier: 'primary', strict_capacity: false, test_model: '', remark: '', status: 'active', is_vip: false, use_proxy: false, sharing_threshold: null as number | null, preemption_threshold: null as number | null, borrowing_cooldown_seconds: null as number | null }) +const form = reactive({ name: '', type: 1, base_url: '', api_key: '', priority: 0, weight: 100, max_concurrency: 100, tier: 'primary', strict_capacity: false, test_model: '', remark: '', status: 'active', is_vip: false, use_proxy: false, sharing_threshold: null as number | null, preemption_threshold: null as number | null, borrowing_cooldown_seconds: null as number | null }) function openCreate() { - Object.assign(form, { name: '', type: 1, base_url: '', api_key: '', priority: 0, weight: 100, tier: 'primary', strict_capacity: false, test_model: '', remark: '', status: 'active', is_vip: false, use_proxy: false, sharing_threshold: null, preemption_threshold: null, borrowing_cooldown_seconds: null }) + Object.assign(form, { name: '', type: 1, base_url: '', api_key: '', priority: 0, weight: 100, max_concurrency: 100, tier: 'primary', strict_capacity: false, test_model: '', remark: '', status: 'active', is_vip: false, use_proxy: false, sharing_threshold: null, preemption_threshold: null, borrowing_cooldown_seconds: null }) showModal.value = true } @@ -372,7 +372,8 @@ const { exporting, exportFile } = useExport({ - + +
基础信息
@@ -392,40 +393,51 @@ const { exporting, exportFile } = useExport({ - + +
调度与容量
- + - + - - - + + + + + + + + + + + + - - - - - - - + + + - - - - + + + + + + + + + @@ -434,18 +446,18 @@ const { exporting, exportFile } = useExport({ - + - + - - - - + + + + @@ -586,4 +598,13 @@ const { exporting, exportFile } = useExport({ color: var(--color-text-3); font-size: 12px; } +.form-group-title { + font-size: 13px; + font-weight: 600; + color: var(--color-text-2); + margin: 12px 0 12px; + padding-left: 8px; + border-left: 3px solid #165dff; + line-height: 1.2; +}