mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-27 18:37:39 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c3b536a8e9 | ||
|
|
85dd3a2cc6 | ||
|
|
bbaa850809 | ||
|
|
0731ac0449 | ||
|
|
2c5a74eb5d | ||
|
|
09c9b88c27 |
@@ -21,11 +21,28 @@ function validatePermissions(permissions) {
|
||||
if (permissions === undefined || permissions === null || permissions === '') {
|
||||
return null
|
||||
}
|
||||
// 兼容旧格式字符串
|
||||
// 兼容字符串格式
|
||||
if (typeof permissions === 'string') {
|
||||
if (permissions === 'all' || VALID_PERMISSIONS.includes(permissions)) {
|
||||
// 旧格式 'all' 表示全部服务
|
||||
if (permissions === 'all') {
|
||||
return null
|
||||
}
|
||||
// 单个有效权限
|
||||
if (VALID_PERMISSIONS.includes(permissions)) {
|
||||
return null
|
||||
}
|
||||
// 尝试解析 JSON 数组字符串(如 "[]" 或 '["claude","gemini"]')
|
||||
if (permissions.startsWith('[')) {
|
||||
try {
|
||||
const parsed = JSON.parse(permissions)
|
||||
if (Array.isArray(parsed)) {
|
||||
// 递归验证解析后的数组
|
||||
return validatePermissions(parsed)
|
||||
}
|
||||
} catch (e) {
|
||||
// 解析失败,返回错误
|
||||
}
|
||||
}
|
||||
return `Invalid permissions value. Must be an array of: ${VALID_PERMISSIONS.join(', ')}`
|
||||
}
|
||||
// 新格式数组
|
||||
|
||||
@@ -122,16 +122,6 @@ async function handleMessagesRequest(req, res) {
|
||||
try {
|
||||
const startTime = Date.now()
|
||||
|
||||
// Claude 服务权限校验,阻止未授权的 Key
|
||||
if (!apiKeyService.hasPermission(req.apiKey.permissions, 'claude')) {
|
||||
return res.status(403).json({
|
||||
error: {
|
||||
type: 'permission_error',
|
||||
message: '此 API Key 无权访问 Claude 服务'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 🔄 并发满额重试标志:最多重试一次(使用req对象存储状态)
|
||||
if (req._concurrencyRetryAttempted === undefined) {
|
||||
req._concurrencyRetryAttempted = false
|
||||
@@ -192,8 +182,7 @@ async function handleMessagesRequest(req, res) {
|
||||
|
||||
// /v1/messages 的扩展:按路径强制分流到 Gemini OAuth 账户(避免 model 前缀混乱)
|
||||
if (forcedVendor === 'gemini-cli' || forcedVendor === 'antigravity') {
|
||||
const permissions = req.apiKey?.permissions || 'all'
|
||||
if (permissions !== 'all' && permissions !== 'gemini') {
|
||||
if (!apiKeyService.hasPermission(req.apiKey?.permissions, 'gemini')) {
|
||||
return res.status(403).json({
|
||||
error: {
|
||||
type: 'permission_error',
|
||||
@@ -207,11 +196,7 @@ async function handleMessagesRequest(req, res) {
|
||||
}
|
||||
|
||||
// Claude 服务权限校验,阻止未授权的 Key(默认路径保持不变)
|
||||
if (
|
||||
req.apiKey.permissions &&
|
||||
req.apiKey.permissions !== 'all' &&
|
||||
req.apiKey.permissions !== 'claude'
|
||||
) {
|
||||
if (!apiKeyService.hasPermission(req.apiKey.permissions, 'claude')) {
|
||||
return res.status(403).json({
|
||||
error: {
|
||||
type: 'permission_error',
|
||||
@@ -1250,8 +1235,7 @@ router.get('/v1/models', authenticateApiKey, async (req, res) => {
|
||||
//(通过 v1internal:fetchAvailableModels),避免依赖静态 modelService 列表。
|
||||
const forcedVendor = req._anthropicVendor || null
|
||||
if (forcedVendor === 'antigravity') {
|
||||
const permissions = req.apiKey?.permissions || 'all'
|
||||
if (permissions !== 'all' && permissions !== 'gemini') {
|
||||
if (!apiKeyService.hasPermission(req.apiKey?.permissions, 'gemini')) {
|
||||
return res.status(403).json({
|
||||
error: {
|
||||
type: 'permission_error',
|
||||
@@ -1445,8 +1429,7 @@ router.post('/v1/messages/count_tokens', authenticateApiKey, async (req, res) =>
|
||||
// 按路径强制分流到 Gemini OAuth 账户(避免 model 前缀混乱)
|
||||
const forcedVendor = req._anthropicVendor || null
|
||||
if (forcedVendor === 'gemini-cli' || forcedVendor === 'antigravity') {
|
||||
const permissions = req.apiKey?.permissions || 'all'
|
||||
if (permissions !== 'all' && permissions !== 'gemini') {
|
||||
if (!apiKeyService.hasPermission(req.apiKey?.permissions, 'gemini')) {
|
||||
return res.status(403).json({
|
||||
error: {
|
||||
type: 'permission_error',
|
||||
@@ -1459,11 +1442,7 @@ router.post('/v1/messages/count_tokens', authenticateApiKey, async (req, res) =>
|
||||
}
|
||||
|
||||
// 检查权限
|
||||
if (
|
||||
req.apiKey.permissions &&
|
||||
req.apiKey.permissions !== 'all' &&
|
||||
req.apiKey.permissions !== 'claude'
|
||||
) {
|
||||
if (!apiKeyService.hasPermission(req.apiKey.permissions, 'claude')) {
|
||||
return res.status(403).json({
|
||||
error: {
|
||||
type: 'permission_error',
|
||||
|
||||
@@ -19,8 +19,7 @@ const { getEffectiveModel } = require('../utils/modelHelper')
|
||||
|
||||
// 🔧 辅助函数:检查 API Key 权限
|
||||
function checkPermissions(apiKeyData, requiredPermission = 'claude') {
|
||||
const permissions = apiKeyData.permissions || 'all'
|
||||
return permissions === 'all' || permissions === requiredPermission
|
||||
return apiKeyService.hasPermission(apiKeyData?.permissions, requiredPermission)
|
||||
}
|
||||
|
||||
function queueRateLimitUpdate(rateLimitInfo, usageSummary, model, context = '') {
|
||||
|
||||
@@ -46,11 +46,11 @@ async function routeToBackend(req, res, requestedModel) {
|
||||
logger.info(`🔀 Routing request - Model: ${requestedModel}, Backend: ${backend}`)
|
||||
|
||||
// 检查权限
|
||||
const permissions = req.apiKey.permissions || 'all'
|
||||
const { permissions } = req.apiKey
|
||||
|
||||
if (backend === 'claude') {
|
||||
// Claude 后端:通过 OpenAI 兼容层
|
||||
if (permissions !== 'all' && permissions !== 'claude') {
|
||||
if (!apiKeyService.hasPermission(permissions, 'claude')) {
|
||||
return res.status(403).json({
|
||||
error: {
|
||||
message: 'This API key does not have permission to access Claude',
|
||||
@@ -62,7 +62,7 @@ async function routeToBackend(req, res, requestedModel) {
|
||||
await handleChatCompletion(req, res, req.apiKey)
|
||||
} else if (backend === 'openai') {
|
||||
// OpenAI 后端
|
||||
if (permissions !== 'all' && permissions !== 'openai') {
|
||||
if (!apiKeyService.hasPermission(permissions, 'openai')) {
|
||||
return res.status(403).json({
|
||||
error: {
|
||||
message: 'This API key does not have permission to access OpenAI',
|
||||
|
||||
@@ -750,8 +750,13 @@ class ApiKeyService {
|
||||
|
||||
for (const [field, value] of Object.entries(updates)) {
|
||||
if (allowedUpdates.includes(field)) {
|
||||
if (field === 'restrictedModels' || field === 'allowedClients' || field === 'tags') {
|
||||
// 特殊处理数组字段
|
||||
if (
|
||||
field === 'restrictedModels' ||
|
||||
field === 'allowedClients' ||
|
||||
field === 'tags' ||
|
||||
field === 'permissions'
|
||||
) {
|
||||
// 特殊处理数组字段,使用 JSON.stringify
|
||||
updatedData[field] = JSON.stringify(value || [])
|
||||
} else if (
|
||||
field === 'enableModelRestriction' ||
|
||||
|
||||
@@ -290,31 +290,79 @@
|
||||
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>服务权限</label
|
||||
>
|
||||
<div class="flex flex-wrap gap-4">
|
||||
<label class="flex cursor-pointer items-center">
|
||||
<input v-model="form.permissions" class="mr-2" type="radio" value="" />
|
||||
<span class="text-sm text-gray-700">不修改</span>
|
||||
</label>
|
||||
<label class="flex cursor-pointer items-center">
|
||||
<input v-model="form.permissions" class="mr-2" type="radio" value="all" />
|
||||
<span class="text-sm text-gray-700">全部服务</span>
|
||||
</label>
|
||||
<label class="flex cursor-pointer items-center">
|
||||
<input v-model="form.permissions" class="mr-2" type="radio" value="claude" />
|
||||
<span class="text-sm text-gray-700">仅 Claude</span>
|
||||
</label>
|
||||
<label class="flex cursor-pointer items-center">
|
||||
<input v-model="form.permissions" class="mr-2" type="radio" value="gemini" />
|
||||
<span class="text-sm text-gray-700">仅 Gemini</span>
|
||||
</label>
|
||||
<label class="flex cursor-pointer items-center">
|
||||
<input v-model="form.permissions" class="mr-2" type="radio" value="openai" />
|
||||
<span class="text-sm text-gray-700">仅 OpenAI</span>
|
||||
</label>
|
||||
<label class="flex cursor-pointer items-center">
|
||||
<input v-model="form.permissions" class="mr-2" type="radio" value="droid" />
|
||||
<span class="text-sm text-gray-700">仅 Droid</span>
|
||||
</label>
|
||||
<div class="space-y-3">
|
||||
<!-- 权限操作模式选择 -->
|
||||
<div class="flex flex-wrap gap-4">
|
||||
<label class="flex cursor-pointer items-center">
|
||||
<input
|
||||
v-model="permissionsOperation"
|
||||
class="mr-2 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
||||
type="radio"
|
||||
value="none"
|
||||
/>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">不修改</span>
|
||||
</label>
|
||||
<label class="flex cursor-pointer items-center">
|
||||
<input
|
||||
v-model="permissionsOperation"
|
||||
class="mr-2 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
||||
type="radio"
|
||||
value="all"
|
||||
/>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">全部服务</span>
|
||||
</label>
|
||||
<label class="flex cursor-pointer items-center">
|
||||
<input
|
||||
v-model="permissionsOperation"
|
||||
class="mr-2 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
||||
type="radio"
|
||||
value="custom"
|
||||
/>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">自定义选择</span>
|
||||
</label>
|
||||
</div>
|
||||
<!-- 自定义选择时显示复选框 -->
|
||||
<div v-if="permissionsOperation === 'custom'" class="flex flex-wrap gap-4 pl-6">
|
||||
<label class="flex cursor-pointer items-center">
|
||||
<input
|
||||
v-model="form.permissions"
|
||||
class="mr-2 rounded text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
||||
type="checkbox"
|
||||
value="claude"
|
||||
/>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">Claude</span>
|
||||
</label>
|
||||
<label class="flex cursor-pointer items-center">
|
||||
<input
|
||||
v-model="form.permissions"
|
||||
class="mr-2 rounded text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
||||
type="checkbox"
|
||||
value="gemini"
|
||||
/>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">Gemini</span>
|
||||
</label>
|
||||
<label class="flex cursor-pointer items-center">
|
||||
<input
|
||||
v-model="form.permissions"
|
||||
class="mr-2 rounded text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
||||
type="checkbox"
|
||||
value="openai"
|
||||
/>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">OpenAI</span>
|
||||
</label>
|
||||
<label class="flex cursor-pointer items-center">
|
||||
<input
|
||||
v-model="form.permissions"
|
||||
class="mr-2 rounded text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
||||
type="checkbox"
|
||||
value="droid"
|
||||
/>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">Droid</span>
|
||||
</label>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
选择"全部服务"表示允许访问所有服务,"自定义选择"可以选择多个特定服务
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -494,6 +542,7 @@ const localAccounts = ref({
|
||||
const newTag = ref('')
|
||||
const availableTags = ref([])
|
||||
const tagOperation = ref('none') // 'replace', 'add', 'remove', 'none'
|
||||
const permissionsOperation = ref('none') // 'none', 'all', 'custom'
|
||||
|
||||
const selectedCount = computed(() => props.selectedKeys.length)
|
||||
|
||||
@@ -511,7 +560,7 @@ const form = reactive({
|
||||
dailyCostLimit: '',
|
||||
totalCostLimit: '',
|
||||
weeklyOpusCostLimit: '', // 新增Opus周费用限制
|
||||
permissions: '', // 空字符串表示不修改
|
||||
permissions: [], // 数组格式,用于多选
|
||||
claudeAccountId: '',
|
||||
geminiAccountId: '',
|
||||
openaiAccountId: '',
|
||||
@@ -547,9 +596,15 @@ const bedrockAccountSelectorValue = createAccountSelectorModel('bedrockAccountId
|
||||
const droidAccountSelectorValue = createAccountSelectorModel('droidAccountId')
|
||||
|
||||
const isServiceSelectable = (service) => {
|
||||
if (!form.permissions) return true
|
||||
if (form.permissions === 'all') return true
|
||||
return form.permissions === service
|
||||
// 不修改权限时,所有服务都可选
|
||||
if (permissionsOperation.value === 'none') return true
|
||||
// 全部服务时,所有服务都可选
|
||||
if (permissionsOperation.value === 'all') return true
|
||||
// 自定义选择时,根据选择的权限判断
|
||||
if (permissionsOperation.value === 'custom') {
|
||||
return form.permissions.length === 0 || form.permissions.includes(service)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// 标签管理方法
|
||||
@@ -737,8 +792,14 @@ const batchUpdateApiKeys = async () => {
|
||||
}
|
||||
|
||||
// 权限设置
|
||||
if (form.permissions !== '') {
|
||||
updates.permissions = form.permissions
|
||||
if (permissionsOperation.value !== 'none') {
|
||||
if (permissionsOperation.value === 'all') {
|
||||
// 全部服务:发送空数组
|
||||
updates.permissions = []
|
||||
} else if (permissionsOperation.value === 'custom') {
|
||||
// 自定义选择:发送选中的权限数组
|
||||
updates.permissions = form.permissions
|
||||
}
|
||||
}
|
||||
|
||||
// 账户绑定
|
||||
|
||||
@@ -1235,11 +1235,65 @@ onMounted(async () => {
|
||||
// 处理权限数据,兼容旧格式(字符串)和新格式(数组)
|
||||
const perms = props.apiKey.permissions
|
||||
if (Array.isArray(perms)) {
|
||||
form.permissions = perms
|
||||
// 过滤掉损坏的数据(如 "claude,droid" 这种逗号分隔的字符串)
|
||||
const validPerms = ['claude', 'gemini', 'openai', 'droid']
|
||||
const cleaned = []
|
||||
for (const p of perms) {
|
||||
if (validPerms.includes(p)) {
|
||||
cleaned.push(p)
|
||||
} else if (typeof p === 'string' && p.includes(',')) {
|
||||
// 处理逗号分隔的旧格式
|
||||
const parts = p.split(',').map((s) => s.trim())
|
||||
for (const part of parts) {
|
||||
if (validPerms.includes(part) && !cleaned.includes(part)) {
|
||||
cleaned.push(part)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
form.permissions = cleaned
|
||||
} else if (perms === 'all' || !perms) {
|
||||
form.permissions = []
|
||||
} else if (typeof perms === 'string') {
|
||||
form.permissions = [perms]
|
||||
// 尝试解析 JSON 数组字符串(如 "[]" 或 '["claude","gemini"]')
|
||||
if (perms.startsWith('[')) {
|
||||
try {
|
||||
const parsed = JSON.parse(perms)
|
||||
if (Array.isArray(parsed)) {
|
||||
// 递归处理解析后的数组
|
||||
const validPerms = ['claude', 'gemini', 'openai', 'droid']
|
||||
const cleaned = []
|
||||
for (const p of parsed) {
|
||||
if (validPerms.includes(p)) {
|
||||
cleaned.push(p)
|
||||
} else if (typeof p === 'string' && p.includes(',')) {
|
||||
const parts = p.split(',').map((s) => s.trim())
|
||||
for (const part of parts) {
|
||||
if (validPerms.includes(part) && !cleaned.includes(part)) {
|
||||
cleaned.push(part)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
form.permissions = cleaned
|
||||
} else {
|
||||
form.permissions = []
|
||||
}
|
||||
} catch (e) {
|
||||
// 解析失败,尝试按逗号分隔处理
|
||||
const validPerms = ['claude', 'gemini', 'openai', 'droid']
|
||||
const parts = perms.split(',').map((s) => s.trim())
|
||||
form.permissions = parts.filter((p) => validPerms.includes(p))
|
||||
}
|
||||
} else if (perms.includes(',')) {
|
||||
// 逗号分隔的旧格式
|
||||
const validPerms = ['claude', 'gemini', 'openai', 'droid']
|
||||
const parts = perms.split(',').map((s) => s.trim())
|
||||
form.permissions = parts.filter((p) => validPerms.includes(p))
|
||||
} else {
|
||||
const validPerms = ['claude', 'gemini', 'openai', 'droid']
|
||||
form.permissions = validPerms.includes(perms) ? [perms] : []
|
||||
}
|
||||
} else {
|
||||
form.permissions = []
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user