diff --git a/backend/internal/handler/admin/group_handler.go b/backend/internal/handler/admin/group_handler.go index dfbd4eba6f..1c3ecb07e1 100644 --- a/backend/internal/handler/admin/group_handler.go +++ b/backend/internal/handler/admin/group_handler.go @@ -98,7 +98,7 @@ func NewGroupHandler(adminService service.AdminService, dashboardService *servic type CreateGroupRequest struct { Name string `json:"name" binding:"required"` Description string `json:"description"` - Platform string `json:"platform" binding:"omitempty,oneof=anthropic openai gemini antigravity grok composite"` + Platform string `json:"platform" binding:"omitempty,oneof=anthropic openai gemini antigravity grok kimi zhipu deepseek composite"` RateMultiplier float64 `json:"rate_multiplier"` IsExclusive bool `json:"is_exclusive"` SubscriptionType string `json:"subscription_type" binding:"omitempty,oneof=standard subscription"` @@ -166,7 +166,7 @@ type CreateGroupRequest struct { type UpdateGroupRequest struct { Name string `json:"name"` Description *string `json:"description"` - Platform string `json:"platform" binding:"omitempty,oneof=anthropic openai gemini antigravity grok composite"` + Platform string `json:"platform" binding:"omitempty,oneof=anthropic openai gemini antigravity grok kimi zhipu deepseek composite"` RateMultiplier *float64 `json:"rate_multiplier"` IsExclusive *bool `json:"is_exclusive"` Status string `json:"status" binding:"omitempty,oneof=active inactive"` diff --git a/backend/internal/handler/admin/group_handler_platform_test.go b/backend/internal/handler/admin/group_handler_platform_test.go new file mode 100644 index 0000000000..ca1703180c --- /dev/null +++ b/backend/internal/handler/admin/group_handler_platform_test.go @@ -0,0 +1,83 @@ +//go:build unit + +package admin + +import ( + "bytes" + "fmt" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/require" +) + +// 回归分组平台枚举:kimi/zhipu/deepseek 必须能通过 Create/Update 的 binding 校验 +// (历史 bug:调度/路由链路已支持 CN 平台分组,但 oneof 白名单漏加三平台,导致 +// 平台分组无法创建、CN 账号"无可用分组");非法值仍须被拒。 +func bindGroupPlatformJSON(t *testing.T, target any, body string) error { + t.Helper() + gin.SetMode(gin.TestMode) + c, _ := gin.CreateTestContext(httptest.NewRecorder()) + c.Request = httptest.NewRequest("POST", "/", bytes.NewBufferString(body)) + c.Request.Header.Set("Content-Type", "application/json") + return c.ShouldBindJSON(target) +} + +func TestGroupPlatformBinding_AllowedPlatforms(t *testing.T) { + allowed := []string{ + "anthropic", "openai", "gemini", "antigravity", "grok", + "kimi", "zhipu", "deepseek", "composite", + } + for _, platform := range allowed { + t.Run("create_"+platform, func(t *testing.T) { + var req CreateGroupRequest + body := fmt.Sprintf(`{"name":"g","platform":%q}`, platform) + require.NoError(t, bindGroupPlatformJSON(t, &req, body), + "platform %q 应通过 CreateGroupRequest 校验", platform) + require.Equal(t, platform, req.Platform) + }) + t.Run("update_"+platform, func(t *testing.T) { + var req UpdateGroupRequest + body := fmt.Sprintf(`{"platform":%q}`, platform) + require.NoError(t, bindGroupPlatformJSON(t, &req, body), + "platform %q 应通过 UpdateGroupRequest 校验", platform) + require.Equal(t, platform, req.Platform) + }) + } +} + +func TestGroupPlatformBinding_RejectsInvalidPlatforms(t *testing.T) { + invalid := []string{ + "moonshot", // 厂商别名,不是平台标识 + "Kimi", // 大小写敏感 + "openai ", // 尾随空格 + "glm", + "bogus", + } + for _, platform := range invalid { + t.Run("create_"+platform, func(t *testing.T) { + var req CreateGroupRequest + body := fmt.Sprintf(`{"name":"g","platform":%q}`, platform) + require.Error(t, bindGroupPlatformJSON(t, &req, body), + "platform %q 应被 CreateGroupRequest 拒绝", platform) + }) + t.Run("update_"+platform, func(t *testing.T) { + var req UpdateGroupRequest + body := fmt.Sprintf(`{"platform":%q}`, platform) + require.Error(t, bindGroupPlatformJSON(t, &req, body), + "platform %q 应被 UpdateGroupRequest 拒绝", platform) + }) + } +} + +// 守住 composite 路由目标不放行 CN:CN 平台不可作为 composite 路由目标 +// (DetectModelPlatform/isConcreteRequestPlatform 均无 CN 分支,放行即打开半实现路径)。 +func TestCompositeRouteTargetPlatform_StillExcludesCNProviders(t *testing.T) { + for _, platform := range []string{"kimi", "zhipu", "deepseek"} { + var req CompositeRouteRequest + body := fmt.Sprintf(`{"public_model":"m","target_platform":%q}`, platform) + require.Error(t, bindGroupPlatformJSON(t, &req, body), + "composite target_platform %q 应保持被拒", platform) + } +} diff --git a/frontend/src/components/common/GroupBadge.vue b/frontend/src/components/common/GroupBadge.vue index ee4b4b9154..8bb615781b 100644 --- a/frontend/src/components/common/GroupBadge.vue +++ b/frontend/src/components/common/GroupBadge.vue @@ -162,6 +162,15 @@ const labelClass = computed(() => { if (props.platform === 'grok') { return `${base} bg-zinc-300/70 text-zinc-800 dark:bg-zinc-700/60 dark:text-zinc-200` } + if (props.platform === 'kimi') { + return `${base} bg-pink-200/60 text-pink-800 dark:bg-pink-800/40 dark:text-pink-300` + } + if (props.platform === 'zhipu') { + return `${base} bg-indigo-200/60 text-indigo-800 dark:bg-indigo-800/40 dark:text-indigo-300` + } + if (props.platform === 'deepseek') { + return `${base} bg-teal-200/60 text-teal-800 dark:bg-teal-800/40 dark:text-teal-300` + } if (props.platform === 'composite') { return `${base} bg-cyan-200/70 text-cyan-900 dark:bg-cyan-900/50 dark:text-cyan-300` } @@ -200,6 +209,21 @@ const badgeClass = computed(() => { ? 'bg-zinc-200 text-zinc-800 dark:bg-zinc-700 dark:text-zinc-100' : 'bg-zinc-100 text-zinc-700 dark:bg-zinc-800 dark:text-zinc-200' } + if (props.platform === 'kimi') { + return isSubscription.value + ? 'bg-pink-100 text-pink-700 dark:bg-pink-900/30 dark:text-pink-400' + : 'bg-pink-50 text-pink-700 dark:bg-pink-900/20 dark:text-pink-400' + } + if (props.platform === 'zhipu') { + return isSubscription.value + ? 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900/30 dark:text-indigo-400' + : 'bg-indigo-50 text-indigo-700 dark:bg-indigo-900/20 dark:text-indigo-400' + } + if (props.platform === 'deepseek') { + return isSubscription.value + ? 'bg-teal-100 text-teal-700 dark:bg-teal-900/30 dark:text-teal-400' + : 'bg-teal-50 text-teal-700 dark:bg-teal-900/20 dark:text-teal-400' + } if (props.platform === 'composite') { return isSubscription.value ? 'bg-cyan-100 text-cyan-800 dark:bg-cyan-900/30 dark:text-cyan-300' diff --git a/frontend/src/i18n/locales/en/admin/overview.ts b/frontend/src/i18n/locales/en/admin/overview.ts index 225865db9d..b10dc0d7d1 100644 --- a/frontend/src/i18n/locales/en/admin/overview.ts +++ b/frontend/src/i18n/locales/en/admin/overview.ts @@ -950,6 +950,9 @@ export default { gemini: 'Gemini', antigravity: 'Antigravity', grok: 'Grok', + kimi: 'Kimi', + zhipu: 'Zhipu GLM', + deepseek: 'DeepSeek', composite: 'Composite', }, deleteConfirm: diff --git a/frontend/src/i18n/locales/zh/admin/overview.ts b/frontend/src/i18n/locales/zh/admin/overview.ts index 200bb6c547..e32cadc641 100644 --- a/frontend/src/i18n/locales/zh/admin/overview.ts +++ b/frontend/src/i18n/locales/zh/admin/overview.ts @@ -883,6 +883,9 @@ export default { gemini: 'Gemini', antigravity: 'Antigravity', grok: 'Grok', + kimi: 'Kimi', + zhipu: 'Zhipu GLM', + deepseek: 'DeepSeek', composite: 'Composite', }, saving: '保存中...', diff --git a/frontend/src/views/admin/GroupsView.vue b/frontend/src/views/admin/GroupsView.vue index 904b8926d4..9c3c36558e 100644 --- a/frontend/src/views/admin/GroupsView.vue +++ b/frontend/src/views/admin/GroupsView.vue @@ -146,7 +146,13 @@ ? 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400' : value === 'grok' ? 'bg-zinc-200 text-zinc-800 dark:bg-zinc-700 dark:text-zinc-100' - : 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400', + : value === 'kimi' + ? 'bg-pink-100 text-pink-700 dark:bg-pink-900/30 dark:text-pink-400' + : value === 'zhipu' + ? 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900/30 dark:text-indigo-400' + : value === 'deepseek' + ? 'bg-teal-100 text-teal-700 dark:bg-teal-900/30 dark:text-teal-400' + : 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400', ]" > @@ -3965,7 +3971,13 @@ ? 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400' : group.platform === 'grok' ? 'bg-zinc-200 text-zinc-800 dark:bg-zinc-700 dark:text-zinc-100' - : 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400', + : group.platform === 'kimi' + ? 'bg-pink-100 text-pink-700 dark:bg-pink-900/30 dark:text-pink-400' + : group.platform === 'zhipu' + ? 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900/30 dark:text-indigo-400' + : group.platform === 'deepseek' + ? 'bg-teal-100 text-teal-700 dark:bg-teal-900/30 dark:text-teal-400' + : 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400', ]" > {{ t("admin.groups.platforms." + group.platform) }} @@ -4727,6 +4739,9 @@ const platformOptions = computed(() => [ { value: "gemini", label: "Gemini" }, { value: "antigravity", label: "Antigravity" }, { value: "grok", label: "Grok" }, + { value: "kimi", label: "Kimi" }, + { value: "zhipu", label: "Zhipu GLM" }, + { value: "deepseek", label: "DeepSeek" }, { value: "composite", label: "Composite" }, ]); @@ -4737,6 +4752,9 @@ const platformFilterOptions = computed(() => [ { value: "gemini", label: "Gemini" }, { value: "antigravity", label: "Antigravity" }, { value: "grok", label: "Grok" }, + { value: "kimi", label: "Kimi" }, + { value: "zhipu", label: "Zhipu GLM" }, + { value: "deepseek", label: "DeepSeek" }, { value: "composite", label: "Composite" }, ]);