From 4137b6fe2056443fae4d3461149b6d8cf0c38d1f Mon Sep 17 00:00:00 2001 From: Sakurasan <26715255+Sakurasan@users.noreply.github.com> Date: Sun, 16 Aug 2026 18:55:05 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B8=A0=E9=81=93:=20=E8=BF=9C=E7=A8=8B?= =?UTF-8?q?=E5=80=99=E9=80=89=E6=8C=89=E6=B8=A0=E9=81=93=E8=BF=87=E6=BB=A4?= =?UTF-8?q?=20+=20key=20=E6=8E=A9=E7=A0=81=20+=20=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E6=89=B9=E9=87=8F=E5=AF=BC=E5=85=A5=E7=AB=AF=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 远程模型候选只排除本渠道已允许的模型,同名模型可被多个渠道各自允许 - 渠道 key 掩码统一 xxxxxxx******Mq4Y(保留前 7 位与后 4 位) - 移除已弃用的批量导入端点 POST /channels/:id/models/import Co-Authored-By: Claude --- server/internal/api/admin_channel_models.go | 16 +++- server/internal/api/admin_channels.go | 88 ++------------------- server/internal/api/router.go | 2 +- web/src/views/admin/ChannelModelsDrawer.vue | 24 +++--- 4 files changed, 35 insertions(+), 95 deletions(-) diff --git a/server/internal/api/admin_channel_models.go b/server/internal/api/admin_channel_models.go index 98ba153..babf801 100644 --- a/server/internal/api/admin_channel_models.go +++ b/server/internal/api/admin_channel_models.go @@ -13,7 +13,8 @@ import ( ) // AdminChannelRemoteModels GET /api/v1/admin/channels/:id/models/remote -// 拉取渠道接口的模型列表(仅预览,不绑定)。 +// 拉取渠道接口的模型列表,返回本渠道尚未允许的模型(新增候选)。 +// 每个渠道有各自的支持列表:只排除本渠道已允许的模型,其他渠道允许的同名模型仍可作为本渠道候选。 func (h *Handler) AdminChannelRemoteModels(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { @@ -53,10 +54,19 @@ func (h *Handler) AdminChannelRemoteModels(c *gin.Context) { resp.Fail(c, http.StatusBadGateway, "failed to parse model list") return } + // 本渠道已允许的上游模型名:不作为新增候选(其他渠道的模型仍可勾选) + var boundNames []string + h.a.DB.Model(&store.ChannelModelBinding{}).Where("channel_id = ?", id).Pluck("upstream_model", &boundNames) + boundSet := make(map[string]bool, len(boundNames)) + for _, n := range boundNames { + boundSet[strings.TrimSpace(n)] = true + } + items := make([]string, 0, len(list.Data)) for _, m := range list.Data { - if strings.TrimSpace(m.ID) != "" { - items = append(items, strings.TrimSpace(m.ID)) + name := strings.TrimSpace(m.ID) + if name != "" && !boundSet[name] { + items = append(items, name) } } resp.OK(c, gin.H{"items": items}) diff --git a/server/internal/api/admin_channels.go b/server/internal/api/admin_channels.go index b9d75a2..ee9dd2c 100644 --- a/server/internal/api/admin_channels.go +++ b/server/internal/api/admin_channels.go @@ -13,8 +13,6 @@ import ( "github.com/gin-gonic/gin" "github.com/openteam/server/internal/pkg/resp" "github.com/openteam/server/internal/store" - "gorm.io/gorm" - "gorm.io/gorm/clause" ) // AdminChannels GET /api/v1/admin/channels — 渠道列表(不返回加密 key,返回掩码)。 @@ -28,7 +26,7 @@ func (h *Handler) AdminChannels(c *gin.Context) { for _, ch := range chs { masked := "" if key, err := h.a.Enc.Decrypt(ch.APIKeyEnc); err == nil && len(key) > 8 { - masked = strings.Repeat("*", len(key)-4) + key[len(key)-4:] + masked = maskAPIKey(key) } else if err == nil { masked = "****" } @@ -371,87 +369,15 @@ func (h *Handler) AdminTestChannel(c *gin.Context) { resp.OK(c, gin.H{"ok": true, "latency_ms": latency, "message": msg}) } -// AdminImportChannelModels POST /api/v1/admin/channels/:id/models/import -// 拉取渠道 GET /v1/models,导入模型库并绑定。 -func (h *Handler) AdminImportChannelModels(c *gin.Context) { - id, err := strconv.ParseUint(c.Param("id"), 10, 64) - if err != nil { - resp.Fail(c, http.StatusBadRequest, "invalid channel id") - return +// maskAPIKey 掩码渠道密钥:保留前 7 位与后 4 位,中间固定 ****** 遮蔽。 +// 示例:xxxxxxx******Mq4Y;密钥较短时退化为仅保留后 4 位。 +func maskAPIKey(key string) string { + if len(key) <= 11 { + return strings.Repeat("*", len(key)-4) + key[len(key)-4:] } - var ch store.Channel - if err := h.a.DB.First(&ch, id).Error; err != nil { - resp.Fail(c, http.StatusNotFound, "channel not found") - return - } - key, err := h.a.Enc.Decrypt(ch.APIKeyEnc) - if err != nil { - resp.Fail(c, http.StatusInternalServerError, "failed to decrypt channel key") - return - } - url := ch.UpstreamURL("", "/models") - client := &http.Client{Timeout: 15 * time.Second} - req, _ := http.NewRequest(http.MethodGet, url, nil) - req.Header.Set("Authorization", "Bearer "+key) - resp2, err := client.Do(req) - if err != nil { - resp.Fail(c, http.StatusBadGateway, "failed to reach channel: "+err.Error()) - return - } - defer resp2.Body.Close() - if resp2.StatusCode != http.StatusOK { - resp.Fail(c, http.StatusBadGateway, "channel returned http "+strconv.Itoa(resp2.StatusCode)) - return - } - var list struct { - Data []struct { - ID string `json:"id"` - } `json:"data"` - } - if err := json.NewDecoder(resp2.Body).Decode(&list); err != nil { - resp.Fail(c, http.StatusBadGateway, "failed to parse model list") - return - } - if len(list.Data) == 0 { - resp.Fail(c, http.StatusNotFound, "channel returned no models") - return - } - - imported := 0 - err = h.a.DB.Transaction(func(tx *gorm.DB) error { - for _, item := range list.Data { - name := strings.TrimSpace(item.ID) - if name == "" { - continue - } - var m store.Model - if err := tx.Where("name = ?", name).FirstOrCreate(&m, store.Model{ - Name: name, DisplayName: name, Enabled: true, - }).Error; err != nil { - return err - } - // upsert 绑定(upstream_model 默认同名) - var binding store.ChannelModelBinding - err := tx.Where("channel_id = ? AND model_id = ?", ch.ID, m.ID).First(&binding).Error - if errors.Is(err, gorm.ErrRecordNotFound) { - binding = store.ChannelModelBinding{ChannelID: ch.ID, ModelID: m.ID, UpstreamModel: name, Weight: 1} - if err := tx.Create(&binding).Error; err != nil { - return err - } - } - imported++ - } - return nil - }) - if err != nil { - resp.Fail(c, http.StatusInternalServerError, "failed to import models") - return - } - resp.OK(c, gin.H{"imported": imported}) + return key[:7] + "******" + key[len(key)-4:] } -var _ = clause.Assignments // 保留 gorm/clause 引用(后续定价批处理用) - func intOr(p *int, def int) int { if p == nil { return def diff --git a/server/internal/api/router.go b/server/internal/api/router.go index 7623044..31bd6ed 100644 --- a/server/internal/api/router.go +++ b/server/internal/api/router.go @@ -103,7 +103,6 @@ func NewRouter(a *app.App, gw *proxy.Gateway) *gin.Engine { admin.PUT("/channels/:id", h.AdminUpdateChannel) admin.DELETE("/channels/:id", h.AdminDeleteChannel) admin.POST("/channels/:id/test", h.AdminTestChannel) - admin.POST("/channels/:id/models/import", h.AdminImportChannelModels) admin.GET("/channels/:id/models/remote", h.AdminChannelRemoteModels) admin.GET("/channels/:id/models", h.AdminChannelModels) admin.POST("/channels/:id/models", h.AdminChannelAddModel) @@ -111,6 +110,7 @@ func NewRouter(a *app.App, gw *proxy.Gateway) *gin.Engine { admin.DELETE("/channels/:id/models/:bid", h.AdminChannelDeleteModel) // 模型与定价 admin.GET("/models", h.AdminModels) + admin.DELETE("/models/unused", h.AdminDeleteUnusedModels) admin.POST("/models", h.AdminCreateModel) admin.PUT("/models/:id", h.AdminUpdateModel) admin.DELETE("/models/:id", h.AdminDeleteModel) diff --git a/web/src/views/admin/ChannelModelsDrawer.vue b/web/src/views/admin/ChannelModelsDrawer.vue index f08ac66..9a9ff2a 100644 --- a/web/src/views/admin/ChannelModelsDrawer.vue +++ b/web/src/views/admin/ChannelModelsDrawer.vue @@ -13,6 +13,7 @@ const mappings = ref([]) const remote = ref([]) const selected = ref([]) const loading = ref(false) +const fetched = ref(false) const addForm = reactive({ custom_name: '', upstream_model: '' }) async function load() { @@ -29,8 +30,8 @@ async function fetchRemote() { try { const { data } = await http.get(`/admin/channels/${props.channel.id}/models/remote`) remote.value = data.data.items - const bound = new Set(mappings.value.map((m) => m.upstream_model)) - selected.value = remote.value.filter((r) => bound.has(r)) + selected.value = [] + fetched.value = true } catch (e) { toast.err(errMsg(e)) } finally { @@ -39,10 +40,8 @@ async function fetchRemote() { } async function addSelected() { - const bound = new Set(mappings.value.map((m) => m.upstream_model)) let added = 0 for (const name of selected.value) { - if (bound.has(name)) continue try { await http.post(`/admin/channels/${props.channel.id}/models`, { upstream_model: name }) added++ @@ -50,8 +49,11 @@ async function addSelected() { /* 单个失败不中断 */ } } + selected.value = [] toast.ok(added ? `已添加 ${added} 个模型` : '所选均已添加') await load() + // 已添加的模型已被渠道允许,从拉取候选中移除 + await fetchRemote() } async function addManual() { @@ -98,9 +100,9 @@ onMounted(load)