From 33ea26f2ac094a77db0a9075a2d7313bf41a2d83 Mon Sep 17 00:00:00 2001 From: Guccbai <1456714872@qq.com> Date: Wed, 17 Dec 2025 11:35:11 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat(permissions):=20=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E6=9D=83=E9=99=90=E4=BB=8E=E5=8D=95=E9=80=89=E6=94=B9=E4=B8=BA?= =?UTF-8?q?=E5=A4=9A=E9=80=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 API Key 的服务权限从单选改为多选,支持同时选择多个服务 - 移除"全部服务"选项,空数组表示允许访问全部服务 - 后端自动兼容旧格式('all' -> [], 'claude' -> ['claude']) - 前端 radio 改为 checkbox,更新账户选择器联动逻辑 修改文件: - apiKeyService.js: 添加 normalizePermissions/hasPermission 函数 - api.js, droidRoutes.js, openaiRoutes.js, unified.js, openaiGeminiRoutes.js, geminiHandlers.js: 使用新权限验证函数 - admin/apiKeys.js: 支持数组格式权限验证 - CreateApiKeyModal.vue, EditApiKeyModal.vue: UI 改为 checkbox 多选 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/handlers/geminiHandlers.js | 3 +- src/routes/admin/apiKeys.js | 86 ++++++++++++------- src/routes/api.js | 6 +- src/routes/droidRoutes.js | 4 +- src/routes/openaiGeminiRoutes.js | 4 +- src/routes/openaiRoutes.js | 3 +- src/routes/unified.js | 3 +- src/services/apiKeyService.js | 55 ++++++++++-- .../components/apikeys/CreateApiKeyModal.vue | 49 +++++------ .../components/apikeys/EditApiKeyModal.vue | 61 ++++++------- 10 files changed, 163 insertions(+), 111 deletions(-) diff --git a/src/handlers/geminiHandlers.js b/src/handlers/geminiHandlers.js index 87295d31..063fec9b 100644 --- a/src/handlers/geminiHandlers.js +++ b/src/handlers/geminiHandlers.js @@ -86,8 +86,7 @@ function generateSessionHash(req) { * 检查 API Key 权限 */ function checkPermissions(apiKeyData, requiredPermission = 'gemini') { - const permissions = apiKeyData?.permissions || 'all' - return permissions === 'all' || permissions === requiredPermission + return apiKeyService.hasPermission(apiKeyData?.permissions, requiredPermission) } /** diff --git a/src/routes/admin/apiKeys.js b/src/routes/admin/apiKeys.js index d88444bd..5994f56d 100644 --- a/src/routes/admin/apiKeys.js +++ b/src/routes/admin/apiKeys.js @@ -8,6 +8,43 @@ const config = require('../../../config/config') 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分配) @@ -1382,16 +1419,10 @@ router.post('/api-keys', authenticateAdmin, async (req, res) => { } } - // 验证服务权限字段 - if ( - permissions !== undefined && - permissions !== null && - 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 permissionsError = validatePermissions(permissions) + if (permissionsError) { + return res.status(400).json({ error: permissionsError }) } 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' }) } - if ( - permissions !== undefined && - permissions !== null && - 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 batchPermissionsError = validatePermissions(permissions) + if (batchPermissionsError) { + return res.status(400).json({ error: batchPermissionsError }) } // 生成批量API Keys @@ -1592,13 +1618,12 @@ router.put('/api-keys/batch', authenticateAdmin, async (req, res) => { }) } - if ( - updates.permissions !== undefined && - !['claude', 'gemini', 'openai', 'droid', 'all'].includes(updates.permissions) - ) { - return res.status(400).json({ - error: 'Invalid permissions value. Must be claude, gemini, openai, droid, or all' - }) + // 验证服务权限字段(支持数组格式) + if (updates.permissions !== undefined) { + const updatePermissionsError = validatePermissions(updates.permissions) + if (updatePermissionsError) { + return res.status(400).json({ error: updatePermissionsError }) + } } logger.info( @@ -1873,11 +1898,10 @@ router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => { } if (permissions !== undefined) { - // 验证权限值 - if (!['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 singlePermissionsError = validatePermissions(permissions) + if (singlePermissionsError) { + return res.status(400).json({ error: singlePermissionsError }) } updates.permissions = permissions } diff --git a/src/routes/api.js b/src/routes/api.js index 3defdc19..bf7abae7 100644 --- a/src/routes/api.js +++ b/src/routes/api.js @@ -111,11 +111,7 @@ async function handleMessagesRequest(req, res) { const startTime = Date.now() // 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', diff --git a/src/routes/droidRoutes.js b/src/routes/droidRoutes.js index f8479cde..b6d9932a 100644 --- a/src/routes/droidRoutes.js +++ b/src/routes/droidRoutes.js @@ -4,12 +4,12 @@ const { authenticateApiKey } = require('../middleware/auth') const droidRelayService = require('../services/droidRelayService') const sessionHelper = require('../utils/sessionHelper') const logger = require('../utils/logger') +const apiKeyService = require('../services/apiKeyService') const router = express.Router() function hasDroidPermission(apiKeyData) { - const permissions = apiKeyData?.permissions || 'all' - return permissions === 'all' || permissions === 'droid' + return apiKeyService.hasPermission(apiKeyData?.permissions, 'droid') } /** diff --git a/src/routes/openaiGeminiRoutes.js b/src/routes/openaiGeminiRoutes.js index fd74ad86..70795d9e 100644 --- a/src/routes/openaiGeminiRoutes.js +++ b/src/routes/openaiGeminiRoutes.js @@ -6,6 +6,7 @@ const geminiAccountService = require('../services/geminiAccountService') const unifiedGeminiScheduler = require('../services/unifiedGeminiScheduler') const { getAvailableModels } = require('../services/geminiRelayService') const crypto = require('crypto') +const apiKeyService = require('../services/apiKeyService') // 生成会话哈希 function generateSessionHash(req) { @@ -21,8 +22,7 @@ function generateSessionHash(req) { // 检查 API Key 权限 function checkPermissions(apiKeyData, requiredPermission = 'gemini') { - const permissions = apiKeyData.permissions || 'all' - return permissions === 'all' || permissions === requiredPermission + return apiKeyService.hasPermission(apiKeyData?.permissions, requiredPermission) } // 转换 OpenAI 消息格式到 Gemini 格式 diff --git a/src/routes/openaiRoutes.js b/src/routes/openaiRoutes.js index 7faf9e87..7f1b04f1 100644 --- a/src/routes/openaiRoutes.js +++ b/src/routes/openaiRoutes.js @@ -20,8 +20,7 @@ function createProxyAgent(proxy) { // 检查 API Key 是否具备 OpenAI 权限 function checkOpenAIPermissions(apiKeyData) { - const permissions = apiKeyData?.permissions || 'all' - return permissions === 'all' || permissions === 'openai' + return apiKeyService.hasPermission(apiKeyData?.permissions, 'openai') } function normalizeHeaders(headers = {}) { diff --git a/src/routes/unified.js b/src/routes/unified.js index a8a8e69d..57c4fe80 100644 --- a/src/routes/unified.js +++ b/src/routes/unified.js @@ -8,6 +8,7 @@ const { handleStreamGenerateContent: geminiHandleStreamGenerateContent } = require('../handlers/geminiHandlers') const openaiRoutes = require('./openaiRoutes') +const apiKeyService = require('../services/apiKeyService') const router = express.Router() @@ -73,7 +74,7 @@ async function routeToBackend(req, res, requestedModel) { return await openaiRoutes.handleResponses(req, res) } else if (backend === 'gemini') { // Gemini 后端 - if (permissions !== 'all' && permissions !== 'gemini') { + if (!apiKeyService.hasPermission(permissions, 'gemini')) { return res.status(403).json({ error: { message: 'This API key does not have permission to access Gemini', diff --git a/src/services/apiKeyService.js b/src/services/apiKeyService.js index 0e9e7597..ef3f3b80 100644 --- a/src/services/apiKeyService.js +++ b/src/services/apiKeyService.js @@ -37,6 +37,43 @@ const ACCOUNT_CATEGORY_MAP = { 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) { if (!type) { return null @@ -89,7 +126,7 @@ class ApiKeyService { azureOpenaiAccountId = null, bedrockAccountId = null, // 添加 Bedrock 账号ID支持 droidAccountId = null, - permissions = 'all', // 可选值:'claude'、'gemini'、'openai'、'droid' 或 'all' + permissions = [], // 数组格式,空数组表示全部服务,如 ['claude', 'gemini'] isActive = true, concurrencyLimit = 0, rateLimitWindow = null, @@ -132,7 +169,7 @@ class ApiKeyService { azureOpenaiAccountId: azureOpenaiAccountId || '', bedrockAccountId: bedrockAccountId || '', // 添加 Bedrock 账号ID droidAccountId: droidAccountId || '', - permissions: permissions || 'all', + permissions: JSON.stringify(normalizePermissions(permissions)), enableModelRestriction: String(enableModelRestriction), restrictedModels: JSON.stringify(restrictedModels || []), enableClientRestriction: String(enableClientRestriction || false), @@ -186,7 +223,7 @@ class ApiKeyService { azureOpenaiAccountId: keyData.azureOpenaiAccountId, bedrockAccountId: keyData.bedrockAccountId, // 添加 Bedrock 账号ID droidAccountId: keyData.droidAccountId, - permissions: keyData.permissions, + permissions: normalizePermissions(keyData.permissions), enableModelRestriction: keyData.enableModelRestriction === 'true', restrictedModels: JSON.parse(keyData.restrictedModels), enableClientRestriction: keyData.enableClientRestriction === 'true', @@ -338,7 +375,7 @@ class ApiKeyService { azureOpenaiAccountId: keyData.azureOpenaiAccountId, bedrockAccountId: keyData.bedrockAccountId, // 添加 Bedrock 账号ID droidAccountId: keyData.droidAccountId, - permissions: keyData.permissions || 'all', + permissions: normalizePermissions(keyData.permissions), tokenLimit: parseInt(keyData.tokenLimit), concurrencyLimit: parseInt(keyData.concurrencyLimit || 0), rateLimitWindow: parseInt(keyData.rateLimitWindow || 0), @@ -467,7 +504,7 @@ class ApiKeyService { azureOpenaiAccountId: keyData.azureOpenaiAccountId, bedrockAccountId: keyData.bedrockAccountId, droidAccountId: keyData.droidAccountId, - permissions: keyData.permissions || 'all', + permissions: normalizePermissions(keyData.permissions), tokenLimit: parseInt(keyData.tokenLimit), concurrencyLimit: parseInt(keyData.concurrencyLimit || 0), rateLimitWindow: parseInt(keyData.rateLimitWindow || 0), @@ -525,7 +562,7 @@ class ApiKeyService { key.isActive = key.isActive === 'true' key.enableModelRestriction = key.enableModelRestriction === 'true' key.enableClientRestriction = key.enableClientRestriction === 'true' - key.permissions = key.permissions || 'all' // 兼容旧数据 + key.permissions = normalizePermissions(key.permissions) key.dailyCostLimit = parseFloat(key.dailyCostLimit || 0) key.totalCostLimit = parseFloat(key.totalCostLimit || 0) key.weeklyOpusCostLimit = parseFloat(key.weeklyOpusCostLimit || 0) @@ -1568,7 +1605,7 @@ class ApiKeyService { userId: keyData.userId, userUsername: keyData.userUsername, createdBy: keyData.createdBy, - permissions: keyData.permissions, + permissions: normalizePermissions(keyData.permissions), dailyCostLimit: parseFloat(keyData.dailyCostLimit || 0), totalCostLimit: parseFloat(keyData.totalCostLimit || 0), // 所有平台账户绑定字段 @@ -1820,4 +1857,8 @@ const apiKeyService = new ApiKeyService() // 为了方便其他服务调用,导出 recordUsage 方法 apiKeyService.recordUsageMetrics = apiKeyService.recordUsage.bind(apiKeyService) +// 导出权限辅助函数供路由使用 +apiKeyService.hasPermission = hasPermission +apiKeyService.normalizePermissions = normalizePermissions + module.exports = apiKeyService diff --git a/web/admin-spa/src/components/apikeys/CreateApiKeyModal.vue b/web/admin-spa/src/components/apikeys/CreateApiKeyModal.vue index 056c3e73..bd464e16 100644 --- a/web/admin-spa/src/components/apikeys/CreateApiKeyModal.vue +++ b/web/admin-spa/src/components/apikeys/CreateApiKeyModal.vue @@ -579,55 +579,46 @@ -
+
-

- 控制此 API Key 可以访问哪些服务 + 不选择任何服务表示允许访问全部服务

@@ -662,7 +653,7 @@ v-model="form.claudeAccountId" :accounts="localAccounts.claude" default-option-text="使用共享账号池" - :disabled="form.permissions !== 'all' && form.permissions !== 'claude'" + :disabled="form.permissions.length > 0 && !form.permissions.includes('claude')" :groups="localAccounts.claudeGroups" placeholder="请选择Claude账号" platform="claude" @@ -676,7 +667,7 @@ v-model="form.geminiAccountId" :accounts="localAccounts.gemini" default-option-text="使用共享账号池" - :disabled="form.permissions !== 'all' && form.permissions !== 'gemini'" + :disabled="form.permissions.length > 0 && !form.permissions.includes('gemini')" :groups="localAccounts.geminiGroups" placeholder="请选择Gemini账号" platform="gemini" @@ -690,7 +681,7 @@ v-model="form.openaiAccountId" :accounts="localAccounts.openai" default-option-text="使用共享账号池" - :disabled="form.permissions !== 'all' && form.permissions !== 'openai'" + :disabled="form.permissions.length > 0 && !form.permissions.includes('openai')" :groups="localAccounts.openaiGroups" placeholder="请选择OpenAI账号" platform="openai" @@ -704,7 +695,7 @@ v-model="form.bedrockAccountId" :accounts="localAccounts.bedrock" default-option-text="使用共享账号池" - :disabled="form.permissions !== 'all' && form.permissions !== 'openai'" + :disabled="form.permissions.length > 0 && !form.permissions.includes('claude')" :groups="[]" placeholder="请选择Bedrock账号" platform="bedrock" @@ -718,7 +709,7 @@ v-model="form.droidAccountId" :accounts="localAccounts.droid" default-option-text="使用共享账号池" - :disabled="form.permissions !== 'all' && form.permissions !== 'droid'" + :disabled="form.permissions.length > 0 && !form.permissions.includes('droid')" :groups="localAccounts.droidGroups" placeholder="请选择Droid账号" platform="droid" @@ -966,7 +957,7 @@ const form = reactive({ expirationMode: 'fixed', // 过期模式:fixed(固定) 或 activation(激活) activationDays: 30, // 激活后有效天数 activationUnit: 'days', // 激活时间单位:hours 或 days - permissions: 'all', + permissions: [], // 数组格式,空数组表示全部服务 claudeAccountId: '', geminiAccountId: '', openaiAccountId: '', diff --git a/web/admin-spa/src/components/apikeys/EditApiKeyModal.vue b/web/admin-spa/src/components/apikeys/EditApiKeyModal.vue index fd831c2b..749039bf 100644 --- a/web/admin-spa/src/components/apikeys/EditApiKeyModal.vue +++ b/web/admin-spa/src/components/apikeys/EditApiKeyModal.vue @@ -412,55 +412,46 @@ -
+
-

- 控制此 API Key 可以访问哪些服务 + 不选择任何服务表示允许访问全部服务

@@ -495,7 +486,7 @@ v-model="form.claudeAccountId" :accounts="localAccounts.claude" default-option-text="使用共享账号池" - :disabled="form.permissions !== 'all' && form.permissions !== 'claude'" + :disabled="form.permissions.length > 0 && !form.permissions.includes('claude')" :groups="localAccounts.claudeGroups" placeholder="请选择Claude账号" platform="claude" @@ -509,7 +500,7 @@ v-model="form.geminiAccountId" :accounts="localAccounts.gemini" default-option-text="使用共享账号池" - :disabled="form.permissions !== 'all' && form.permissions !== 'gemini'" + :disabled="form.permissions.length > 0 && !form.permissions.includes('gemini')" :groups="localAccounts.geminiGroups" placeholder="请选择Gemini账号" platform="gemini" @@ -523,7 +514,7 @@ v-model="form.openaiAccountId" :accounts="localAccounts.openai" default-option-text="使用共享账号池" - :disabled="form.permissions !== 'all' && form.permissions !== 'openai'" + :disabled="form.permissions.length > 0 && !form.permissions.includes('openai')" :groups="localAccounts.openaiGroups" placeholder="请选择OpenAI账号" platform="openai" @@ -537,7 +528,7 @@ v-model="form.bedrockAccountId" :accounts="localAccounts.bedrock" default-option-text="使用共享账号池" - :disabled="form.permissions !== 'all' && form.permissions !== 'openai'" + :disabled="form.permissions.length > 0 && !form.permissions.includes('claude')" :groups="[]" placeholder="请选择Bedrock账号" platform="bedrock" @@ -551,7 +542,7 @@ v-model="form.droidAccountId" :accounts="localAccounts.droid" default-option-text="使用共享账号池" - :disabled="form.permissions !== 'all' && form.permissions !== 'droid'" + :disabled="form.permissions.length > 0 && !form.permissions.includes('droid')" :groups="localAccounts.droidGroups" placeholder="请选择Droid账号" platform="droid" @@ -800,7 +791,7 @@ const form = reactive({ dailyCostLimit: '', totalCostLimit: '', weeklyOpusCostLimit: '', - permissions: 'all', + permissions: [], // 数组格式,空数组表示全部服务 claudeAccountId: '', geminiAccountId: '', openaiAccountId: '', @@ -1241,7 +1232,17 @@ onMounted(async () => { form.dailyCostLimit = props.apiKey.dailyCostLimit || '' form.totalCostLimit = props.apiKey.totalCostLimit || '' 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) if (props.apiKey.claudeConsoleAccountId) { form.claudeAccountId = `console:${props.apiKey.claudeConsoleAccountId}` From 534fbf6ac2eb9f0b03162dbc9d48819c5f2cde52 Mon Sep 17 00:00:00 2001 From: Guccbai <1456714872@qq.com> Date: Tue, 23 Dec 2025 20:26:18 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix(eslint):=20=E4=BF=AE=E5=A4=8D=20ESLint?= =?UTF-8?q?=20=E6=A3=80=E6=9F=A5=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复 apiKeyService.js 中 if 语句缺少大括号的 curly 错误 - 移除 openaiGeminiRoutes.js 中重复声明 apiKeyService 导致的 no-shadow 错误 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/routes/openaiGeminiRoutes.js | 2 -- src/services/apiKeyService.js | 16 ++++++++++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/routes/openaiGeminiRoutes.js b/src/routes/openaiGeminiRoutes.js index 70795d9e..82b76b95 100644 --- a/src/routes/openaiGeminiRoutes.js +++ b/src/routes/openaiGeminiRoutes.js @@ -499,7 +499,6 @@ router.post('/v1/chat/completions', authenticateApiKey, async (req, res) => { // 记录使用统计 if (!usageReported && totalUsage.totalTokenCount > 0) { try { - const apiKeyService = require('../services/apiKeyService') await apiKeyService.recordUsage( apiKeyData.id, totalUsage.promptTokenCount || 0, @@ -580,7 +579,6 @@ router.post('/v1/chat/completions', authenticateApiKey, async (req, res) => { // 记录使用统计 if (openaiResponse.usage) { try { - const apiKeyService = require('../services/apiKeyService') await apiKeyService.recordUsage( apiKeyData.id, openaiResponse.usage.prompt_tokens || 0, diff --git a/src/services/apiKeyService.js b/src/services/apiKeyService.js index ef3f3b80..771f973b 100644 --- a/src/services/apiKeyService.js +++ b/src/services/apiKeyService.js @@ -43,20 +43,28 @@ const ACCOUNT_CATEGORY_MAP = { * @returns {array} - 权限数组,空数组表示全部服务 */ function normalizePermissions(permissions) { - if (!permissions) return [] // 空 = 全部服务 - if (Array.isArray(permissions)) return 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 + if (Array.isArray(parsed)) { + return parsed + } } catch (e) { // 解析失败,继续处理为普通字符串 } } // 旧格式 'all' 转为空数组 - if (permissions === 'all') return [] + if (permissions === 'all') { + return [] + } // 旧单个字符串转为数组 return [permissions] }