"frontend/src/i18n/git@web.lueluesay.top:chenxi/sub2api.git" did not exist on "23686b13915515a70018f57c5e112446a7a6432d"
Unverified Commit 820c5318 authored by Wesley Liddick's avatar Wesley Liddick Committed by GitHub
Browse files

Merge pull request #1406 from DaydreamCoding/feat/group-account-filter

feat(group-filter): 分组账号过滤控制 — require_oauth_only + require_privacy_set
parents 1727b8df aeed2eb9
...@@ -59,6 +59,8 @@ type Group struct { ...@@ -59,6 +59,8 @@ type Group struct {
// OpenAI Messages 调度配置(仅 openai 平台使用) // OpenAI Messages 调度配置(仅 openai 平台使用)
AllowMessagesDispatch bool AllowMessagesDispatch bool
RequireOAuthOnly bool // 仅允许非 apikey 类型账号关联(OpenAI/Antigravity/Anthropic/Gemini)
RequirePrivacySet bool // 调度时仅允许 privacy 已成功设置的账号(OpenAI/Antigravity/Anthropic/Gemini)
DefaultMappedModel string DefaultMappedModel string
CreatedAt time.Time CreatedAt time.Time
......
...@@ -4,6 +4,7 @@ import ( ...@@ -4,6 +4,7 @@ import (
"container/heap" "container/heap"
"context" "context"
"errors" "errors"
"fmt"
"hash/fnv" "hash/fnv"
"math" "math"
"sort" "sort"
...@@ -575,6 +576,12 @@ func (s *defaultOpenAIAccountScheduler) selectByLoadBalance( ...@@ -575,6 +576,12 @@ func (s *defaultOpenAIAccountScheduler) selectByLoadBalance(
return nil, 0, 0, 0, errors.New("no available OpenAI accounts") 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)) filtered := make([]*Account, 0, len(accounts))
loadReq := make([]AccountWithConcurrency, 0, len(accounts)) loadReq := make([]AccountWithConcurrency, 0, len(accounts))
for i := range accounts { for i := range accounts {
...@@ -587,6 +594,12 @@ func (s *defaultOpenAIAccountScheduler) selectByLoadBalance( ...@@ -587,6 +594,12 @@ func (s *defaultOpenAIAccountScheduler) selectByLoadBalance(
if !account.IsSchedulable() || !account.IsOpenAI() { if !account.IsSchedulable() || !account.IsOpenAI() {
continue 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) { if req.RequestedModel != "" && !account.IsModelSupported(req.RequestedModel) {
continue continue
} }
......
...@@ -152,6 +152,14 @@ func (s *SchedulerSnapshotService) GetAccount(ctx context.Context, accountID int ...@@ -152,6 +152,14 @@ func (s *SchedulerSnapshotService) GetAccount(ctx context.Context, accountID int
return s.accountRepo.GetByID(fallbackCtx, accountID) 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 中单个账号的数据(用于模型限流后立即生效) // UpdateAccountInCache 立即更新 Redis 中单个账号的数据(用于模型限流后立即生效)
func (s *SchedulerSnapshotService) UpdateAccountInCache(ctx context.Context, account *Account) error { func (s *SchedulerSnapshotService) UpdateAccountInCache(ctx context.Context, account *Account) error {
if s.cache == nil || account == nil { 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 { ...@@ -399,6 +399,8 @@ export interface Group {
fallback_group_id_on_invalid_request: number | null fallback_group_id_on_invalid_request: number | null
// OpenAI Messages 调度开关(用户侧需要此字段判断是否展示 Claude Code 教程) // OpenAI Messages 调度开关(用户侧需要此字段判断是否展示 Claude Code 教程)
allow_messages_dispatch?: boolean allow_messages_dispatch?: boolean
require_oauth_only: boolean
require_privacy_set: boolean
created_at: string created_at: string
updated_at: string updated_at: string
} }
...@@ -510,6 +512,8 @@ export interface CreateGroupRequest { ...@@ -510,6 +512,8 @@ export interface CreateGroupRequest {
mcp_xml_inject?: boolean mcp_xml_inject?: boolean
simulate_claude_max_enabled?: boolean simulate_claude_max_enabled?: boolean
supported_model_scopes?: string[] supported_model_scopes?: string[]
require_oauth_only?: boolean
require_privacy_set?: boolean
// 从指定分组复制账号 // 从指定分组复制账号
copy_accounts_from_group_ids?: number[] copy_accounts_from_group_ids?: number[]
} }
...@@ -539,6 +543,8 @@ export interface UpdateGroupRequest { ...@@ -539,6 +543,8 @@ export interface UpdateGroupRequest {
mcp_xml_inject?: boolean mcp_xml_inject?: boolean
simulate_claude_max_enabled?: boolean simulate_claude_max_enabled?: boolean
supported_model_scopes?: string[] supported_model_scopes?: string[]
require_oauth_only?: boolean
require_privacy_set?: boolean
copy_accounts_from_group_ids?: number[] copy_accounts_from_group_ids?: number[]
} }
......
...@@ -792,6 +792,61 @@ ...@@ -792,6 +792,61 @@
</div> </div>
</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 平台,且非订阅分组) --> <!-- 无效请求兜底(仅 anthropic/antigravity 平台,且非订阅分组) -->
<div <div
v-if="['anthropic', 'antigravity'].includes(createForm.platform) && createForm.subscription_type !== 'subscription'" v-if="['anthropic', 'antigravity'].includes(createForm.platform) && createForm.subscription_type !== 'subscription'"
...@@ -1527,6 +1582,61 @@ ...@@ -1527,6 +1582,61 @@
</div> </div>
</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 平台,且非订阅分组) --> <!-- 无效请求兜底(仅 anthropic/antigravity 平台,且非订阅分组) -->
<div <div
v-if="['anthropic', 'antigravity'].includes(editForm.platform) && editForm.subscription_type !== 'subscription'" v-if="['anthropic', 'antigravity'].includes(editForm.platform) && editForm.subscription_type !== 'subscription'"
...@@ -2063,6 +2173,9 @@ const createForm = reactive({ ...@@ -2063,6 +2173,9 @@ const createForm = reactive({
// OpenAI Messages 调度配置(仅 openai 平台使用) // OpenAI Messages 调度配置(仅 openai 平台使用)
allow_messages_dispatch: false, allow_messages_dispatch: false,
default_mapped_model: 'gpt-5.4', default_mapped_model: 'gpt-5.4',
// 账号过滤控制(OpenAI/Antigravity 平台)
require_oauth_only: false,
require_privacy_set: false,
// 模型路由开关 // 模型路由开关
model_routing_enabled: false, model_routing_enabled: false,
// 支持的模型系列(仅 antigravity 平台) // 支持的模型系列(仅 antigravity 平台)
...@@ -2307,6 +2420,9 @@ const editForm = reactive({ ...@@ -2307,6 +2420,9 @@ const editForm = reactive({
// OpenAI Messages 调度配置(仅 openai 平台使用) // OpenAI Messages 调度配置(仅 openai 平台使用)
allow_messages_dispatch: false, allow_messages_dispatch: false,
default_mapped_model: '', default_mapped_model: '',
// 账号过滤控制(OpenAI/Antigravity 平台)
require_oauth_only: false,
require_privacy_set: false,
// 模型路由开关 // 模型路由开关
model_routing_enabled: false, model_routing_enabled: false,
// 支持的模型系列(仅 antigravity 平台) // 支持的模型系列(仅 antigravity 平台)
...@@ -2452,6 +2568,8 @@ const closeCreateModal = () => { ...@@ -2452,6 +2568,8 @@ const closeCreateModal = () => {
createForm.fallback_group_id = null createForm.fallback_group_id = null
createForm.fallback_group_id_on_invalid_request = null createForm.fallback_group_id_on_invalid_request = null
createForm.allow_messages_dispatch = false createForm.allow_messages_dispatch = false
createForm.require_oauth_only = false
createForm.require_privacy_set = false
createForm.default_mapped_model = 'gpt-5.4' createForm.default_mapped_model = 'gpt-5.4'
createForm.supported_model_scopes = ['claude', 'gemini_text', 'gemini_image'] createForm.supported_model_scopes = ['claude', 'gemini_text', 'gemini_image']
createForm.mcp_xml_inject = true createForm.mcp_xml_inject = true
...@@ -2539,6 +2657,8 @@ const handleEdit = async (group: AdminGroup) => { ...@@ -2539,6 +2657,8 @@ const handleEdit = async (group: AdminGroup) => {
editForm.fallback_group_id = group.fallback_group_id editForm.fallback_group_id = group.fallback_group_id
editForm.fallback_group_id_on_invalid_request = group.fallback_group_id_on_invalid_request editForm.fallback_group_id_on_invalid_request = group.fallback_group_id_on_invalid_request
editForm.allow_messages_dispatch = group.allow_messages_dispatch || false 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.default_mapped_model = group.default_mapped_model || ''
editForm.model_routing_enabled = group.model_routing_enabled || false editForm.model_routing_enabled = group.model_routing_enabled || false
editForm.supported_model_scopes = group.supported_model_scopes || ['claude', 'gemini_text', 'gemini_image'] editForm.supported_model_scopes = group.supported_model_scopes || ['claude', 'gemini_text', 'gemini_image']
...@@ -2647,6 +2767,10 @@ watch( ...@@ -2647,6 +2767,10 @@ watch(
createForm.allow_messages_dispatch = false createForm.allow_messages_dispatch = false
createForm.default_mapped_model = '' 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