feat(groups): add Claude Code client restriction and session isolation
- Add claude_code_only field to restrict groups to Claude Code clients only - Add fallback_group_id for non-Claude Code requests to use alternate group - Implement ClaudeCodeValidator for User-Agent detection - Add group-level session binding isolation (groupID in Redis key) - Prevent cross-group sticky session pollution - Update frontend with Claude Code restriction controls
This commit is contained in:
@@ -857,6 +857,15 @@ export default {
|
||||
imagePricing: {
|
||||
title: 'Image Generation Pricing',
|
||||
description: 'Configure pricing for gemini-3-pro-image model. Leave empty to use default prices.'
|
||||
},
|
||||
claudeCode: {
|
||||
title: 'Claude Code Client Restriction',
|
||||
tooltip: 'When enabled, this group only allows official Claude Code clients. Non-Claude Code requests will be rejected or fallback to the specified group.',
|
||||
enabled: 'Claude Code Only',
|
||||
disabled: 'Allow All Clients',
|
||||
fallbackGroup: 'Fallback Group',
|
||||
fallbackHint: 'Non-Claude Code requests will use this group. Leave empty to reject directly.',
|
||||
noFallback: 'No Fallback (Reject)'
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -934,6 +934,15 @@ export default {
|
||||
imagePricing: {
|
||||
title: '图片生成计费',
|
||||
description: '配置 gemini-3-pro-image 模型的图片生成价格,留空则使用默认价格'
|
||||
},
|
||||
claudeCode: {
|
||||
title: 'Claude Code 客户端限制',
|
||||
tooltip: '启用后,此分组仅允许 Claude Code 官方客户端访问。非 Claude Code 请求将被拒绝或降级到指定分组。',
|
||||
enabled: '仅限 Claude Code',
|
||||
disabled: '允许所有客户端',
|
||||
fallbackGroup: '降级分组',
|
||||
fallbackHint: '非 Claude Code 请求将使用此分组,留空则直接拒绝',
|
||||
noFallback: '不降级(直接拒绝)'
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -263,6 +263,9 @@ export interface Group {
|
||||
image_price_1k: number | null
|
||||
image_price_2k: number | null
|
||||
image_price_4k: number | null
|
||||
// Claude Code 客户端限制
|
||||
claude_code_only: boolean
|
||||
fallback_group_id: number | null
|
||||
account_count?: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
@@ -298,6 +301,15 @@ export interface CreateGroupRequest {
|
||||
platform?: GroupPlatform
|
||||
rate_multiplier?: number
|
||||
is_exclusive?: boolean
|
||||
subscription_type?: SubscriptionType
|
||||
daily_limit_usd?: number | null
|
||||
weekly_limit_usd?: number | null
|
||||
monthly_limit_usd?: number | null
|
||||
image_price_1k?: number | null
|
||||
image_price_2k?: number | null
|
||||
image_price_4k?: number | null
|
||||
claude_code_only?: boolean
|
||||
fallback_group_id?: number | null
|
||||
}
|
||||
|
||||
export interface UpdateGroupRequest {
|
||||
@@ -307,6 +319,15 @@ export interface UpdateGroupRequest {
|
||||
rate_multiplier?: number
|
||||
is_exclusive?: boolean
|
||||
status?: 'active' | 'inactive'
|
||||
subscription_type?: SubscriptionType
|
||||
daily_limit_usd?: number | null
|
||||
weekly_limit_usd?: number | null
|
||||
monthly_limit_usd?: number | null
|
||||
image_price_1k?: number | null
|
||||
image_price_2k?: number | null
|
||||
image_price_4k?: number | null
|
||||
claude_code_only?: boolean
|
||||
fallback_group_id?: number | null
|
||||
}
|
||||
|
||||
// ==================== Account & Proxy Types ====================
|
||||
|
||||
@@ -403,6 +403,62 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Claude Code 客户端限制(仅 anthropic 平台) -->
|
||||
<div v-if="createForm.platform === 'anthropic'" 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.claudeCode.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.claudeCode.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="flex items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
@click="createForm.claude_code_only = !createForm.claude_code_only"
|
||||
:class="[
|
||||
'relative inline-flex h-6 w-11 items-center rounded-full transition-colors',
|
||||
createForm.claude_code_only ? 'bg-primary-500' : 'bg-gray-300 dark:bg-dark-600'
|
||||
]"
|
||||
>
|
||||
<span
|
||||
:class="[
|
||||
'inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform',
|
||||
createForm.claude_code_only ? 'translate-x-6' : 'translate-x-1'
|
||||
]"
|
||||
/>
|
||||
</button>
|
||||
<span class="text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ createForm.claude_code_only ? t('admin.groups.claudeCode.enabled') : t('admin.groups.claudeCode.disabled') }}
|
||||
</span>
|
||||
</div>
|
||||
<!-- 降级分组选择(仅当启用 claude_code_only 时显示) -->
|
||||
<div v-if="createForm.claude_code_only" class="mt-3">
|
||||
<label class="input-label">{{ t('admin.groups.claudeCode.fallbackGroup') }}</label>
|
||||
<Select
|
||||
v-model="createForm.fallback_group_id"
|
||||
:options="fallbackGroupOptions"
|
||||
:placeholder="t('admin.groups.claudeCode.noFallback')"
|
||||
/>
|
||||
<p class="input-hint">{{ t('admin.groups.claudeCode.fallbackHint') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
<template #footer>
|
||||
@@ -648,6 +704,62 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Claude Code 客户端限制(仅 anthropic 平台) -->
|
||||
<div v-if="editForm.platform === 'anthropic'" 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.claudeCode.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.claudeCode.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="flex items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
@click="editForm.claude_code_only = !editForm.claude_code_only"
|
||||
:class="[
|
||||
'relative inline-flex h-6 w-11 items-center rounded-full transition-colors',
|
||||
editForm.claude_code_only ? 'bg-primary-500' : 'bg-gray-300 dark:bg-dark-600'
|
||||
]"
|
||||
>
|
||||
<span
|
||||
:class="[
|
||||
'inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform',
|
||||
editForm.claude_code_only ? 'translate-x-6' : 'translate-x-1'
|
||||
]"
|
||||
/>
|
||||
</button>
|
||||
<span class="text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ editForm.claude_code_only ? t('admin.groups.claudeCode.enabled') : t('admin.groups.claudeCode.disabled') }}
|
||||
</span>
|
||||
</div>
|
||||
<!-- 降级分组选择(仅当启用 claude_code_only 时显示) -->
|
||||
<div v-if="editForm.claude_code_only" class="mt-3">
|
||||
<label class="input-label">{{ t('admin.groups.claudeCode.fallbackGroup') }}</label>
|
||||
<Select
|
||||
v-model="editForm.fallback_group_id"
|
||||
:options="fallbackGroupOptionsForEdit"
|
||||
:placeholder="t('admin.groups.claudeCode.noFallback')"
|
||||
/>
|
||||
<p class="input-hint">{{ t('admin.groups.claudeCode.fallbackHint') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
<template #footer>
|
||||
@@ -774,6 +886,35 @@ const subscriptionTypeOptions = computed(() => [
|
||||
{ value: 'subscription', label: t('admin.groups.subscription.subscription') }
|
||||
])
|
||||
|
||||
// 降级分组选项(创建时)- 仅包含 anthropic 平台且未启用 claude_code_only 的分组
|
||||
const fallbackGroupOptions = computed(() => {
|
||||
const options: { value: number | null; label: string }[] = [
|
||||
{ value: null, label: t('admin.groups.claudeCode.noFallback') }
|
||||
]
|
||||
const eligibleGroups = groups.value.filter(
|
||||
(g) => g.platform === 'anthropic' && !g.claude_code_only && g.status === 'active'
|
||||
)
|
||||
eligibleGroups.forEach((g) => {
|
||||
options.push({ value: g.id, label: g.name })
|
||||
})
|
||||
return options
|
||||
})
|
||||
|
||||
// 降级分组选项(编辑时)- 排除自身
|
||||
const fallbackGroupOptionsForEdit = computed(() => {
|
||||
const options: { value: number | null; label: string }[] = [
|
||||
{ value: null, label: t('admin.groups.claudeCode.noFallback') }
|
||||
]
|
||||
const currentId = editingGroup.value?.id
|
||||
const eligibleGroups = groups.value.filter(
|
||||
(g) => g.platform === 'anthropic' && !g.claude_code_only && g.status === 'active' && g.id !== currentId
|
||||
)
|
||||
eligibleGroups.forEach((g) => {
|
||||
options.push({ value: g.id, label: g.name })
|
||||
})
|
||||
return options
|
||||
})
|
||||
|
||||
const groups = ref<Group[]>([])
|
||||
const loading = ref(false)
|
||||
const searchQuery = ref('')
|
||||
@@ -821,7 +962,10 @@ const createForm = reactive({
|
||||
// 图片生成计费配置(仅 antigravity 平台使用)
|
||||
image_price_1k: null as number | null,
|
||||
image_price_2k: null as number | null,
|
||||
image_price_4k: null as number | null
|
||||
image_price_4k: null as number | null,
|
||||
// Claude Code 客户端限制(仅 anthropic 平台使用)
|
||||
claude_code_only: false,
|
||||
fallback_group_id: null as number | null
|
||||
})
|
||||
|
||||
const editForm = reactive({
|
||||
@@ -838,7 +982,10 @@ const editForm = reactive({
|
||||
// 图片生成计费配置(仅 antigravity 平台使用)
|
||||
image_price_1k: null as number | null,
|
||||
image_price_2k: null as number | null,
|
||||
image_price_4k: null as number | null
|
||||
image_price_4k: null as number | null,
|
||||
// Claude Code 客户端限制(仅 anthropic 平台使用)
|
||||
claude_code_only: false,
|
||||
fallback_group_id: null as number | null
|
||||
})
|
||||
|
||||
// 根据分组类型返回不同的删除确认消息
|
||||
@@ -908,6 +1055,8 @@ const closeCreateModal = () => {
|
||||
createForm.image_price_1k = null
|
||||
createForm.image_price_2k = null
|
||||
createForm.image_price_4k = null
|
||||
createForm.claude_code_only = false
|
||||
createForm.fallback_group_id = null
|
||||
}
|
||||
|
||||
const handleCreateGroup = async () => {
|
||||
@@ -949,6 +1098,8 @@ const handleEdit = (group: Group) => {
|
||||
editForm.image_price_1k = group.image_price_1k
|
||||
editForm.image_price_2k = group.image_price_2k
|
||||
editForm.image_price_4k = group.image_price_4k
|
||||
editForm.claude_code_only = group.claude_code_only || false
|
||||
editForm.fallback_group_id = group.fallback_group_id
|
||||
showEditModal.value = true
|
||||
}
|
||||
|
||||
@@ -966,7 +1117,12 @@ const handleUpdateGroup = async () => {
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
await adminAPI.groups.update(editingGroup.value.id, editForm)
|
||||
// 转换 fallback_group_id: null -> 0 (后端使用 0 表示清除)
|
||||
const payload = {
|
||||
...editForm,
|
||||
fallback_group_id: editForm.fallback_group_id === null ? 0 : editForm.fallback_group_id
|
||||
}
|
||||
await adminAPI.groups.update(editingGroup.value.id, payload)
|
||||
appStore.showSuccess(t('admin.groups.groupUpdated'))
|
||||
closeEditModal()
|
||||
loadGroups()
|
||||
|
||||
Reference in New Issue
Block a user