diff --git a/web/admin-spa/package-lock.json b/web/admin-spa/package-lock.json index 60401ee7..fb29e58e 100644 --- a/web/admin-spa/package-lock.json +++ b/web/admin-spa/package-lock.json @@ -18,12 +18,14 @@ "vue-router": "^4.2.5" }, "devDependencies": { + "@playwright/test": "^1.55.0", "@vitejs/plugin-vue": "^4.5.2", "@vue/eslint-config-prettier": "^10.2.0", "autoprefixer": "^10.4.16", "eslint": "^8.55.0", "eslint-plugin-prettier": "^5.5.4", "eslint-plugin-vue": "^9.19.2", + "playwright": "^1.55.0", "postcss": "^8.4.32", "prettier": "^3.1.1", "prettier-plugin-tailwindcss": "^0.6.14", @@ -806,6 +808,22 @@ "url": "https://opencollective.com/pkgr" } }, + "node_modules/@playwright/test": { + "version": "1.55.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.55.0.tgz", + "integrity": "sha512-04IXzPwHrW69XusN/SIdDdKZBzMfOT9UNT/YiJit/xpy2VuAoB8NHc8Aplb96zsWDddLnbkPL3TsmrS04ZU2xQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.55.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@popperjs/core": { "name": "@sxzz/popperjs-es", "version": "2.11.7", @@ -3465,6 +3483,53 @@ "pathe": "^2.0.1" } }, + "node_modules/playwright": { + "version": "1.55.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.0.tgz", + "integrity": "sha512-sdCWStblvV1YU909Xqx0DhOjPZE4/5lJsIS84IfN9dAZfcl/CIZ5O8l3o0j7hPMjDvqoTF8ZUcc+i/GL5erstA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.55.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.55.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.0.tgz", + "integrity": "sha512-GvZs4vU3U5ro2nZpeiwyb0zuFaqb9sUiAJuyrWpcGouD8y9/HLgGbNRjIph7zU9D3hnPaisMl9zG9CgFi/biIg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.6.tgz", diff --git a/web/admin-spa/package.json b/web/admin-spa/package.json index 9cb15379..4881e1b3 100644 --- a/web/admin-spa/package.json +++ b/web/admin-spa/package.json @@ -21,12 +21,14 @@ "vue-router": "^4.2.5" }, "devDependencies": { + "@playwright/test": "^1.55.0", "@vitejs/plugin-vue": "^4.5.2", "@vue/eslint-config-prettier": "^10.2.0", "autoprefixer": "^10.4.16", "eslint": "^8.55.0", "eslint-plugin-prettier": "^5.5.4", "eslint-plugin-vue": "^9.19.2", + "playwright": "^1.55.0", "postcss": "^8.4.32", "prettier": "^3.1.1", "prettier-plugin-tailwindcss": "^0.6.14", diff --git a/web/admin-spa/src/components/apikeys/BatchEditApiKeyModal.vue b/web/admin-spa/src/components/apikeys/BatchEditApiKeyModal.vue new file mode 100644 index 00000000..5224e891 --- /dev/null +++ b/web/admin-spa/src/components/apikeys/BatchEditApiKeyModal.vue @@ -0,0 +1,726 @@ + + + + + + + + + + + 批量编辑 API Keys ({{ selectedCount }} 个) + + + + + + + + + + + + + + 批量编辑说明 + + 以下设置将应用到所选的 {{ selectedCount }} 个 API + Key。只有填写或修改的字段才会被更新,空白字段将保持原值不变。 + + + + + + + + + 标签 (批量操作) + + + + + + + 替换标签 + + + + 添加标签 + + + + 移除标签 + + + + 不修改标签 + + + + + + + + + {{ + tagOperation === 'replace' + ? '新标签列表:' + : tagOperation === 'add' + ? '要添加的标签:' + : '要移除的标签:' + }} + + + + {{ tag }} + + + + + + + + + + 点击选择已有标签: + + + + {{ tag }} + + + + + + + 创建新标签: + + + + + + + + + + + + + + + + + + 速率限制设置 + + + + + + + 时间窗口 (分钟) + + + + + + 请求次数限制 + + + + + Token 限制 + + + + + + + + + + 每日费用限制 (美元) + + + + + + + 并发限制 + + + + + + + 激活状态 + + + + 激活 + + + + 禁用 + + + + 不修改 + + + + + + + + 服务权限 + + + + 不修改 + + + + 全部服务 + + + + 仅 Claude + + + + 仅 Gemini + + + + 仅 OpenAI + + + + + + + + 专属账号绑定 + + + {{ accountsLoading ? '刷新中...' : '刷新账号' }} + + + + + Claude 专属账号 + + 不修改 + 使用共享账号池 + + + 分组 - {{ group.name }} + + + + + {{ account.name }} ({{ + account.platform === 'claude-console' ? 'Console' : 'OAuth' + }}) + + + + + + Gemini 专属账号 + + 不修改 + 使用共享账号池 + + + 分组 - {{ group.name }} + + + + + {{ account.name }} + + + + + + OpenAI 专属账号 + + 不修改 + 使用共享账号池 + + + 分组 - {{ group.name }} + + + + + {{ account.name }} + + + + + + Bedrock 专属账号 + + 不修改 + 使用共享账号池 + + + {{ account.name }} + + + + + + + + + + 取消 + + + + + {{ loading ? '保存中...' : '批量保存' }} + + + + + + + + + + + diff --git a/web/admin-spa/src/views/ApiKeysView.vue b/web/admin-spa/src/views/ApiKeysView.vue index fbf4dfcc..ac22cda2 100644 --- a/web/admin-spa/src/views/ApiKeysView.vue +++ b/web/admin-spa/src/views/ApiKeysView.vue @@ -89,6 +89,19 @@ 刷新 + + + + + 编辑选中 ({{ selectedApiKeys.length }}) + + + + + + + Bedrock + + + {{ getBedrockBindingInfo(key) }} + + @@ -859,13 +885,26 @@ {{ getOpenAIBindingInfo(key) }} + + + + + Bedrock + + + {{ getBedrockBindingInfo(key) }} + + @@ -1158,6 +1197,14 @@ @close="showBatchApiKeyModal = false" /> + + { // 加载账户列表 const loadAccounts = async () => { try { - const [claudeData, claudeConsoleData, geminiData, openaiData, groupsData] = await Promise.all([ - apiClient.get('/admin/claude-accounts'), - apiClient.get('/admin/claude-console-accounts'), - apiClient.get('/admin/gemini-accounts'), - apiClient.get('/admin/openai-accounts'), - apiClient.get('/admin/account-groups') - ]) + const [claudeData, claudeConsoleData, geminiData, openaiData, bedrockData, groupsData] = + await Promise.all([ + apiClient.get('/admin/claude-accounts'), + apiClient.get('/admin/claude-console-accounts'), + apiClient.get('/admin/gemini-accounts'), + apiClient.get('/admin/openai-accounts'), + apiClient.get('/admin/bedrock-accounts'), + apiClient.get('/admin/account-groups') + ]) + + // 合并Claude OAuth账户和Claude Console账户 + const claudeAccounts = [] if (claudeData.success) { - accounts.value.claude = claudeData.data || [] + claudeData.data?.forEach((account) => { + claudeAccounts.push({ + ...account, + platform: 'claude-oauth', + isDedicated: account.accountType === 'dedicated' + }) + }) } if (claudeConsoleData.success) { - // 将 Claude Console 账号合并到 claude 数组中 - const consoleAccounts = (claudeConsoleData.data || []).map((acc) => ({ - ...acc, - platform: 'claude-console' - })) - accounts.value.claude = [...accounts.value.claude, ...consoleAccounts] + claudeConsoleData.data?.forEach((account) => { + claudeAccounts.push({ + ...account, + platform: 'claude-console', + isDedicated: account.accountType === 'dedicated' + }) + }) } + accounts.value.claude = claudeAccounts + if (geminiData.success) { - accounts.value.gemini = geminiData.data || [] + accounts.value.gemini = (geminiData.data || []).map((account) => ({ + ...account, + isDedicated: account.accountType === 'dedicated' + })) } if (openaiData.success) { - accounts.value.openai = openaiData.data || [] + accounts.value.openai = (openaiData.data || []).map((account) => ({ + ...account, + isDedicated: account.accountType === 'dedicated' + })) + } + + if (bedrockData.success) { + accounts.value.bedrock = (bedrockData.data || []).map((account) => ({ + ...account, + isDedicated: account.accountType === 'dedicated' + })) } if (groupsData.success) { @@ -1494,6 +1571,12 @@ const getBoundAccountName = (accountId) => { return `${openaiAccount.name}` } + // 从Bedrock账户列表中查找 + const bedrockAccount = accounts.value.bedrock.find((acc) => acc.id === accountId) + if (bedrockAccount) { + return `${bedrockAccount.name}` + } + // 如果找不到,返回账户ID的前8位 return `${accountId.substring(0, 8)}` } @@ -1567,6 +1650,26 @@ const getOpenAIBindingInfo = (key) => { return '' } +// 获取Bedrock绑定信息 +const getBedrockBindingInfo = (key) => { + if (key.bedrockAccountId) { + const info = getBoundAccountName(key.bedrockAccountId) + if (key.bedrockAccountId.startsWith('group:')) { + return info + } + // 检查账户是否存在 + const account = accounts.value.bedrock.find((acc) => acc.id === key.bedrockAccountId) + if (!account) { + return `⚠️ ${info} (账户不存在)` + } + if (account.accountType === 'dedicated') { + return `🔒 专属-${info}` + } + return info + } + return '' +} + // 检查API Key是否过期 const isApiKeyExpired = (expiresAt) => { if (!expiresAt) return false @@ -1799,6 +1902,27 @@ const handleBatchCreateSuccess = (data) => { loadApiKeys() } +// 打开批量编辑模态框 +const openBatchEditModal = async () => { + if (selectedApiKeys.value.length === 0) { + showToast('请先选择要编辑的 API Keys', 'warning') + return + } + + // 重新加载账号数据,确保显示最新的专属账号 + await loadAccounts() + showBatchEditModal.value = true +} + +// 处理批量编辑成功 +const handleBatchEditSuccess = () => { + showBatchEditModal.value = false + // 清空选中状态 + selectedApiKeys.value = [] + updateSelectAllState() + loadApiKeys() +} + // 处理编辑成功 const handleEditSuccess = () => { showEditApiKeyModal.value = false
批量编辑说明
+ 以下设置将应用到所选的 {{ selectedCount }} 个 API + Key。只有填写或修改的字段才会被更新,空白字段将保持原值不变。 +