From 40c1ae43e9b09825be37ce7a85b2468cd045168a Mon Sep 17 00:00:00 2001 From: Sakurasan <26715255+Sakurasan@users.noreply.github.com> Date: Sun, 16 Aug 2026 01:34:22 +0800 Subject: [PATCH] =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E5=AE=9A=E4=BB=B7:=20?= =?UTF-8?q?=E4=B8=80=E8=87=B4=E6=80=A7=E6=A3=80=E6=9F=A5=E4=B8=8E=E6=8F=90?= =?UTF-8?q?=E7=A4=BA(=E6=B8=A0=E9=81=93=E5=8F=AF=E7=94=A8=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E5=BA=94=E5=85=A8=E9=83=A8=E7=BA=B3=E5=85=A5=E5=AE=9A?= =?UTF-8?q?=E4=BB=B7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - /admin/models 返回每模型 used/needs_pricing/denied + summary(total/unpriced/missing) - missing 检测孤儿绑定(渠道提供但目录缺失) - 模型定价页: 缺失警告、在用未定价提示、已禁止/未定价徽章 Co-Authored-By: Claude --- server/internal/api/admin_models.go | 75 ++++++++++++++++++++++++++++- web/src/types.ts | 10 ++++ web/src/views/admin/ModelsView.vue | 20 +++++++- 3 files changed, 101 insertions(+), 4 deletions(-) diff --git a/server/internal/api/admin_models.go b/server/internal/api/admin_models.go index bcd619e..234151e 100644 --- a/server/internal/api/admin_models.go +++ b/server/internal/api/admin_models.go @@ -1,6 +1,7 @@ package api import ( + "encoding/json" "errors" "net/http" "strconv" @@ -11,13 +12,17 @@ import ( "gorm.io/gorm" ) -// AdminModels GET /api/v1/admin/models — 模型列表(含价格与渠道绑定)。 +// AdminModels GET /api/v1/admin/models — 模型列表(含价格、渠道绑定、定价/禁止状态)。 func (h *Handler) AdminModels(c *gin.Context) { var ms []store.Model if err := h.a.DB.Order("sort ASC, id ASC").Find(&ms).Error; err != nil { resp.Fail(c, http.StatusInternalServerError, "failed to load models") return } + + // 全局模型限制策略 + allow, deny := h.modelPolicyConfig() + out := make([]gin.H, 0, len(ms)) for _, m := range ms { var bindings []store.ChannelModelBinding @@ -29,13 +34,79 @@ func (h *Handler) AdminModels(c *gin.Context) { "upstream_model": b.UpstreamModel, "weight": b.Weight, }) } + used := len(bindings) > 0 + needsPricing := used && m.InputPrice == 0 && m.OutputPrice == 0 && m.CacheReadPrice == 0 + denied := containsStr(deny, m.Name) || (len(allow) > 0 && !containsStr(allow, m.Name)) out = append(out, gin.H{ "id": m.ID, "name": m.Name, "display_name": m.DisplayName, "input_price": m.InputPrice, "output_price": m.OutputPrice, "cache_read_price": m.CacheReadPrice, "enabled": m.Enabled, "sort": m.Sort, "channels": chs, + "used": used, "needs_pricing": needsPricing, "denied": denied, }) } - resp.OK(c, gin.H{"items": out}) + + // 渠道提供了但目录中缺失的模型(孤儿绑定,数据不一致时出现) + var orphans []struct { + ChannelName string + ModelID uint64 + } + h.a.DB.Raw(`SELECT c.name as channel_name, b.model_id + FROM channel_model_bindings b + LEFT JOIN models m ON m.id = b.model_id + LEFT JOIN channels c ON c.id = b.channel_id + WHERE m.id IS NULL`).Scan(&orphans) + missing := make([]gin.H, 0, len(orphans)) + for _, o := range orphans { + missing = append(missing, gin.H{"channel": o.ChannelName, "model_id": o.ModelID}) + } + + // 在用但未定价的模型数(渠道已提供、需定价) + unpriced := 0 + { + var usedBindings []struct { + ModelID uint64 + } + h.a.DB.Model(&store.ChannelModelBinding{}).Distinct("model_id").Scan(&usedBindings) + usedIDs := map[uint64]bool{} + for _, u := range usedBindings { + usedIDs[u.ModelID] = true + } + for _, m := range ms { + if usedIDs[m.ID] && m.InputPrice == 0 && m.OutputPrice == 0 && m.CacheReadPrice == 0 { + unpriced++ + } + } + } + + resp.OK(c, gin.H{ + "items": out, + "summary": gin.H{ + "total": len(ms), + "unpriced": unpriced, + "missing": missing, + "denied_count": len(deny), + }, + }) +} + +// modelPolicyConfig 读取全局模型允许/禁止列表。 +func (h *Handler) modelPolicyConfig() (allow, deny []string) { + var raw string + h.a.DB.Model(&store.SystemConfig{}).Where("key = ?", "model_allowlist").Pluck("value", &raw) + _ = json.Unmarshal([]byte(raw), &allow) + raw = "" + h.a.DB.Model(&store.SystemConfig{}).Where("key = ?", "model_denylist").Pluck("value", &raw) + _ = json.Unmarshal([]byte(raw), &deny) + return +} + +func containsStr(list []string, s string) bool { + for _, v := range list { + if v == s { + return true + } + } + return false } // AdminCreateModel POST /api/v1/admin/models diff --git a/web/src/types.ts b/web/src/types.ts index dbe29b4..d08af97 100644 --- a/web/src/types.ts +++ b/web/src/types.ts @@ -65,6 +65,16 @@ export interface Model { enabled: boolean sort: number channels: ModelBinding[] + used?: boolean + needs_pricing?: boolean + denied?: boolean +} + +export interface ModelSummary { + total: number + unpriced: number + missing: { channel: string; model_id: number }[] + denied_count: number } export interface UsageLog { diff --git a/web/src/views/admin/ModelsView.vue b/web/src/views/admin/ModelsView.vue index 1ba04fa..262d0e4 100644 --- a/web/src/views/admin/ModelsView.vue +++ b/web/src/views/admin/ModelsView.vue @@ -6,11 +6,12 @@ import Button from '@/components/ui/Button.vue' import Input from '@/components/ui/Input.vue' import Modal from '@/components/ui/Modal.vue' import Badge from '@/components/ui/Badge.vue' -import type { Channel, Model } from '@/types' +import type { Channel, Model, ModelSummary } from '@/types' const toast = useToastStore() const models = ref([]) const channels = ref([]) +const summary = ref({ total: 0, unpriced: 0, missing: [], denied_count: 0 }) const editOpen = ref(false) const editing = ref(null) const saving = ref(false) @@ -33,6 +34,7 @@ async function load() { const [m, c] = await Promise.all([http.get('/admin/models'), http.get('/admin/channels')]) models.value = m.data.data.items channels.value = c.data.data.items + summary.value = m.data.data.summary } catch (e) { toast.err(errMsg(e)) } @@ -135,12 +137,26 @@ onMounted(load) + +
+

以下渠道提供了未纳入定价的模型

+

+ {{ x.channel }} → 模型 id#{{ x.model_id }}(请在下方添加并定价) +

+
+

+ 有 {{ summary.unpriced }} 个在用模型未定价,网关将按示例价计费 +

+

已启用渠道提供的全部模型均已纳入定价

+
-
+
{{ m.name }} {{ m.enabled ? '启用' : '停用' }} + 已禁止 + 未定价
入 {{ m.input_price }}