Commit 3ecadf4a authored by song's avatar song
Browse files

chore: apply stashed changes

parent 0170d19f
......@@ -241,6 +241,7 @@ func (s *APIKeyService) snapshotFromAPIKey(apiKey *APIKey) *APIKeyAuthSnapshot {
ModelRouting: apiKey.Group.ModelRouting,
ModelRoutingEnabled: apiKey.Group.ModelRoutingEnabled,
MCPXMLInject: apiKey.Group.MCPXMLInject,
SupportedModelScopes: apiKey.Group.SupportedModelScopes,
}
}
return snapshot
......@@ -287,6 +288,7 @@ func (s *APIKeyService) snapshotToAPIKey(key string, snapshot *APIKeyAuthSnapsho
ModelRouting: snapshot.Group.ModelRouting,
ModelRoutingEnabled: snapshot.Group.ModelRoutingEnabled,
MCPXMLInject: snapshot.Group.MCPXMLInject,
SupportedModelScopes: snapshot.Group.SupportedModelScopes,
}
}
return apiKey
......
......@@ -31,6 +31,7 @@ const (
AccountTypeOAuth = domain.AccountTypeOAuth // OAuth类型账号(full scope: profile + inference)
AccountTypeSetupToken = domain.AccountTypeSetupToken // Setup Token类型账号(inference only scope)
AccountTypeAPIKey = domain.AccountTypeAPIKey // API Key类型账号
AccountTypeUpstream = domain.AccountTypeUpstream // 上游透传类型账号(通过 Base URL + API Key 连接上游)
)
// Redeem type constants
......
......@@ -92,6 +92,9 @@ var (
// ErrClaudeCodeOnly 表示分组仅允许 Claude Code 客户端访问
var ErrClaudeCodeOnly = errors.New("this group only allows Claude Code clients")
// ErrModelScopeNotSupported 表示请求的模型系列不在分组支持的范围内
var ErrModelScopeNotSupported = errors.New("model scope not supported by this group")
// allowedHeaders 白名单headers(参考CRS项目)
var allowedHeaders = map[string]bool{
"accept": true,
......@@ -582,6 +585,13 @@ func (s *GatewayService) SelectAccountWithLoadAwareness(ctx context.Context, gro
log.Printf("[ModelRoutingDebug] load-aware enabled: group_id=%v model=%s session=%s platform=%s", derefGroupID(groupID), requestedModel, shortSessionHash(sessionHash), platform)
}
// Antigravity 模型系列检查(在账号选择前检查,确保所有代码路径都经过此检查)
if platform == PlatformAntigravity && groupID != nil && requestedModel != "" {
if err := s.checkAntigravityModelScope(ctx, *groupID, requestedModel); err != nil {
return nil, err
}
}
accounts, useMixed, err := s.listSchedulableAccounts(ctx, groupID, platform, hasForcePlatform)
if err != nil {
return nil, err
......@@ -1477,6 +1487,13 @@ func shuffleWithinPriority(accounts []*Account) {
// selectAccountForModelWithPlatform 选择单平台账户(完全隔离)
func (s *GatewayService) selectAccountForModelWithPlatform(ctx context.Context, groupID *int64, sessionHash string, requestedModel string, excludedIDs map[int64]struct{}, platform string) (*Account, error) {
// 对 Antigravity 平台,检查请求的模型系列是否在分组支持范围内
if platform == PlatformAntigravity && groupID != nil && requestedModel != "" {
if err := s.checkAntigravityModelScope(ctx, *groupID, requestedModel); err != nil {
return nil, err
}
}
preferOAuth := platform == PlatformGemini
routingAccountIDs := s.routingAccountIDsForRequest(ctx, groupID, requestedModel, platform)
......@@ -3898,6 +3915,27 @@ func (s *GatewayService) validateUpstreamBaseURL(raw string) (string, error) {
return normalized, nil
}
// checkAntigravityModelScope 检查 Antigravity 平台的模型系列是否在分组支持范围内
func (s *GatewayService) checkAntigravityModelScope(ctx context.Context, groupID int64, requestedModel string) error {
scope, ok := ResolveAntigravityQuotaScope(requestedModel)
if !ok {
return nil // 无法解析 scope,跳过检查
}
group, err := s.resolveGroupByID(ctx, groupID)
if err != nil {
return nil // 查询失败时放行
}
if group == nil {
return nil // 分组不存在时放行
}
if !IsScopeSupported(group.SupportedModelScopes, scope) {
return ErrModelScopeNotSupported
}
return nil
}
// GetAvailableModels returns the list of models available for a group
// It aggregates model_mapping keys from all schedulable accounts in the group
func (s *GatewayService) GetAvailableModels(ctx context.Context, groupID *int64, platform string) []string {
......
......@@ -41,6 +41,10 @@ type Group struct {
// MCP XML 协议注入开关(仅 antigravity 平台使用)
MCPXMLInject bool
// 支持的模型系列(仅 antigravity 平台使用)
// 可选值: claude, gemini_text, gemini_image
SupportedModelScopes []string
CreatedAt time.Time
UpdatedAt time.Time
......
......@@ -21,6 +21,11 @@ type User struct {
CreatedAt time.Time
UpdatedAt time.Time
// TOTP 双因素认证字段
TotpSecretEncrypted *string // AES-256-GCM 加密的 TOTP 密钥
TotpEnabled bool // 是否启用 TOTP
TotpEnabledAt *time.Time // TOTP 启用时间
APIKeys []APIKey
Subscriptions []UserSubscription
}
......
......@@ -39,7 +39,7 @@ type UserRepository interface {
ExistsByEmail(ctx context.Context, email string) (bool, error)
RemoveGroupFromAllowedGroups(ctx context.Context, groupID int64) (int64, error)
// TOTP 相关方法
// TOTP 双因素认证
UpdateTotpSecret(ctx context.Context, userID int64, encryptedSecret *string) error
EnableTotp(ctx context.Context, userID int64) error
DisableTotp(ctx context.Context, userID int64) error
......
-- 添加分组支持的模型系列字段
ALTER TABLE groups
ADD COLUMN IF NOT EXISTS supported_model_scopes JSONB NOT NULL
DEFAULT '["claude", "gemini_text", "gemini_image"]'::jsonb;
COMMENT ON COLUMN groups.supported_model_scopes IS '支持的模型系列:claude, gemini_text, gemini_image';
-- 修正 schema_migrations 中“本地改名”的迁移文件名
-- 适用场景:你已执行过旧文件名的迁移,合并后仅改了自己这边的文件名
BEGIN;
UPDATE schema_migrations
SET filename = '042b_add_ops_system_metrics_switch_count.sql'
WHERE filename = '042_add_ops_system_metrics_switch_count.sql'
AND NOT EXISTS (
SELECT 1 FROM schema_migrations WHERE filename = '042b_add_ops_system_metrics_switch_count.sql'
);
UPDATE schema_migrations
SET filename = '043b_add_group_invalid_request_fallback.sql'
WHERE filename = '043_add_group_invalid_request_fallback.sql'
AND NOT EXISTS (
SELECT 1 FROM schema_migrations WHERE filename = '043b_add_group_invalid_request_fallback.sql'
);
UPDATE schema_migrations
SET filename = '044b_add_group_mcp_xml_inject.sql'
WHERE filename = '044_add_group_mcp_xml_inject.sql'
AND NOT EXISTS (
SELECT 1 FROM schema_migrations WHERE filename = '044b_add_group_mcp_xml_inject.sql'
);
COMMIT;
......@@ -614,21 +614,87 @@
</div>
</div>
<!-- Account Type Selection (Antigravity - OAuth only) -->
<!-- Account Type Selection (Antigravity - OAuth or Upstream) -->
<div v-if="form.platform === 'antigravity'">
<label class="input-label">{{ t('admin.accounts.accountType') }}</label>
<div class="mt-2">
<div class="mt-2 grid grid-cols-2 gap-3">
<button
type="button"
@click="antigravityAccountType = 'oauth'"
:class="[
'flex items-center gap-3 rounded-lg border-2 p-3 text-left transition-all',
antigravityAccountType === 'oauth'
? 'border-purple-500 bg-purple-50 dark:bg-purple-900/20'
: 'border-gray-200 hover:border-purple-300 dark:border-dark-600 dark:hover:border-purple-700'
]"
>
<div
class="flex items-center gap-3 rounded-lg border-2 border-purple-500 bg-purple-50 p-3 dark:bg-purple-900/20"
:class="[
'flex h-8 w-8 shrink-0 items-center justify-center rounded-lg',
antigravityAccountType === 'oauth'
? 'bg-purple-500 text-white'
: 'bg-gray-100 text-gray-500 dark:bg-dark-600 dark:text-gray-400'
]"
>
<div class="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-purple-500 text-white">
<Icon name="key" size="sm" />
</div>
<div>
<span class="block text-sm font-medium text-gray-900 dark:text-white">OAuth</span>
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('admin.accounts.types.antigravityOauth') }}</span>
</div>
</button>
<button
type="button"
@click="antigravityAccountType = 'upstream'"
:class="[
'flex items-center gap-3 rounded-lg border-2 p-3 text-left transition-all',
antigravityAccountType === 'upstream'
? 'border-purple-500 bg-purple-50 dark:bg-purple-900/20'
: 'border-gray-200 hover:border-purple-300 dark:border-dark-600 dark:hover:border-purple-700'
]"
>
<div
:class="[
'flex h-8 w-8 shrink-0 items-center justify-center rounded-lg',
antigravityAccountType === 'upstream'
? 'bg-purple-500 text-white'
: 'bg-gray-100 text-gray-500 dark:bg-dark-600 dark:text-gray-400'
]"
>
<Icon name="cloud" size="sm" />
</div>
<div>
<span class="block text-sm font-medium text-gray-900 dark:text-white">{{ t('admin.accounts.types.upstream') }}</span>
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('admin.accounts.types.upstreamDesc') }}</span>
</div>
</button>
</div>
</div>
<!-- Upstream config (only for Antigravity upstream type) -->
<div v-if="form.platform === 'antigravity' && antigravityAccountType === 'upstream'" class="space-y-4">
<div>
<label class="input-label">{{ t('admin.accounts.upstream.baseUrl') }}</label>
<input
v-model="upstreamBaseUrl"
type="text"
required
class="input"
placeholder="https://upstream.example.com"
/>
<p class="input-hint">{{ t('admin.accounts.upstream.baseUrlHint') }}</p>
</div>
<div>
<label class="input-label">{{ t('admin.accounts.upstream.apiKey') }}</label>
<input
v-model="upstreamApiKey"
type="password"
required
class="input font-mono"
placeholder="sk-..."
/>
<p class="input-hint">{{ t('admin.accounts.upstream.apiKeyHint') }}</p>
</div>
</div>
......@@ -1940,6 +2006,9 @@ const customErrorCodeInput = ref<number | null>(null)
const interceptWarmupRequests = ref(false)
const autoPauseOnExpired = ref(true)
const mixedScheduling = ref(false) // For antigravity accounts: enable mixed scheduling
const antigravityAccountType = ref<'oauth' | 'upstream'>('oauth') // For antigravity: oauth or upstream
const upstreamBaseUrl = ref('') // For upstream type: base URL
const upstreamApiKey = ref('') // For upstream type: API key
const tempUnschedEnabled = ref(false)
const tempUnschedRules = ref<TempUnschedRuleForm[]>([])
const geminiOAuthType = ref<'code_assist' | 'google_one' | 'ai_studio'>('google_one')
......@@ -2037,7 +2106,13 @@ const form = reactive({
})
// Helper to check if current type needs OAuth flow
const isOAuthFlow = computed(() => accountCategory.value === 'oauth-based')
const isOAuthFlow = computed(() => {
// Antigravity upstream 类型不需要 OAuth 流程
if (form.platform === 'antigravity' && antigravityAccountType.value === 'upstream') {
return false
}
return accountCategory.value === 'oauth-based'
})
const isManualInputMethod = computed(() => {
return oauthFlowRef.value?.inputMethod === 'manual'
......@@ -2077,10 +2152,15 @@ watch(
}
)
// Sync form.type based on accountCategory and addMethod
// Sync form.type based on accountCategory, addMethod, and antigravityAccountType
watch(
[accountCategory, addMethod],
([category, method]) => {
[accountCategory, addMethod, antigravityAccountType],
([category, method, agType]) => {
// Antigravity upstream 类型
if (form.platform === 'antigravity' && agType === 'upstream') {
form.type = 'upstream'
return
}
if (category === 'oauth-based') {
form.type = method as AccountType // 'oauth' or 'setup-token'
} else {
......@@ -2108,9 +2188,10 @@ watch(
if (newPlatform !== 'anthropic') {
interceptWarmupRequests.value = false
}
// Antigravity only supports OAuth
// Antigravity: reset to OAuth by default, but allow upstream selection
if (newPlatform === 'antigravity') {
accountCategory.value = 'oauth-based'
antigravityAccountType.value = 'oauth'
}
// Reset OAuth states
oauth.resetState()
......@@ -2343,6 +2424,9 @@ const resetForm = () => {
sessionIdleTimeout.value = null
tlsFingerprintEnabled.value = false
sessionIdMaskingEnabled.value = false
antigravityAccountType.value = 'oauth'
upstreamBaseUrl.value = ''
upstreamApiKey.value = ''
tempUnschedEnabled.value = false
tempUnschedRules.value = []
geminiOAuthType.value = 'code_assist'
......@@ -2371,6 +2455,36 @@ const handleSubmit = async () => {
return
}
// For Antigravity upstream type, create directly
if (form.platform === 'antigravity' && antigravityAccountType.value === 'upstream') {
if (!form.name.trim()) {
appStore.showError(t('admin.accounts.pleaseEnterAccountName'))
return
}
if (!upstreamBaseUrl.value.trim()) {
appStore.showError(t('admin.accounts.upstream.pleaseEnterBaseUrl'))
return
}
if (!upstreamApiKey.value.trim()) {
appStore.showError(t('admin.accounts.upstream.pleaseEnterApiKey'))
return
}
submitting.value = true
try {
const credentials: Record<string, unknown> = {
base_url: upstreamBaseUrl.value.trim(),
api_key: upstreamApiKey.value.trim()
}
await createAccountAndFinish(form.platform, 'upstream', credentials)
} catch (error: any) {
appStore.showError(error.response?.data?.detail || t('admin.accounts.failedToCreate'))
} finally {
submitting.value = false
}
return
}
// For apikey type, create directly
if (!apiKeyValue.value.trim()) {
appStore.showError(t('admin.accounts.pleaseEnterApiKey'))
......
......@@ -1034,6 +1034,14 @@ export default {
tooltip: 'When enabled, if the request contains MCP tools, an XML format call protocol prompt will be injected into the system prompt. Disable this to avoid interference with certain clients.',
enabled: 'Enabled',
disabled: 'Disabled'
},
supportedScopes: {
title: 'Supported Model Families',
tooltip: 'Select the model families this group supports. Unchecked families will not be routed to this group.',
claude: 'Claude',
geminiText: 'Gemini Text',
geminiImage: 'Gemini Image',
hint: 'Select at least one model family'
}
},
......@@ -1173,7 +1181,9 @@ export default {
responsesApi: 'Responses API',
googleOauth: 'Google OAuth',
codeAssist: 'Code Assist',
antigravityOauth: 'Antigravity OAuth'
antigravityOauth: 'Antigravity OAuth',
upstream: 'Upstream',
upstreamDesc: 'Connect via Base URL + API Key'
},
status: {
active: 'Active',
......@@ -1431,6 +1441,15 @@ export default {
pleaseEnterApiKey: 'Please enter API Key',
apiKeyIsRequired: 'API Key is required',
leaveEmptyToKeep: 'Leave empty to keep current key',
// Upstream type
upstream: {
baseUrl: 'Upstream Base URL',
baseUrlHint: 'The address of the upstream Antigravity service, e.g., https://upstream.example.com',
apiKey: 'Upstream API Key',
apiKeyHint: 'API Key for the upstream service',
pleaseEnterBaseUrl: 'Please enter upstream Base URL',
pleaseEnterApiKey: 'Please enter upstream API Key'
},
// OAuth flow
oauth: {
title: 'Claude Account Authorization',
......
......@@ -1109,6 +1109,14 @@ export default {
tooltip: '启用后,当请求包含 MCP 工具时,会在 system prompt 中注入 XML 格式调用协议提示词。关闭此选项可避免对某些客户端造成干扰。',
enabled: '已启用',
disabled: '已禁用'
},
supportedScopes: {
title: '支持的模型系列',
tooltip: '选择此分组支持的模型系列。未勾选的系列将不会被路由到此分组。',
claude: 'Claude',
geminiText: 'Gemini Text',
geminiImage: 'Gemini Image',
hint: '至少选择一个模型系列'
}
},
......@@ -1294,6 +1302,8 @@ export default {
googleOauth: 'Google OAuth',
codeAssist: 'Code Assist',
antigravityOauth: 'Antigravity OAuth',
upstream: '对接上游',
upstreamDesc: '通过 Base URL + API Key 连接上游',
api_key: 'API Key',
cookie: 'Cookie'
},
......@@ -1563,6 +1573,15 @@ export default {
pleaseEnterApiKey: '请输入 API Key',
apiKeyIsRequired: 'API Key 是必需的',
leaveEmptyToKeep: '留空以保持当前密钥',
// Upstream type
upstream: {
baseUrl: '上游 Base URL',
baseUrlHint: '上游 Antigravity 服务的地址,例如:https://upstream.example.com',
apiKey: '上游 API Key',
apiKeyHint: '上游服务的 API Key',
pleaseEnterBaseUrl: '请输入上游 Base URL',
pleaseEnterApiKey: '请输入上游 API Key'
},
// OAuth flow
oauth: {
title: 'Claude 账号授权',
......
......@@ -365,6 +365,11 @@ export interface AdminGroup extends Group {
// MCP XML 协议注入(仅 antigravity 平台使用)
mcp_xml_inject: boolean
// 支持的模型系列(仅 antigravity 平台使用)
supported_model_scopes?: string[]
// 分组下账号数量(仅管理员可见)
account_count?: number
}
......@@ -414,6 +419,7 @@ export interface CreateGroupRequest {
claude_code_only?: boolean
fallback_group_id?: number | null
fallback_group_id_on_invalid_request?: number | null
supported_model_scopes?: string[]
}
export interface UpdateGroupRequest {
......@@ -433,12 +439,13 @@ export interface UpdateGroupRequest {
claude_code_only?: boolean
fallback_group_id?: number | null
fallback_group_id_on_invalid_request?: number | null
supported_model_scopes?: string[]
}
// ==================== Account & Proxy Types ====================
export type AccountPlatform = 'anthropic' | 'openai' | 'gemini' | 'antigravity'
export type AccountType = 'oauth' | 'setup-token' | 'apikey'
export type AccountType = 'oauth' | 'setup-token' | 'apikey' | 'upstream'
export type OAuthAddMethod = 'oauth' | 'setup-token'
export type ProxyProtocol = 'http' | 'https' | 'socks5' | 'socks5h'
......
......@@ -404,6 +404,62 @@
</div>
</div>
<!-- 支持的模型系列 antigravity 平台 -->
<div v-if="createForm.platform === 'antigravity'" class="border-t pt-4">
<div class="mb-1.5 flex items-center gap-1">
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">
{{ t('admin.groups.supportedScopes.title') }}
</label>
<!-- Help Tooltip -->
<div class="group relative inline-flex">
<Icon
name="questionCircle"
size="sm"
:stroke-width="2"
class="cursor-help text-gray-400 transition-colors hover:text-primary-500 dark:text-gray-500 dark:hover:text-primary-400"
/>
<div class="pointer-events-none absolute bottom-full left-0 z-50 mb-2 w-72 opacity-0 transition-all duration-200 group-hover:pointer-events-auto group-hover:opacity-100">
<div class="rounded-lg bg-gray-900 p-3 text-white shadow-lg dark:bg-gray-800">
<p class="text-xs leading-relaxed text-gray-300">
{{ t('admin.groups.supportedScopes.tooltip') }}
</p>
<div class="absolute -bottom-1.5 left-3 h-3 w-3 rotate-45 bg-gray-900 dark:bg-gray-800"></div>
</div>
</div>
</div>
</div>
<div class="space-y-2">
<label class="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
:checked="createForm.supported_model_scopes.includes('claude')"
@change="toggleCreateScope('claude')"
class="h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500 dark:border-dark-600 dark:bg-dark-700"
/>
<span class="text-sm text-gray-700 dark:text-gray-300">{{ t('admin.groups.supportedScopes.claude') }}</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
:checked="createForm.supported_model_scopes.includes('gemini_text')"
@change="toggleCreateScope('gemini_text')"
class="h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500 dark:border-dark-600 dark:bg-dark-700"
/>
<span class="text-sm text-gray-700 dark:text-gray-300">{{ t('admin.groups.supportedScopes.geminiText') }}</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
:checked="createForm.supported_model_scopes.includes('gemini_image')"
@change="toggleCreateScope('gemini_image')"
class="h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500 dark:border-dark-600 dark:bg-dark-700"
/>
<span class="text-sm text-gray-700 dark:text-gray-300">{{ t('admin.groups.supportedScopes.geminiImage') }}</span>
</label>
</div>
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">{{ t('admin.groups.supportedScopes.hint') }}</p>
</div>
<!-- MCP XML 协议注入 antigravity 平台 -->
<div v-if="createForm.platform === 'antigravity'" class="border-t pt-4">
<div class="mb-1.5 flex items-center gap-1">
......@@ -907,6 +963,62 @@
</div>
</div>
<!-- 支持的模型系列 antigravity 平台 -->
<div v-if="editForm.platform === 'antigravity'" class="border-t pt-4">
<div class="mb-1.5 flex items-center gap-1">
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">
{{ t('admin.groups.supportedScopes.title') }}
</label>
<!-- Help Tooltip -->
<div class="group relative inline-flex">
<Icon
name="questionCircle"
size="sm"
:stroke-width="2"
class="cursor-help text-gray-400 transition-colors hover:text-primary-500 dark:text-gray-500 dark:hover:text-primary-400"
/>
<div class="pointer-events-none absolute bottom-full left-0 z-50 mb-2 w-72 opacity-0 transition-all duration-200 group-hover:pointer-events-auto group-hover:opacity-100">
<div class="rounded-lg bg-gray-900 p-3 text-white shadow-lg dark:bg-gray-800">
<p class="text-xs leading-relaxed text-gray-300">
{{ t('admin.groups.supportedScopes.tooltip') }}
</p>
<div class="absolute -bottom-1.5 left-3 h-3 w-3 rotate-45 bg-gray-900 dark:bg-gray-800"></div>
</div>
</div>
</div>
</div>
<div class="space-y-2">
<label class="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
:checked="editForm.supported_model_scopes.includes('claude')"
@change="toggleEditScope('claude')"
class="h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500 dark:border-dark-600 dark:bg-dark-700"
/>
<span class="text-sm text-gray-700 dark:text-gray-300">{{ t('admin.groups.supportedScopes.claude') }}</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
:checked="editForm.supported_model_scopes.includes('gemini_text')"
@change="toggleEditScope('gemini_text')"
class="h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500 dark:border-dark-600 dark:bg-dark-700"
/>
<span class="text-sm text-gray-700 dark:text-gray-300">{{ t('admin.groups.supportedScopes.geminiText') }}</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
:checked="editForm.supported_model_scopes.includes('gemini_image')"
@change="toggleEditScope('gemini_image')"
class="h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500 dark:border-dark-600 dark:bg-dark-700"
/>
<span class="text-sm text-gray-700 dark:text-gray-300">{{ t('admin.groups.supportedScopes.geminiImage') }}</span>
</label>
</div>
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">{{ t('admin.groups.supportedScopes.hint') }}</p>
</div>
<!-- MCP XML 协议注入 antigravity 平台 -->
<div v-if="editForm.platform === 'antigravity'" class="border-t pt-4">
<div class="mb-1.5 flex items-center gap-1">
......@@ -1402,6 +1514,9 @@ const createForm = reactive({
fallback_group_id_on_invalid_request: null as number | null,
// 模型路由开关
model_routing_enabled: false,
// 支持的模型系列(仅 antigravity 平台)
supported_model_scopes: ['claude', 'gemini_text', 'gemini_image'] as string[],
// MCP XML 协议注入开关(仅 antigravity 平台)
mcp_xml_inject: true
})
......@@ -1472,6 +1587,26 @@ const removeSelectedAccount = (ruleIndex: number, accountId: number, isEdit: boo
rule.accounts = rule.accounts.filter(a => a.id !== accountId)
}
// 切换创建表单的模型系列选择
const toggleCreateScope = (scope: string) => {
const idx = createForm.supported_model_scopes.indexOf(scope)
if (idx === -1) {
createForm.supported_model_scopes.push(scope)
} else {
createForm.supported_model_scopes.splice(idx, 1)
}
}
// 切换编辑表单的模型系列选择
const toggleEditScope = (scope: string) => {
const idx = editForm.supported_model_scopes.indexOf(scope)
if (idx === -1) {
editForm.supported_model_scopes.push(scope)
} else {
editForm.supported_model_scopes.splice(idx, 1)
}
}
// 处理账号搜索输入框聚焦
const onAccountSearchFocus = (ruleIndex: number, isEdit: boolean = false) => {
const key = `${isEdit ? 'edit' : 'create'}-${ruleIndex}`
......@@ -1575,6 +1710,9 @@ const editForm = reactive({
fallback_group_id_on_invalid_request: null as number | null,
// 模型路由开关
model_routing_enabled: false,
// 支持的模型系列(仅 antigravity 平台)
supported_model_scopes: ['claude', 'gemini_text', 'gemini_image'] as string[],
// MCP XML 协议注入开关(仅 antigravity 平台)
mcp_xml_inject: true
})
......@@ -1658,6 +1796,7 @@ const closeCreateModal = () => {
createForm.claude_code_only = false
createForm.fallback_group_id = null
createForm.fallback_group_id_on_invalid_request = null
createForm.supported_model_scopes = ['claude', 'gemini_text', 'gemini_image']
createForm.mcp_xml_inject = true
createModelRoutingRules.value = []
}
......@@ -1710,6 +1849,7 @@ 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.model_routing_enabled = group.model_routing_enabled || false
editForm.supported_model_scopes = group.supported_model_scopes || ['claude', 'gemini_text', 'gemini_image']
editForm.mcp_xml_inject = group.mcp_xml_inject ?? true
// 加载模型路由规则(异步加载账号名称)
editModelRoutingRules.value = await convertApiFormatToRoutingRules(group.model_routing)
......
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