Просмотр исходного кода

✨ feat: enhance model billing aggregation & UI display for unknown quota type

Summary
-------
1. **Backend**
   • `controller/model_meta.go`
     – For prefix/suffix/contains rules, aggregate endpoints, bound channels, enable groups, and quota types across all matched models.
     – When mixed billing types are detected, return `quota_type = -1` (unknown) instead of defaulting to volume-based.

2. **Frontend**
   • `web/src/helpers/utils.js`
     – `calculateModelPrice` now handles `quota_type = -1`, returning placeholder `'-'`.

   • `web/src/components/table/model-pricing/view/card/PricingCardView.jsx`
     – Billing tag logic updated: displays “按次计费” (times), “按量计费” (volume), or `'-'` for unknown.

   • `web/src/components/table/model-pricing/view/table/PricingTableColumns.js`
     – `renderQuotaType` shows “未知” for unknown billing type.

   • `web/src/components/table/models/ModelsColumnDefs.js`
     – Unified `renderQuotaType` to return `'-'` when type is unknown.

   • `web/src/components/table/model-pricing/modal/components/ModelPricingTable.jsx`
     – Group price table honors unknown billing type; pricing columns show `'-'` and neutral tag color.

3. **Utilities**
   • Added safe fallback colours/tags for unknown billing type across affected components.

Impact
------
• Ensures correct data aggregation for non-exact model matches.
• Prevents UI from implying volume billing when actual type is ambiguous.
• Provides consistent placeholder display (`'-'` or “未知”) across cards, tables and modals.

No breaking API changes; frontend gracefully handles legacy values.
t0ng7u 6 месяцев назад
Родитель
Сommit
94bd44d0f2

+ 97 - 7
controller/model_meta.go

@@ -3,8 +3,10 @@ package controller
 import (
 	"encoding/json"
 	"strconv"
+	"strings"
 
 	"one-api/common"
+	"one-api/constant"
 	"one-api/model"
 
 	"github.com/gin-gonic/gin"
@@ -162,17 +164,105 @@ func DeleteModelMeta(c *gin.Context) {
 
 // 辅助函数:填充 Endpoints 和 BoundChannels 和 EnableGroups
 func fillModelExtra(m *model.Model) {
-	if m.Endpoints == "" {
-		eps := model.GetModelSupportEndpointTypes(m.ModelName)
+	// 若为精确匹配,保持原有逻辑
+	if m.NameRule == model.NameRuleExact {
+		if m.Endpoints == "" {
+			eps := model.GetModelSupportEndpointTypes(m.ModelName)
+			if b, err := json.Marshal(eps); err == nil {
+				m.Endpoints = string(b)
+			}
+		}
+		if channels, err := model.GetBoundChannels(m.ModelName); err == nil {
+			m.BoundChannels = channels
+		}
+		m.EnableGroups = model.GetModelEnableGroups(m.ModelName)
+		m.QuotaType = model.GetModelQuotaType(m.ModelName)
+		return
+	}
+
+	// 非精确匹配:计算并集
+	pricings := model.GetPricing()
+
+	// 端点去重集合
+	endpointSet := make(map[constant.EndpointType]struct{})
+	// 已绑定渠道去重集合
+	channelSet := make(map[string]model.BoundChannel)
+	// 分组去重集合
+	groupSet := make(map[string]struct{})
+	// 计费类型(若有任意模型为 1,则返回 1)
+	quotaTypeSet := make(map[int]struct{})
+
+	for _, p := range pricings {
+		var matched bool
+		switch m.NameRule {
+		case model.NameRulePrefix:
+			matched = strings.HasPrefix(p.ModelName, m.ModelName)
+		case model.NameRuleSuffix:
+			matched = strings.HasSuffix(p.ModelName, m.ModelName)
+		case model.NameRuleContains:
+			matched = strings.Contains(p.ModelName, m.ModelName)
+		}
+		if !matched {
+			continue
+		}
+
+		// 收集端点
+		for _, et := range p.SupportedEndpointTypes {
+			endpointSet[et] = struct{}{}
+		}
+
+		// 收集分组
+		for _, g := range p.EnableGroup {
+			groupSet[g] = struct{}{}
+		}
+
+		// 收集计费类型
+		quotaTypeSet[p.QuotaType] = struct{}{}
+
+		// 收集渠道
+		if channels, err := model.GetBoundChannels(p.ModelName); err == nil {
+			for _, ch := range channels {
+				key := ch.Name + "_" + strconv.Itoa(ch.Type)
+				channelSet[key] = ch
+			}
+		}
+	}
+
+	// 序列化端点
+	if len(endpointSet) > 0 && m.Endpoints == "" {
+		eps := make([]constant.EndpointType, 0, len(endpointSet))
+		for et := range endpointSet {
+			eps = append(eps, et)
+		}
 		if b, err := json.Marshal(eps); err == nil {
 			m.Endpoints = string(b)
 		}
 	}
-	if channels, err := model.GetBoundChannels(m.ModelName); err == nil {
+
+	// 序列化渠道
+	if len(channelSet) > 0 {
+		channels := make([]model.BoundChannel, 0, len(channelSet))
+		for _, ch := range channelSet {
+			channels = append(channels, ch)
+		}
 		m.BoundChannels = channels
 	}
-	// 填充启用分组
-	m.EnableGroups = model.GetModelEnableGroups(m.ModelName)
-	// 填充计费类型
-	m.QuotaType = model.GetModelQuotaType(m.ModelName)
+
+	// 序列化分组
+	if len(groupSet) > 0 {
+		groups := make([]string, 0, len(groupSet))
+		for g := range groupSet {
+			groups = append(groups, g)
+		}
+		m.EnableGroups = groups
+	}
+
+	// 确定计费类型:仅当所有匹配模型计费类型一致时才返回该类型,否则返回 -1 表示未知/不确定
+	if len(quotaTypeSet) == 1 {
+		for k := range quotaTypeSet {
+			m.QuotaType = k
+		}
+	} else {
+		m.QuotaType = -1
+	}
 }

+ 11 - 6
web/src/components/table/model-pricing/modal/components/ModelPricingTable.jsx

@@ -63,7 +63,7 @@ const ModelPricingTable = ({
         key: group,
         group: group,
         ratio: groupRatioValue,
-        billingType: modelData?.quota_type === 0 ? t('按量计费') : t('按次计费'),
+        billingType: modelData?.quota_type === 0 ? t('按量计费') : (modelData?.quota_type === 1 ? t('按次计费') : '-'),
         inputPrice: modelData?.quota_type === 0 ? priceData.inputPrice : '-',
         outputPrice: modelData?.quota_type === 0 ? (priceData.completionPrice || priceData.outputPrice) : '-',
         fixedPrice: modelData?.quota_type === 1 ? priceData.price : '-',
@@ -100,11 +100,16 @@ const ModelPricingTable = ({
     columns.push({
       title: t('计费类型'),
       dataIndex: 'billingType',
-      render: (text) => (
-        <Tag color={text === t('按量计费') ? 'violet' : 'teal'} size="small" shape="circle">
-          {text}
-        </Tag>
-      ),
+      render: (text) => {
+        let color = 'white';
+        if (text === t('按量计费')) color = 'violet';
+        else if (text === t('按次计费')) color = 'teal';
+        return (
+          <Tag color={color} size="small" shape="circle">
+            {text || '-'}
+          </Tag>
+        );
+      },
     });
 
     // 根据计费类型添加价格列

+ 16 - 5
web/src/components/table/model-pricing/view/card/PricingCardView.jsx

@@ -144,13 +144,24 @@ const PricingCardView = ({
   // 渲染标签
   const renderTags = (record) => {
     // 计费类型标签(左边)
-    const billingType = record.quota_type === 1 ? 'teal' : 'violet';
-    const billingText = record.quota_type === 1 ? t('按次计费') : t('按量计费');
-    const billingTag = (
-      <Tag key="billing" shape='circle' color={billingType} size='small'>
-        {billingText}
+    let billingTag = (
+      <Tag key="billing" shape='circle' color='white' size='small'>
+        -
       </Tag>
     );
+    if (record.quota_type === 1) {
+      billingTag = (
+        <Tag key="billing" shape='circle' color='teal' size='small'>
+          {t('按次计费')}
+        </Tag>
+      );
+    } else if (record.quota_type === 0) {
+      billingTag = (
+        <Tag key="billing" shape='circle' color='violet' size='small'>
+          {t('按量计费')}
+        </Tag>
+      );
+    }
 
     // 自定义标签(右边)
     const customTags = [];

+ 2 - 1
web/src/components/table/models/ModelsColumnDefs.js

@@ -137,7 +137,8 @@ const renderQuotaType = (qt, t) => {
       </Tag>
     );
   }
-  return qt ?? '-';
+  // 未知
+  return '-';
 };
 
 // Render bound channels

+ 14 - 4
web/src/helpers/utils.js

@@ -632,12 +632,22 @@ export const calculateModelPrice = ({
     };
   }
 
-  // 按次计费
-  const priceUSD = parseFloat(record.model_price) * usedGroupRatio;
-  const displayVal = displayPrice(priceUSD);
+  if (record.quota_type === 1) {
+    // 按次计费
+    const priceUSD = parseFloat(record.model_price) * usedGroupRatio;
+    const displayVal = displayPrice(priceUSD);
 
+    return {
+      price: displayVal,
+      isPerToken: false,
+      usedGroup,
+      usedGroupRatio,
+    };
+  }
+
+  // 未知计费类型,返回占位信息
   return {
-    price: displayVal,
+    price: '-',
     isPerToken: false,
     usedGroup,
     usedGroupRatio,