From 85dd3a2cc62b3bf9e5de4913f706a05d1a32f4e9 Mon Sep 17 00:00:00 2001 From: shaw Date: Fri, 26 Dec 2025 15:47:33 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8DPR=20#814=20=E9=81=97?= =?UTF-8?q?=E7=95=99bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/routes/admin/apiKeys.js | 21 ++- src/services/apiKeyService.js | 9 +- .../apikeys/BatchEditApiKeyModal.vue | 123 +++++++++++++----- .../components/apikeys/EditApiKeyModal.vue | 58 ++++++++- 4 files changed, 174 insertions(+), 37 deletions(-) diff --git a/src/routes/admin/apiKeys.js b/src/routes/admin/apiKeys.js index 5994f56d..4fc0f54b 100644 --- a/src/routes/admin/apiKeys.js +++ b/src/routes/admin/apiKeys.js @@ -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(', ')}` } // 新格式数组 diff --git a/src/services/apiKeyService.js b/src/services/apiKeyService.js index 771f973b..769a7226 100644 --- a/src/services/apiKeyService.js +++ b/src/services/apiKeyService.js @@ -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' || diff --git a/web/admin-spa/src/components/apikeys/BatchEditApiKeyModal.vue b/web/admin-spa/src/components/apikeys/BatchEditApiKeyModal.vue index 39eff929..255d1f88 100644 --- a/web/admin-spa/src/components/apikeys/BatchEditApiKeyModal.vue +++ b/web/admin-spa/src/components/apikeys/BatchEditApiKeyModal.vue @@ -290,31 +290,79 @@ -
- - - - - - +
+ +
+ + + +
+ +
+ + + + +
+

+ 选择"全部服务"表示允许访问所有服务,"自定义选择"可以选择多个特定服务 +

@@ -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 + } } // 账户绑定 diff --git a/web/admin-spa/src/components/apikeys/EditApiKeyModal.vue b/web/admin-spa/src/components/apikeys/EditApiKeyModal.vue index 749039bf..e0cf46a7 100644 --- a/web/admin-spa/src/components/apikeys/EditApiKeyModal.vue +++ b/web/admin-spa/src/components/apikeys/EditApiKeyModal.vue @@ -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 = [] }