"backend/vscode:/vscode.git/clone" did not exist on "44a93c1922fedcd6979955383b4a9530735580ed"
Commit e2ec1d30 authored by QTom's avatar QTom Committed by 陈曦
Browse files

feat(group-filter): 分组账号过滤控制 — require_oauth_only + require_privacy_set



为 OpenAI/Antigravity/Anthropic/Gemini 分组新增两个布尔控制字段:
- require_oauth_only: 创建/更新账号绑定分组时拒绝 apikey 类型加入
- require_privacy_set: 调度选号时跳过 privacy 未成功设置的账号并标记 error

后端:Ent schema 新增字段 + 迁移、Group CRUD 全链路透传、
      gateway_service 与 openai_account_scheduler 两套调度路径过滤
前端:创建/编辑表单 toggle 开关(OpenAI/Antigravity/Anthropic/Gemini 平台可见)
Co-Authored-By: default avatarClaude Opus 4.6 (1M context) <noreply@anthropic.com>
parent dfbdd8ab
......@@ -59,6 +59,8 @@ type Group struct {
// OpenAI Messages 调度配置(仅 openai 平台使用)
AllowMessagesDispatch bool
RequireOAuthOnly bool // 仅允许非 apikey 类型账号关联(OpenAI/Antigravity/Anthropic/Gemini)
RequirePrivacySet bool // 调度时仅允许 privacy 已成功设置的账号(OpenAI/Antigravity/Anthropic/Gemini)
DefaultMappedModel string
CreatedAt time.Time
......
......@@ -4,6 +4,7 @@ import (
"container/heap"
"context"
"errors"
"fmt"
"hash/fnv"
"math"
"sort"
......@@ -575,6 +576,12 @@ func (s *defaultOpenAIAccountScheduler) selectByLoadBalance(
return nil, 0, 0, 0, errors.New("no available OpenAI accounts")
}
// require_privacy_set: 获取分组信息
var schedGroup *Group
if req.GroupID != nil && s.service.schedulerSnapshot != nil {
schedGroup, _ = s.service.schedulerSnapshot.GetGroupByID(ctx, *req.GroupID)
}
filtered := make([]*Account, 0, len(accounts))
loadReq := make([]AccountWithConcurrency, 0, len(accounts))
for i := range accounts {
......@@ -587,6 +594,12 @@ func (s *defaultOpenAIAccountScheduler) selectByLoadBalance(
if !account.IsSchedulable() || !account.IsOpenAI() {
continue
}
// require_privacy_set: 跳过 privacy 未设置的账号并标记异常
if schedGroup != nil && schedGroup.RequirePrivacySet && !account.IsPrivacySet() {
_ = s.service.accountRepo.SetError(ctx, account.ID,
fmt.Sprintf("Privacy not set, required by group [%s]", schedGroup.Name))
continue
}
if req.RequestedModel != "" && !account.IsModelSupported(req.RequestedModel) {
continue
}
......
......@@ -152,6 +152,14 @@ func (s *SchedulerSnapshotService) GetAccount(ctx context.Context, accountID int
return s.accountRepo.GetByID(fallbackCtx, accountID)
}
// GetGroupByID 获取分组信息(供调度器使用)
func (s *SchedulerSnapshotService) GetGroupByID(ctx context.Context, groupID int64) (*Group, error) {
if s.groupRepo == nil {
return nil, nil
}
return s.groupRepo.GetByID(ctx, groupID)
}
// UpdateAccountInCache 立即更新 Redis 中单个账号的数据(用于模型限流后立即生效)
func (s *SchedulerSnapshotService) UpdateAccountInCache(ctx context.Context, account *Account) error {
if s.cache == nil || account == nil {
......
ALTER TABLE groups ADD COLUMN IF NOT EXISTS require_oauth_only BOOLEAN NOT NULL DEFAULT false;
ALTER TABLE groups ADD COLUMN IF NOT EXISTS require_privacy_set BOOLEAN NOT NULL DEFAULT false;
......@@ -399,6 +399,8 @@ export interface Group {
fallback_group_id_on_invalid_request: number | null
// OpenAI Messages 调度开关(用户侧需要此字段判断是否展示 Claude Code 教程)
allow_messages_dispatch?: boolean
require_oauth_only: boolean
require_privacy_set: boolean
created_at: string
updated_at: string
}
......@@ -510,6 +512,8 @@ export interface CreateGroupRequest {
mcp_xml_inject?: boolean
simulate_claude_max_enabled?: boolean
supported_model_scopes?: string[]
require_oauth_only?: boolean
require_privacy_set?: boolean
// 从指定分组复制账号
copy_accounts_from_group_ids?: number[]
}
......@@ -539,6 +543,8 @@ export interface UpdateGroupRequest {
mcp_xml_inject?: boolean
simulate_claude_max_enabled?: boolean
supported_model_scopes?: string[]
require_oauth_only?: boolean
require_privacy_set?: boolean
copy_accounts_from_group_ids?: number[]
}
......
......@@ -792,6 +792,61 @@
</div>
</div>
<!-- 账号过滤控制 (OpenAI/Antigravity/Anthropic/Gemini) -->
<div v-if="['openai', 'antigravity', 'anthropic', 'gemini'].includes(createForm.platform)" class="border-t border-gray-200 dark:border-dark-400 pt-4 mt-4 space-y-4">
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">账号过滤控制</h4>
<!-- require_oauth_only toggle -->
<div class="flex items-center justify-between">
<div>
<label class="text-sm text-gray-600 dark:text-gray-400">仅允许 OAuth 账号</label>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
{{ createForm.require_oauth_only ? '已启用 — 排除 API Key 类型账号' : '未启用' }}
</p>
</div>
<button
type="button"
@click="createForm.require_oauth_only = !createForm.require_oauth_only"
class="relative inline-flex h-6 w-12 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none"
:class="
createForm.require_oauth_only ? 'bg-primary-500' : 'bg-gray-300 dark:bg-dark-600'
"
>
<span
class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
:class="
createForm.require_oauth_only ? 'translate-x-6' : 'translate-x-1'
"
/>
</button>
</div>
<!-- require_privacy_set toggle -->
<div class="flex items-center justify-between">
<div>
<label class="text-sm text-gray-600 dark:text-gray-400">仅允许隐私保护已设置的账号</label>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
{{ createForm.require_privacy_set ? '已启用 — Privacy 未设置的账号将被排除' : '未启用' }}
</p>
</div>
<button
type="button"
@click="createForm.require_privacy_set = !createForm.require_privacy_set"
class="relative inline-flex h-6 w-12 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none"
:class="
createForm.require_privacy_set ? 'bg-primary-500' : 'bg-gray-300 dark:bg-dark-600'
"
>
<span
class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
:class="
createForm.require_privacy_set ? 'translate-x-6' : 'translate-x-1'
"
/>
</button>
</div>
</div>
<!-- 无效请求兜底(仅 anthropic/antigravity 平台,且非订阅分组) -->
<div
v-if="['anthropic', 'antigravity'].includes(createForm.platform) && createForm.subscription_type !== 'subscription'"
......@@ -1527,6 +1582,61 @@
</div>
</div>
<!-- 账号过滤控制 (OpenAI/Antigravity/Anthropic/Gemini) -->
<div v-if="['openai', 'antigravity', 'anthropic', 'gemini'].includes(editForm.platform)" class="border-t border-gray-200 dark:border-dark-400 pt-4 mt-4 space-y-4">
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">账号过滤控制</h4>
<!-- require_oauth_only toggle -->
<div class="flex items-center justify-between">
<div>
<label class="text-sm text-gray-600 dark:text-gray-400">仅允许 OAuth 账号</label>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
{{ editForm.require_oauth_only ? '已启用 — 排除 API Key 类型账号' : '未启用' }}
</p>
</div>
<button
type="button"
@click="editForm.require_oauth_only = !editForm.require_oauth_only"
class="relative inline-flex h-6 w-12 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none"
:class="
editForm.require_oauth_only ? 'bg-primary-500' : 'bg-gray-300 dark:bg-dark-600'
"
>
<span
class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
:class="
editForm.require_oauth_only ? 'translate-x-6' : 'translate-x-1'
"
/>
</button>
</div>
<!-- require_privacy_set toggle -->
<div class="flex items-center justify-between">
<div>
<label class="text-sm text-gray-600 dark:text-gray-400">仅允许隐私保护已设置的账号</label>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
{{ editForm.require_privacy_set ? '已启用 — Privacy 未设置的账号将被排除' : '未启用' }}
</p>
</div>
<button
type="button"
@click="editForm.require_privacy_set = !editForm.require_privacy_set"
class="relative inline-flex h-6 w-12 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none"
:class="
editForm.require_privacy_set ? 'bg-primary-500' : 'bg-gray-300 dark:bg-dark-600'
"
>
<span
class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
:class="
editForm.require_privacy_set ? 'translate-x-6' : 'translate-x-1'
"
/>
</button>
</div>
</div>
<!-- 无效请求兜底(仅 anthropic/antigravity 平台,且非订阅分组) -->
<div
v-if="['anthropic', 'antigravity'].includes(editForm.platform) && editForm.subscription_type !== 'subscription'"
......@@ -2063,6 +2173,9 @@ const createForm = reactive({
// OpenAI Messages 调度配置(仅 openai 平台使用)
allow_messages_dispatch: false,
default_mapped_model: 'gpt-5.4',
// 账号过滤控制(OpenAI/Antigravity 平台)
require_oauth_only: false,
require_privacy_set: false,
// 模型路由开关
model_routing_enabled: false,
// 支持的模型系列(仅 antigravity 平台)
......@@ -2307,6 +2420,9 @@ const editForm = reactive({
// OpenAI Messages 调度配置(仅 openai 平台使用)
allow_messages_dispatch: false,
default_mapped_model: '',
// 账号过滤控制(OpenAI/Antigravity 平台)
require_oauth_only: false,
require_privacy_set: false,
// 模型路由开关
model_routing_enabled: false,
// 支持的模型系列(仅 antigravity 平台)
......@@ -2452,6 +2568,8 @@ const closeCreateModal = () => {
createForm.fallback_group_id = null
createForm.fallback_group_id_on_invalid_request = null
createForm.allow_messages_dispatch = false
createForm.require_oauth_only = false
createForm.require_privacy_set = false
createForm.default_mapped_model = 'gpt-5.4'
createForm.supported_model_scopes = ['claude', 'gemini_text', 'gemini_image']
createForm.mcp_xml_inject = true
......@@ -2539,6 +2657,8 @@ const handleEdit = async (group: AdminGroup) => {
editForm.fallback_group_id = group.fallback_group_id
editForm.fallback_group_id_on_invalid_request = group.fallback_group_id_on_invalid_request
editForm.allow_messages_dispatch = group.allow_messages_dispatch || false
editForm.require_oauth_only = group.require_oauth_only ?? false
editForm.require_privacy_set = group.require_privacy_set ?? false
editForm.default_mapped_model = group.default_mapped_model || ''
editForm.model_routing_enabled = group.model_routing_enabled || false
editForm.supported_model_scopes = group.supported_model_scopes || ['claude', 'gemini_text', 'gemini_image']
......@@ -2647,6 +2767,10 @@ watch(
createForm.allow_messages_dispatch = false
createForm.default_mapped_model = ''
}
if (!['openai', 'antigravity', 'anthropic', 'gemini'].includes(newVal)) {
createForm.require_oauth_only = false
createForm.require_privacy_set = false
}
}
)
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment