Merge pull request #814 from Guccbai/feature/multi-select-permissions [skip ci]

feat(permissions): 服务权限从单选改为多选
This commit is contained in:
Wesley Liddick
2025-12-26 00:52:42 -05:00
committed by GitHub
10 changed files with 171 additions and 113 deletions

View File

@@ -86,8 +86,7 @@ function generateSessionHash(req) {
* 检查 API Key 权限 * 检查 API Key 权限
*/ */
function checkPermissions(apiKeyData, requiredPermission = 'gemini') { function checkPermissions(apiKeyData, requiredPermission = 'gemini') {
const permissions = apiKeyData?.permissions || 'all' return apiKeyService.hasPermission(apiKeyData?.permissions, requiredPermission)
return permissions === 'all' || permissions === requiredPermission
} }
/** /**

View File

@@ -8,6 +8,43 @@ const config = require('../../../config/config')
const router = express.Router() const router = express.Router()
// 有效的权限值列表
const VALID_PERMISSIONS = ['claude', 'gemini', 'openai', 'droid']
/**
* 验证权限数组格式
* @param {any} permissions - 权限值(可以是数组或其他)
* @returns {string|null} - 返回错误消息null 表示验证通过
*/
function validatePermissions(permissions) {
// 空值或未定义表示全部服务
if (permissions === undefined || permissions === null || permissions === '') {
return null
}
// 兼容旧格式字符串
if (typeof permissions === 'string') {
if (permissions === 'all' || VALID_PERMISSIONS.includes(permissions)) {
return null
}
return `Invalid permissions value. Must be an array of: ${VALID_PERMISSIONS.join(', ')}`
}
// 新格式数组
if (Array.isArray(permissions)) {
// 空数组表示全部服务
if (permissions.length === 0) {
return null
}
// 验证数组中的每个值
for (const perm of permissions) {
if (!VALID_PERMISSIONS.includes(perm)) {
return `Invalid permission value "${perm}". Valid values are: ${VALID_PERMISSIONS.join(', ')}`
}
}
return null
}
return `Permissions must be an array. Valid values are: ${VALID_PERMISSIONS.join(', ')}`
}
// 👥 用户管理 (用于API Key分配) // 👥 用户管理 (用于API Key分配)
// 获取所有用户列表用于API Key分配 // 获取所有用户列表用于API Key分配
@@ -1382,16 +1419,10 @@ router.post('/api-keys', authenticateAdmin, async (req, res) => {
} }
} }
// 验证服务权限字段 // 验证服务权限字段(支持数组格式)
if ( const permissionsError = validatePermissions(permissions)
permissions !== undefined && if (permissionsError) {
permissions !== null && return res.status(400).json({ error: permissionsError })
permissions !== '' &&
!['claude', 'gemini', 'openai', 'droid', 'all'].includes(permissions)
) {
return res.status(400).json({
error: 'Invalid permissions value. Must be claude, gemini, openai, droid, or all'
})
} }
const newKey = await apiKeyService.generateApiKey({ const newKey = await apiKeyService.generateApiKey({
@@ -1481,15 +1512,10 @@ router.post('/api-keys/batch', authenticateAdmin, async (req, res) => {
.json({ error: 'Base name must be less than 90 characters to allow for numbering' }) .json({ error: 'Base name must be less than 90 characters to allow for numbering' })
} }
if ( // 验证服务权限字段(支持数组格式)
permissions !== undefined && const batchPermissionsError = validatePermissions(permissions)
permissions !== null && if (batchPermissionsError) {
permissions !== '' && return res.status(400).json({ error: batchPermissionsError })
!['claude', 'gemini', 'openai', 'droid', 'all'].includes(permissions)
) {
return res.status(400).json({
error: 'Invalid permissions value. Must be claude, gemini, openai, droid, or all'
})
} }
// 生成批量API Keys // 生成批量API Keys
@@ -1592,13 +1618,12 @@ router.put('/api-keys/batch', authenticateAdmin, async (req, res) => {
}) })
} }
if ( // 验证服务权限字段(支持数组格式)
updates.permissions !== undefined && if (updates.permissions !== undefined) {
!['claude', 'gemini', 'openai', 'droid', 'all'].includes(updates.permissions) const updatePermissionsError = validatePermissions(updates.permissions)
) { if (updatePermissionsError) {
return res.status(400).json({ return res.status(400).json({ error: updatePermissionsError })
error: 'Invalid permissions value. Must be claude, gemini, openai, droid, or all' }
})
} }
logger.info( logger.info(
@@ -1873,11 +1898,10 @@ router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => {
} }
if (permissions !== undefined) { if (permissions !== undefined) {
// 验证权限值 // 验证服务权限字段(支持数组格式)
if (!['claude', 'gemini', 'openai', 'droid', 'all'].includes(permissions)) { const singlePermissionsError = validatePermissions(permissions)
return res.status(400).json({ if (singlePermissionsError) {
error: 'Invalid permissions value. Must be claude, gemini, openai, droid, or all' return res.status(400).json({ error: singlePermissionsError })
})
} }
updates.permissions = permissions updates.permissions = permissions
} }

View File

@@ -118,11 +118,7 @@ async function handleMessagesRequest(req, res) {
const startTime = Date.now() const startTime = Date.now()
// Claude 服务权限校验,阻止未授权的 Key // Claude 服务权限校验,阻止未授权的 Key
if ( if (!apiKeyService.hasPermission(req.apiKey.permissions, 'claude')) {
req.apiKey.permissions &&
req.apiKey.permissions !== 'all' &&
req.apiKey.permissions !== 'claude'
) {
return res.status(403).json({ return res.status(403).json({
error: { error: {
type: 'permission_error', type: 'permission_error',

View File

@@ -4,12 +4,12 @@ const { authenticateApiKey } = require('../middleware/auth')
const droidRelayService = require('../services/droidRelayService') const droidRelayService = require('../services/droidRelayService')
const sessionHelper = require('../utils/sessionHelper') const sessionHelper = require('../utils/sessionHelper')
const logger = require('../utils/logger') const logger = require('../utils/logger')
const apiKeyService = require('../services/apiKeyService')
const router = express.Router() const router = express.Router()
function hasDroidPermission(apiKeyData) { function hasDroidPermission(apiKeyData) {
const permissions = apiKeyData?.permissions || 'all' return apiKeyService.hasPermission(apiKeyData?.permissions, 'droid')
return permissions === 'all' || permissions === 'droid'
} }
/** /**

View File

@@ -6,6 +6,7 @@ const geminiAccountService = require('../services/geminiAccountService')
const unifiedGeminiScheduler = require('../services/unifiedGeminiScheduler') const unifiedGeminiScheduler = require('../services/unifiedGeminiScheduler')
const { getAvailableModels } = require('../services/geminiRelayService') const { getAvailableModels } = require('../services/geminiRelayService')
const crypto = require('crypto') const crypto = require('crypto')
const apiKeyService = require('../services/apiKeyService')
// 生成会话哈希 // 生成会话哈希
function generateSessionHash(req) { function generateSessionHash(req) {
@@ -21,8 +22,7 @@ function generateSessionHash(req) {
// 检查 API Key 权限 // 检查 API Key 权限
function checkPermissions(apiKeyData, requiredPermission = 'gemini') { function checkPermissions(apiKeyData, requiredPermission = 'gemini') {
const permissions = apiKeyData.permissions || 'all' return apiKeyService.hasPermission(apiKeyData?.permissions, requiredPermission)
return permissions === 'all' || permissions === requiredPermission
} }
// 转换 OpenAI 消息格式到 Gemini 格式 // 转换 OpenAI 消息格式到 Gemini 格式
@@ -499,7 +499,6 @@ router.post('/v1/chat/completions', authenticateApiKey, async (req, res) => {
// 记录使用统计 // 记录使用统计
if (!usageReported && totalUsage.totalTokenCount > 0) { if (!usageReported && totalUsage.totalTokenCount > 0) {
try { try {
const apiKeyService = require('../services/apiKeyService')
await apiKeyService.recordUsage( await apiKeyService.recordUsage(
apiKeyData.id, apiKeyData.id,
totalUsage.promptTokenCount || 0, totalUsage.promptTokenCount || 0,
@@ -580,7 +579,6 @@ router.post('/v1/chat/completions', authenticateApiKey, async (req, res) => {
// 记录使用统计 // 记录使用统计
if (openaiResponse.usage) { if (openaiResponse.usage) {
try { try {
const apiKeyService = require('../services/apiKeyService')
await apiKeyService.recordUsage( await apiKeyService.recordUsage(
apiKeyData.id, apiKeyData.id,
openaiResponse.usage.prompt_tokens || 0, openaiResponse.usage.prompt_tokens || 0,

View File

@@ -20,8 +20,7 @@ function createProxyAgent(proxy) {
// 检查 API Key 是否具备 OpenAI 权限 // 检查 API Key 是否具备 OpenAI 权限
function checkOpenAIPermissions(apiKeyData) { function checkOpenAIPermissions(apiKeyData) {
const permissions = apiKeyData?.permissions || 'all' return apiKeyService.hasPermission(apiKeyData?.permissions, 'openai')
return permissions === 'all' || permissions === 'openai'
} }
function normalizeHeaders(headers = {}) { function normalizeHeaders(headers = {}) {

View File

@@ -8,6 +8,7 @@ const {
handleStreamGenerateContent: geminiHandleStreamGenerateContent handleStreamGenerateContent: geminiHandleStreamGenerateContent
} = require('../handlers/geminiHandlers') } = require('../handlers/geminiHandlers')
const openaiRoutes = require('./openaiRoutes') const openaiRoutes = require('./openaiRoutes')
const apiKeyService = require('../services/apiKeyService')
const router = express.Router() const router = express.Router()
@@ -73,7 +74,7 @@ async function routeToBackend(req, res, requestedModel) {
return await openaiRoutes.handleResponses(req, res) return await openaiRoutes.handleResponses(req, res)
} else if (backend === 'gemini') { } else if (backend === 'gemini') {
// Gemini 后端 // Gemini 后端
if (permissions !== 'all' && permissions !== 'gemini') { if (!apiKeyService.hasPermission(permissions, 'gemini')) {
return res.status(403).json({ return res.status(403).json({
error: { error: {
message: 'This API key does not have permission to access Gemini', message: 'This API key does not have permission to access Gemini',

View File

@@ -37,6 +37,51 @@ const ACCOUNT_CATEGORY_MAP = {
droid: 'droid' droid: 'droid'
} }
/**
* 规范化权限数据,兼容旧格式(字符串)和新格式(数组)
* @param {string|array} permissions - 权限数据
* @returns {array} - 权限数组,空数组表示全部服务
*/
function normalizePermissions(permissions) {
if (!permissions) {
return [] // 空 = 全部服务
}
if (Array.isArray(permissions)) {
return permissions
}
// 尝试解析 JSON 字符串(新格式存储)
if (typeof permissions === 'string') {
if (permissions.startsWith('[')) {
try {
const parsed = JSON.parse(permissions)
if (Array.isArray(parsed)) {
return parsed
}
} catch (e) {
// 解析失败,继续处理为普通字符串
}
}
// 旧格式 'all' 转为空数组
if (permissions === 'all') {
return []
}
// 旧单个字符串转为数组
return [permissions]
}
return []
}
/**
* 检查是否有访问特定服务的权限
* @param {string|array} permissions - 权限数据
* @param {string} service - 服务名称claude/gemini/openai/droid
* @returns {boolean} - 是否有权限
*/
function hasPermission(permissions, service) {
const perms = normalizePermissions(permissions)
return perms.length === 0 || perms.includes(service) // 空数组 = 全部服务
}
function normalizeAccountTypeKey(type) { function normalizeAccountTypeKey(type) {
if (!type) { if (!type) {
return null return null
@@ -89,7 +134,7 @@ class ApiKeyService {
azureOpenaiAccountId = null, azureOpenaiAccountId = null,
bedrockAccountId = null, // 添加 Bedrock 账号ID支持 bedrockAccountId = null, // 添加 Bedrock 账号ID支持
droidAccountId = null, droidAccountId = null,
permissions = 'all', // 可选值:'claude''gemini'、'openai'、'droid' 或 'all' permissions = [], // 数组格式,空数组表示全部服务,如 ['claude', 'gemini']
isActive = true, isActive = true,
concurrencyLimit = 0, concurrencyLimit = 0,
rateLimitWindow = null, rateLimitWindow = null,
@@ -132,7 +177,7 @@ class ApiKeyService {
azureOpenaiAccountId: azureOpenaiAccountId || '', azureOpenaiAccountId: azureOpenaiAccountId || '',
bedrockAccountId: bedrockAccountId || '', // 添加 Bedrock 账号ID bedrockAccountId: bedrockAccountId || '', // 添加 Bedrock 账号ID
droidAccountId: droidAccountId || '', droidAccountId: droidAccountId || '',
permissions: permissions || 'all', permissions: JSON.stringify(normalizePermissions(permissions)),
enableModelRestriction: String(enableModelRestriction), enableModelRestriction: String(enableModelRestriction),
restrictedModels: JSON.stringify(restrictedModels || []), restrictedModels: JSON.stringify(restrictedModels || []),
enableClientRestriction: String(enableClientRestriction || false), enableClientRestriction: String(enableClientRestriction || false),
@@ -186,7 +231,7 @@ class ApiKeyService {
azureOpenaiAccountId: keyData.azureOpenaiAccountId, azureOpenaiAccountId: keyData.azureOpenaiAccountId,
bedrockAccountId: keyData.bedrockAccountId, // 添加 Bedrock 账号ID bedrockAccountId: keyData.bedrockAccountId, // 添加 Bedrock 账号ID
droidAccountId: keyData.droidAccountId, droidAccountId: keyData.droidAccountId,
permissions: keyData.permissions, permissions: normalizePermissions(keyData.permissions),
enableModelRestriction: keyData.enableModelRestriction === 'true', enableModelRestriction: keyData.enableModelRestriction === 'true',
restrictedModels: JSON.parse(keyData.restrictedModels), restrictedModels: JSON.parse(keyData.restrictedModels),
enableClientRestriction: keyData.enableClientRestriction === 'true', enableClientRestriction: keyData.enableClientRestriction === 'true',
@@ -338,7 +383,7 @@ class ApiKeyService {
azureOpenaiAccountId: keyData.azureOpenaiAccountId, azureOpenaiAccountId: keyData.azureOpenaiAccountId,
bedrockAccountId: keyData.bedrockAccountId, // 添加 Bedrock 账号ID bedrockAccountId: keyData.bedrockAccountId, // 添加 Bedrock 账号ID
droidAccountId: keyData.droidAccountId, droidAccountId: keyData.droidAccountId,
permissions: keyData.permissions || 'all', permissions: normalizePermissions(keyData.permissions),
tokenLimit: parseInt(keyData.tokenLimit), tokenLimit: parseInt(keyData.tokenLimit),
concurrencyLimit: parseInt(keyData.concurrencyLimit || 0), concurrencyLimit: parseInt(keyData.concurrencyLimit || 0),
rateLimitWindow: parseInt(keyData.rateLimitWindow || 0), rateLimitWindow: parseInt(keyData.rateLimitWindow || 0),
@@ -467,7 +512,7 @@ class ApiKeyService {
azureOpenaiAccountId: keyData.azureOpenaiAccountId, azureOpenaiAccountId: keyData.azureOpenaiAccountId,
bedrockAccountId: keyData.bedrockAccountId, bedrockAccountId: keyData.bedrockAccountId,
droidAccountId: keyData.droidAccountId, droidAccountId: keyData.droidAccountId,
permissions: keyData.permissions || 'all', permissions: normalizePermissions(keyData.permissions),
tokenLimit: parseInt(keyData.tokenLimit), tokenLimit: parseInt(keyData.tokenLimit),
concurrencyLimit: parseInt(keyData.concurrencyLimit || 0), concurrencyLimit: parseInt(keyData.concurrencyLimit || 0),
rateLimitWindow: parseInt(keyData.rateLimitWindow || 0), rateLimitWindow: parseInt(keyData.rateLimitWindow || 0),
@@ -525,7 +570,7 @@ class ApiKeyService {
key.isActive = key.isActive === 'true' key.isActive = key.isActive === 'true'
key.enableModelRestriction = key.enableModelRestriction === 'true' key.enableModelRestriction = key.enableModelRestriction === 'true'
key.enableClientRestriction = key.enableClientRestriction === 'true' key.enableClientRestriction = key.enableClientRestriction === 'true'
key.permissions = key.permissions || 'all' // 兼容旧数据 key.permissions = normalizePermissions(key.permissions)
key.dailyCostLimit = parseFloat(key.dailyCostLimit || 0) key.dailyCostLimit = parseFloat(key.dailyCostLimit || 0)
key.totalCostLimit = parseFloat(key.totalCostLimit || 0) key.totalCostLimit = parseFloat(key.totalCostLimit || 0)
key.weeklyOpusCostLimit = parseFloat(key.weeklyOpusCostLimit || 0) key.weeklyOpusCostLimit = parseFloat(key.weeklyOpusCostLimit || 0)
@@ -1568,7 +1613,7 @@ class ApiKeyService {
userId: keyData.userId, userId: keyData.userId,
userUsername: keyData.userUsername, userUsername: keyData.userUsername,
createdBy: keyData.createdBy, createdBy: keyData.createdBy,
permissions: keyData.permissions, permissions: normalizePermissions(keyData.permissions),
dailyCostLimit: parseFloat(keyData.dailyCostLimit || 0), dailyCostLimit: parseFloat(keyData.dailyCostLimit || 0),
totalCostLimit: parseFloat(keyData.totalCostLimit || 0), totalCostLimit: parseFloat(keyData.totalCostLimit || 0),
// 所有平台账户绑定字段 // 所有平台账户绑定字段
@@ -1820,4 +1865,8 @@ const apiKeyService = new ApiKeyService()
// 为了方便其他服务调用,导出 recordUsage 方法 // 为了方便其他服务调用,导出 recordUsage 方法
apiKeyService.recordUsageMetrics = apiKeyService.recordUsage.bind(apiKeyService) apiKeyService.recordUsageMetrics = apiKeyService.recordUsage.bind(apiKeyService)
// 导出权限辅助函数供路由使用
apiKeyService.hasPermission = hasPermission
apiKeyService.normalizePermissions = normalizePermissions
module.exports = apiKeyService module.exports = apiKeyService

View File

@@ -579,55 +579,46 @@
<label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300" <label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300"
>服务权限</label >服务权限</label
> >
<div class="flex gap-4"> <div class="flex flex-wrap gap-4">
<label class="flex cursor-pointer items-center"> <label class="flex cursor-pointer items-center">
<input <input
v-model="form.permissions" v-model="form.permissions"
class="mr-2 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700" class="mr-2 rounded text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
type="radio" type="checkbox"
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="form.permissions"
class="mr-2 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
type="radio"
value="claude" value="claude"
/> />
<span class="text-sm text-gray-700 dark:text-gray-300"> Claude</span> <span class="text-sm text-gray-700 dark:text-gray-300">Claude</span>
</label> </label>
<label class="flex cursor-pointer items-center"> <label class="flex cursor-pointer items-center">
<input <input
v-model="form.permissions" v-model="form.permissions"
class="mr-2 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700" class="mr-2 rounded text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
type="radio" type="checkbox"
value="gemini" value="gemini"
/> />
<span class="text-sm text-gray-700 dark:text-gray-300"> Gemini</span> <span class="text-sm text-gray-700 dark:text-gray-300">Gemini</span>
</label> </label>
<label class="flex cursor-pointer items-center"> <label class="flex cursor-pointer items-center">
<input <input
v-model="form.permissions" v-model="form.permissions"
class="mr-2 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700" class="mr-2 rounded text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
type="radio" type="checkbox"
value="openai" value="openai"
/> />
<span class="text-sm text-gray-700 dark:text-gray-300"> OpenAI</span> <span class="text-sm text-gray-700 dark:text-gray-300">OpenAI</span>
</label> </label>
<label class="flex cursor-pointer items-center"> <label class="flex cursor-pointer items-center">
<input <input
v-model="form.permissions" v-model="form.permissions"
class="mr-2 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700" class="mr-2 rounded text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
type="radio" type="checkbox"
value="droid" value="droid"
/> />
<span class="text-sm text-gray-700 dark:text-gray-300"> Droid</span> <span class="text-sm text-gray-700 dark:text-gray-300">Droid</span>
</label> </label>
</div> </div>
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400"> <p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
控制此 API Key 可以访问哪些服务 不选择任何服务表示允许访问全部服务
</p> </p>
</div> </div>
@@ -662,7 +653,7 @@
v-model="form.claudeAccountId" v-model="form.claudeAccountId"
:accounts="localAccounts.claude" :accounts="localAccounts.claude"
default-option-text="使用共享账号池" default-option-text="使用共享账号池"
:disabled="form.permissions !== 'all' && form.permissions !== 'claude'" :disabled="form.permissions.length > 0 && !form.permissions.includes('claude')"
:groups="localAccounts.claudeGroups" :groups="localAccounts.claudeGroups"
placeholder="请选择Claude账号" placeholder="请选择Claude账号"
platform="claude" platform="claude"
@@ -676,7 +667,7 @@
v-model="form.geminiAccountId" v-model="form.geminiAccountId"
:accounts="localAccounts.gemini" :accounts="localAccounts.gemini"
default-option-text="使用共享账号池" default-option-text="使用共享账号池"
:disabled="form.permissions !== 'all' && form.permissions !== 'gemini'" :disabled="form.permissions.length > 0 && !form.permissions.includes('gemini')"
:groups="localAccounts.geminiGroups" :groups="localAccounts.geminiGroups"
placeholder="请选择Gemini账号" placeholder="请选择Gemini账号"
platform="gemini" platform="gemini"
@@ -690,7 +681,7 @@
v-model="form.openaiAccountId" v-model="form.openaiAccountId"
:accounts="localAccounts.openai" :accounts="localAccounts.openai"
default-option-text="使用共享账号池" default-option-text="使用共享账号池"
:disabled="form.permissions !== 'all' && form.permissions !== 'openai'" :disabled="form.permissions.length > 0 && !form.permissions.includes('openai')"
:groups="localAccounts.openaiGroups" :groups="localAccounts.openaiGroups"
placeholder="请选择OpenAI账号" placeholder="请选择OpenAI账号"
platform="openai" platform="openai"
@@ -704,7 +695,7 @@
v-model="form.bedrockAccountId" v-model="form.bedrockAccountId"
:accounts="localAccounts.bedrock" :accounts="localAccounts.bedrock"
default-option-text="使用共享账号池" default-option-text="使用共享账号池"
:disabled="form.permissions !== 'all' && form.permissions !== 'openai'" :disabled="form.permissions.length > 0 && !form.permissions.includes('claude')"
:groups="[]" :groups="[]"
placeholder="请选择Bedrock账号" placeholder="请选择Bedrock账号"
platform="bedrock" platform="bedrock"
@@ -718,7 +709,7 @@
v-model="form.droidAccountId" v-model="form.droidAccountId"
:accounts="localAccounts.droid" :accounts="localAccounts.droid"
default-option-text="使用共享账号池" default-option-text="使用共享账号池"
:disabled="form.permissions !== 'all' && form.permissions !== 'droid'" :disabled="form.permissions.length > 0 && !form.permissions.includes('droid')"
:groups="localAccounts.droidGroups" :groups="localAccounts.droidGroups"
placeholder="请选择Droid账号" placeholder="请选择Droid账号"
platform="droid" platform="droid"
@@ -966,7 +957,7 @@ const form = reactive({
expirationMode: 'fixed', // 过期模式fixed(固定) 或 activation(激活) expirationMode: 'fixed', // 过期模式fixed(固定) 或 activation(激活)
activationDays: 30, // 激活后有效天数 activationDays: 30, // 激活后有效天数
activationUnit: 'days', // 激活时间单位hours 或 days activationUnit: 'days', // 激活时间单位hours 或 days
permissions: 'all', permissions: [], // 数组格式,空数组表示全部服务
claudeAccountId: '', claudeAccountId: '',
geminiAccountId: '', geminiAccountId: '',
openaiAccountId: '', openaiAccountId: '',

View File

@@ -412,55 +412,46 @@
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300" <label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
>服务权限</label >服务权限</label
> >
<div class="flex gap-4"> <div class="flex flex-wrap gap-4">
<label class="flex cursor-pointer items-center"> <label class="flex cursor-pointer items-center">
<input <input
v-model="form.permissions" v-model="form.permissions"
class="mr-2 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700" class="mr-2 rounded text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
type="radio" type="checkbox"
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="form.permissions"
class="mr-2 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
type="radio"
value="claude" value="claude"
/> />
<span class="text-sm text-gray-700 dark:text-gray-300"> Claude</span> <span class="text-sm text-gray-700 dark:text-gray-300">Claude</span>
</label> </label>
<label class="flex cursor-pointer items-center"> <label class="flex cursor-pointer items-center">
<input <input
v-model="form.permissions" v-model="form.permissions"
class="mr-2 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700" class="mr-2 rounded text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
type="radio" type="checkbox"
value="gemini" value="gemini"
/> />
<span class="text-sm text-gray-700 dark:text-gray-300"> Gemini</span> <span class="text-sm text-gray-700 dark:text-gray-300">Gemini</span>
</label> </label>
<label class="flex cursor-pointer items-center"> <label class="flex cursor-pointer items-center">
<input <input
v-model="form.permissions" v-model="form.permissions"
class="mr-2 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700" class="mr-2 rounded text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
type="radio" type="checkbox"
value="openai" value="openai"
/> />
<span class="text-sm text-gray-700 dark:text-gray-300"> OpenAI</span> <span class="text-sm text-gray-700 dark:text-gray-300">OpenAI</span>
</label> </label>
<label class="flex cursor-pointer items-center"> <label class="flex cursor-pointer items-center">
<input <input
v-model="form.permissions" v-model="form.permissions"
class="mr-2 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700" class="mr-2 rounded text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
type="radio" type="checkbox"
value="droid" value="droid"
/> />
<span class="text-sm text-gray-700 dark:text-gray-300"> Droid</span> <span class="text-sm text-gray-700 dark:text-gray-300">Droid</span>
</label> </label>
</div> </div>
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400"> <p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
控制此 API Key 可以访问哪些服务 不选择任何服务表示允许访问全部服务
</p> </p>
</div> </div>
@@ -495,7 +486,7 @@
v-model="form.claudeAccountId" v-model="form.claudeAccountId"
:accounts="localAccounts.claude" :accounts="localAccounts.claude"
default-option-text="使用共享账号池" default-option-text="使用共享账号池"
:disabled="form.permissions !== 'all' && form.permissions !== 'claude'" :disabled="form.permissions.length > 0 && !form.permissions.includes('claude')"
:groups="localAccounts.claudeGroups" :groups="localAccounts.claudeGroups"
placeholder="请选择Claude账号" placeholder="请选择Claude账号"
platform="claude" platform="claude"
@@ -509,7 +500,7 @@
v-model="form.geminiAccountId" v-model="form.geminiAccountId"
:accounts="localAccounts.gemini" :accounts="localAccounts.gemini"
default-option-text="使用共享账号池" default-option-text="使用共享账号池"
:disabled="form.permissions !== 'all' && form.permissions !== 'gemini'" :disabled="form.permissions.length > 0 && !form.permissions.includes('gemini')"
:groups="localAccounts.geminiGroups" :groups="localAccounts.geminiGroups"
placeholder="请选择Gemini账号" placeholder="请选择Gemini账号"
platform="gemini" platform="gemini"
@@ -523,7 +514,7 @@
v-model="form.openaiAccountId" v-model="form.openaiAccountId"
:accounts="localAccounts.openai" :accounts="localAccounts.openai"
default-option-text="使用共享账号池" default-option-text="使用共享账号池"
:disabled="form.permissions !== 'all' && form.permissions !== 'openai'" :disabled="form.permissions.length > 0 && !form.permissions.includes('openai')"
:groups="localAccounts.openaiGroups" :groups="localAccounts.openaiGroups"
placeholder="请选择OpenAI账号" placeholder="请选择OpenAI账号"
platform="openai" platform="openai"
@@ -537,7 +528,7 @@
v-model="form.bedrockAccountId" v-model="form.bedrockAccountId"
:accounts="localAccounts.bedrock" :accounts="localAccounts.bedrock"
default-option-text="使用共享账号池" default-option-text="使用共享账号池"
:disabled="form.permissions !== 'all' && form.permissions !== 'openai'" :disabled="form.permissions.length > 0 && !form.permissions.includes('claude')"
:groups="[]" :groups="[]"
placeholder="请选择Bedrock账号" placeholder="请选择Bedrock账号"
platform="bedrock" platform="bedrock"
@@ -551,7 +542,7 @@
v-model="form.droidAccountId" v-model="form.droidAccountId"
:accounts="localAccounts.droid" :accounts="localAccounts.droid"
default-option-text="使用共享账号池" default-option-text="使用共享账号池"
:disabled="form.permissions !== 'all' && form.permissions !== 'droid'" :disabled="form.permissions.length > 0 && !form.permissions.includes('droid')"
:groups="localAccounts.droidGroups" :groups="localAccounts.droidGroups"
placeholder="请选择Droid账号" placeholder="请选择Droid账号"
platform="droid" platform="droid"
@@ -800,7 +791,7 @@ const form = reactive({
dailyCostLimit: '', dailyCostLimit: '',
totalCostLimit: '', totalCostLimit: '',
weeklyOpusCostLimit: '', weeklyOpusCostLimit: '',
permissions: 'all', permissions: [], // 数组格式,空数组表示全部服务
claudeAccountId: '', claudeAccountId: '',
geminiAccountId: '', geminiAccountId: '',
openaiAccountId: '', openaiAccountId: '',
@@ -1241,7 +1232,17 @@ onMounted(async () => {
form.dailyCostLimit = props.apiKey.dailyCostLimit || '' form.dailyCostLimit = props.apiKey.dailyCostLimit || ''
form.totalCostLimit = props.apiKey.totalCostLimit || '' form.totalCostLimit = props.apiKey.totalCostLimit || ''
form.weeklyOpusCostLimit = props.apiKey.weeklyOpusCostLimit || '' form.weeklyOpusCostLimit = props.apiKey.weeklyOpusCostLimit || ''
form.permissions = props.apiKey.permissions || 'all' // 处理权限数据,兼容旧格式(字符串)和新格式(数组)
const perms = props.apiKey.permissions
if (Array.isArray(perms)) {
form.permissions = perms
} else if (perms === 'all' || !perms) {
form.permissions = []
} else if (typeof perms === 'string') {
form.permissions = [perms]
} else {
form.permissions = []
}
// 处理 Claude 账号(区分 OAuth 和 Console // 处理 Claude 账号(区分 OAuth 和 Console
if (props.apiKey.claudeConsoleAccountId) { if (props.apiKey.claudeConsoleAccountId) {
form.claudeAccountId = `console:${props.apiKey.claudeConsoleAccountId}` form.claudeAccountId = `console:${props.apiKey.claudeConsoleAccountId}`