From dca0054e93cdf45859a0a0c9ad873647baf620ab Mon Sep 17 00:00:00 2001 From: erio Date: Mon, 30 Mar 2026 02:24:54 +0800 Subject: [PATCH] =?UTF-8?q?feat(channel):=20=E6=A8=A1=E5=9E=8B=E6=A0=87?= =?UTF-8?q?=E7=AD=BE=E8=BE=93=E5=85=A5=20+=20$/MTok=20=E4=BB=B7=E6=A0=BC?= =?UTF-8?q?=E5=8D=95=E4=BD=8D=20+=20=E5=B7=A6=E5=BC=80=E5=8F=B3=E9=97=AD?= =?UTF-8?q?=E5=8C=BA=E9=97=B4=20+=20i18n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 模型输入改为标签列表(输入回车添加,支持粘贴批量导入) - 价格显示单位改为 $/MTok(每百万 token),提交时自动转换 - Token 模式增加图片输出价格字段(适配 Gemini 图片模型按 token 计费) - 区间边界改为左开右闭 (min, max],右边界包含 - 默认价格作为未命中区间时的回退价格 - 添加完整中英文 i18n 翻译 --- backend/internal/service/channel.go | 5 +- backend/internal/service/channel_test.go | 15 +- .../components/admin/channel/IntervalRow.vue | 125 ++++---------- .../admin/channel/ModelTagInput.vue | 86 ++++++++++ .../admin/channel/PricingEntryCard.vue | 154 +++++++----------- .../src/components/admin/channel/types.ts | 34 ++-- frontend/src/i18n/locales/en.ts | 74 +++++++++ frontend/src/i18n/locales/zh.ts | 74 +++++++++ frontend/src/views/admin/ChannelsView.vue | 32 ++-- 9 files changed, 375 insertions(+), 224 deletions(-) create mode 100644 frontend/src/components/admin/channel/ModelTagInput.vue diff --git a/backend/internal/service/channel.go b/backend/internal/service/channel.go index f408f246..be82b997 100644 --- a/backend/internal/service/channel.go +++ b/backend/internal/service/channel.go @@ -110,11 +110,12 @@ func (c *Channel) GetModelPricing(model string) *ChannelModelPricing { } // FindMatchingInterval 在区间列表中查找匹配 totalTokens 的区间。 -// 通用辅助函数,供 GetIntervalForContext、ModelPricingResolver 等复用。 +// 区间为左开右闭 (min, max]:min 不含,max 包含。 +// 第一个区间 min=0 时,0 token 不匹配任何区间(回退到默认价格)。 func FindMatchingInterval(intervals []PricingInterval, totalTokens int) *PricingInterval { for i := range intervals { iv := &intervals[i] - if totalTokens >= iv.MinTokens && (iv.MaxTokens == nil || totalTokens < *iv.MaxTokens) { + if totalTokens > iv.MinTokens && (iv.MaxTokens == nil || totalTokens <= *iv.MaxTokens) { return iv } } diff --git a/backend/internal/service/channel_test.go b/backend/internal/service/channel_test.go index 004d06b1..0c055ce4 100644 --- a/backend/internal/service/channel_test.go +++ b/backend/internal/service/channel_test.go @@ -87,10 +87,13 @@ func TestGetIntervalForContext(t *testing.T) { wantNil bool }{ {"first interval", 50000, channelTestPtrFloat64(1e-6), false}, - {"boundary: at min of second", 128000, channelTestPtrFloat64(2e-6), false}, - {"boundary: at max of first (exclusive)", 128000, channelTestPtrFloat64(2e-6), false}, + // (min, max] — 128000 在第一个区间的 max,包含,所以匹配第一个 + {"boundary: max of first (inclusive)", 128000, channelTestPtrFloat64(1e-6), false}, + // 128001 > 128000,匹配第二个区间 + {"boundary: just above first max", 128001, channelTestPtrFloat64(2e-6), false}, {"unbounded interval", 500000, channelTestPtrFloat64(2e-6), false}, - {"zero tokens", 0, channelTestPtrFloat64(1e-6), false}, + // (0, max] — 0 不匹配任何区间(左开) + {"zero tokens: no match", 0, nil, true}, } for _, tt := range tests { @@ -112,8 +115,10 @@ func TestGetIntervalForContext_NoMatch(t *testing.T) { {MinTokens: 10000, MaxTokens: channelTestPtrInt(50000)}, }, } - require.Nil(t, p.GetIntervalForContext(5000)) - require.Nil(t, p.GetIntervalForContext(50000)) + require.Nil(t, p.GetIntervalForContext(5000)) // 5000 <= 10000, not > min + require.Nil(t, p.GetIntervalForContext(10000)) // 10000 not > 10000 (left-open) + require.NotNil(t, p.GetIntervalForContext(50000)) // 50000 <= 50000 (right-closed) + require.Nil(t, p.GetIntervalForContext(50001)) // 50001 > 50000 } func TestGetIntervalForContext_Empty(t *testing.T) { diff --git a/frontend/src/components/admin/channel/IntervalRow.vue b/frontend/src/components/admin/channel/IntervalRow.vue index ad7447e8..6f6e5826 100644 --- a/frontend/src/components/admin/channel/IntervalRow.vue +++ b/frontend/src/components/admin/channel/IntervalRow.vue @@ -1,125 +1,66 @@