mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
Merge pull request #814 from Guccbai/feature/multi-select-permissions [skip ci]
feat(permissions): 服务权限从单选改为多选
This commit is contained in:
@@ -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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 = {}) {
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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: '',
|
||||||
|
|||||||
@@ -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}`
|
||||||
|
|||||||
Reference in New Issue
Block a user