mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
保存当前API Key管理功能的修改
- 统一用户创建和admin创建API Key的逻辑 - 修复admin更新用户创建的API Key功能 - 用户创建API Key名称改为displayName - 默认无限制配置 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -791,6 +791,8 @@ router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const { keyId } = req.params
|
const { keyId } = req.params
|
||||||
const {
|
const {
|
||||||
|
name,
|
||||||
|
description,
|
||||||
tokenLimit,
|
tokenLimit,
|
||||||
concurrencyLimit,
|
concurrencyLimit,
|
||||||
rateLimitWindow,
|
rateLimitWindow,
|
||||||
@@ -814,6 +816,30 @@ router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => {
|
|||||||
// 只允许更新指定字段
|
// 只允许更新指定字段
|
||||||
const updates = {}
|
const updates = {}
|
||||||
|
|
||||||
|
// 处理name字段
|
||||||
|
if (name !== undefined) {
|
||||||
|
if (name === null || name === '') {
|
||||||
|
return res.status(400).json({ error: 'Name cannot be empty' })
|
||||||
|
}
|
||||||
|
if (typeof name !== 'string' || name.trim().length === 0) {
|
||||||
|
return res.status(400).json({ error: 'Name must be a non-empty string' })
|
||||||
|
}
|
||||||
|
if (name.length > 100) {
|
||||||
|
return res.status(400).json({ error: 'Name must be less than 100 characters' })
|
||||||
|
}
|
||||||
|
updates.name = name.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理description字段
|
||||||
|
if (description !== undefined) {
|
||||||
|
if (description && (typeof description !== 'string' || description.length > 500)) {
|
||||||
|
return res
|
||||||
|
.status(400)
|
||||||
|
.json({ error: 'Description must be a string with less than 500 characters' })
|
||||||
|
}
|
||||||
|
updates.description = description || ''
|
||||||
|
}
|
||||||
|
|
||||||
if (tokenLimit !== undefined && tokenLimit !== null && tokenLimit !== '') {
|
if (tokenLimit !== undefined && tokenLimit !== null && tokenLimit !== '') {
|
||||||
if (!Number.isInteger(Number(tokenLimit)) || Number(tokenLimit) < 0) {
|
if (!Number.isInteger(Number(tokenLimit)) || Number(tokenLimit) < 0) {
|
||||||
return res.status(400).json({ error: 'Token limit must be a non-negative integer' })
|
return res.status(400).json({ error: 'Token limit must be a non-negative integer' })
|
||||||
@@ -954,12 +980,20 @@ router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => {
|
|||||||
updates.isActive = isActive
|
updates.isActive = isActive
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.info(`🔧 Admin updating API key: ${keyId}`, {
|
||||||
|
updates: Object.keys(updates),
|
||||||
|
updatesData: updates
|
||||||
|
})
|
||||||
|
|
||||||
await apiKeyService.updateApiKey(keyId, updates)
|
await apiKeyService.updateApiKey(keyId, updates)
|
||||||
|
|
||||||
logger.success(`📝 Admin updated API key: ${keyId}`)
|
logger.success(`📝 Admin updated API key: ${keyId}`)
|
||||||
return res.json({ success: true, message: 'API key updated successfully' })
|
return res.json({ success: true, message: 'API key updated successfully' })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('❌ Failed to update API key:', error)
|
logger.error(`❌ Failed to update API key ${req.params.keyId}:`, {
|
||||||
|
error: error.message,
|
||||||
|
stack: error.stack
|
||||||
|
})
|
||||||
return res.status(500).json({ error: 'Failed to update API key', message: error.message })
|
return res.status(500).json({ error: 'Failed to update API key', message: error.message })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -397,57 +397,44 @@ const authenticateUser = (req, res, next) => {
|
|||||||
*/
|
*/
|
||||||
router.get('/user/api-keys', authenticateUser, async (req, res) => {
|
router.get('/user/api-keys', authenticateUser, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
const apiKeyService = require('../services/apiKeyService')
|
||||||
const redis = require('../models/redis')
|
const redis = require('../models/redis')
|
||||||
const { username, displayName } = req.user
|
const { username, displayName } = req.user
|
||||||
|
|
||||||
logger.info(`获取用户API Keys: ${username}, displayName: ${displayName}`)
|
logger.info(`获取用户API Keys: ${username}, displayName: ${displayName}`)
|
||||||
logger.info(`用户完整信息: ${JSON.stringify(req.user)}`)
|
|
||||||
|
|
||||||
// 获取所有API Keys
|
// 使用与admin相同的API Key服务,获取所有API Keys的完整信息
|
||||||
const allKeysPattern = 'api_key:*'
|
const allApiKeys = await apiKeyService.getAllApiKeys()
|
||||||
const keys = await redis.getClient().keys(allKeysPattern)
|
|
||||||
|
|
||||||
const userKeys = []
|
const userKeys = []
|
||||||
let foundHistoricalKey = false
|
let foundHistoricalKey = false
|
||||||
|
|
||||||
// 筛选属于该用户的API Keys
|
// 筛选属于该用户的API Keys,并处理自动关联
|
||||||
for (const key of keys) {
|
for (const apiKey of allApiKeys) {
|
||||||
const apiKeyData = await redis.getClient().hgetall(key)
|
logger.debug(
|
||||||
if (!apiKeyData) {
|
`检查API Key: ${apiKey.id}, name: "${apiKey.name}", owner: "${apiKey.owner || '无'}", displayName: "${displayName}"`
|
||||||
continue
|
)
|
||||||
}
|
|
||||||
|
|
||||||
// 规则1: 直接owner匹配(已关联的Key)
|
// 规则1: 直接owner匹配(已关联的Key)
|
||||||
if (apiKeyData.owner === username) {
|
if (apiKey.owner === username) {
|
||||||
userKeys.push({
|
logger.info(`找到已关联的API Key: ${apiKey.id}`)
|
||||||
id: apiKeyData.id,
|
userKeys.push(apiKey)
|
||||||
name: apiKeyData.name || '未命名',
|
|
||||||
key: apiKeyData.key,
|
|
||||||
limit: parseInt(apiKeyData.limit) || 1000000,
|
|
||||||
used: parseInt(apiKeyData.used) || 0,
|
|
||||||
createdAt: apiKeyData.createdAt,
|
|
||||||
status: apiKeyData.status || 'active'
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
// 规则2: 历史Key自动关联(name字段匹配displayName且无owner)
|
// 规则2: 历史Key自动关联(name字段匹配displayName且无owner)
|
||||||
else if (displayName && apiKeyData.name === displayName && !apiKeyData.owner) {
|
else if (displayName && apiKey.name === displayName && !apiKey.owner) {
|
||||||
logger.info(`发现历史API Key需要关联: name=${apiKeyData.name}, displayName=${displayName}`)
|
logger.info(
|
||||||
|
`🔗 发现历史API Key需要关联: id=${apiKey.id}, name="${apiKey.name}", displayName="${displayName}"`
|
||||||
|
)
|
||||||
|
|
||||||
// 自动关联: 设置owner为当前用户
|
// 自动关联: 设置owner为当前用户
|
||||||
await redis.getClient().hset(key, 'owner', username)
|
await redis.getClient().hset(`apikey:${apiKey.id}`, 'owner', username)
|
||||||
foundHistoricalKey = true
|
foundHistoricalKey = true
|
||||||
|
|
||||||
userKeys.push({
|
// 更新本地数据并添加到用户Key列表
|
||||||
id: apiKeyData.id,
|
apiKey.owner = username
|
||||||
name: apiKeyData.name || '未命名',
|
userKeys.push(apiKey)
|
||||||
key: apiKeyData.key,
|
|
||||||
limit: parseInt(apiKeyData.limit) || 1000000,
|
|
||||||
used: parseInt(apiKeyData.used) || 0,
|
|
||||||
createdAt: apiKeyData.createdAt,
|
|
||||||
status: apiKeyData.status || 'active'
|
|
||||||
})
|
|
||||||
|
|
||||||
logger.info(`历史API Key关联成功: ${apiKeyData.id} -> ${username}`)
|
logger.info(`✅ 历史API Key关联成功: ${apiKey.id} -> ${username}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -474,11 +461,11 @@ router.get('/user/api-keys', authenticateUser, async (req, res) => {
|
|||||||
router.post('/user/api-keys', authenticateUser, async (req, res) => {
|
router.post('/user/api-keys', authenticateUser, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { username } = req.user
|
const { username } = req.user
|
||||||
const { name, limit } = req.body
|
const { limit } = req.body
|
||||||
|
|
||||||
// 检查用户是否已有API Key
|
// 检查用户是否已有API Key
|
||||||
const redis = require('../models/redis')
|
const redis = require('../models/redis')
|
||||||
const allKeysPattern = 'api_key:*'
|
const allKeysPattern = 'apikey:*'
|
||||||
const keys = await redis.getClient().keys(allKeysPattern)
|
const keys = await redis.getClient().keys(allKeysPattern)
|
||||||
|
|
||||||
let userKeyCount = 0
|
let userKeyCount = 0
|
||||||
@@ -496,45 +483,53 @@ router.post('/user/api-keys', authenticateUser, async (req, res) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成API Key
|
// 使用与admin相同的API Key生成服务,确保数据结构一致性
|
||||||
const crypto = require('crypto')
|
const apiKeyService = require('../services/apiKeyService')
|
||||||
const uuid = require('uuid')
|
|
||||||
|
|
||||||
const keyId = uuid.v4()
|
// 获取用户的显示名称
|
||||||
const apiKey = `cr_${crypto.randomBytes(32).toString('hex')}`
|
const { displayName } = req.user
|
||||||
|
// 用户创建的API Key名称固定为displayName,不允许自定义
|
||||||
|
const defaultName = displayName || username
|
||||||
|
|
||||||
const keyData = {
|
const keyParams = {
|
||||||
id: keyId,
|
name: defaultName, // 忽略用户输入的name,强制使用displayName
|
||||||
key: apiKey,
|
tokenLimit: limit || 0,
|
||||||
name: name || 'AD用户密钥',
|
description: `AD用户${username}创建的API Key`,
|
||||||
limit: limit || 100000,
|
// AD用户创建的Key添加owner信息以区分用户归属
|
||||||
used: 0,
|
|
||||||
owner: username,
|
owner: username,
|
||||||
ownerType: 'ad_user',
|
ownerType: 'ad_user',
|
||||||
createdAt: new Date().toISOString(),
|
// 确保用户创建的Key默认激活
|
||||||
status: 'active'
|
isActive: true,
|
||||||
|
// 设置基本权限(与admin创建保持一致)
|
||||||
|
permissions: 'all',
|
||||||
|
// 设置合理的并发和速率限制(与admin创建保持一致)
|
||||||
|
concurrencyLimit: 0,
|
||||||
|
rateLimitWindow: 0,
|
||||||
|
rateLimitRequests: 0,
|
||||||
|
// 添加标签标识AD用户创建
|
||||||
|
tags: ['ad-user', 'user-created']
|
||||||
}
|
}
|
||||||
|
|
||||||
// 存储到Redis
|
const newKey = await apiKeyService.generateApiKey(keyParams)
|
||||||
await redis.getClient().hset(`api_key:${keyId}`, keyData)
|
|
||||||
|
|
||||||
// 创建哈希映射以快速查找
|
logger.info(`用户${username}创建API Key成功: ${newKey.id}`)
|
||||||
const keyHash = crypto.createHash('sha256').update(apiKey).digest('hex')
|
|
||||||
await redis.getClient().set(`api_key_hash:${keyHash}`, keyId)
|
|
||||||
|
|
||||||
logger.info(`用户${username}创建API Key成功: ${keyId}`)
|
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: 'API Key创建成功',
|
message: 'API Key创建成功',
|
||||||
apiKey: {
|
apiKey: {
|
||||||
id: keyId,
|
id: newKey.id,
|
||||||
key: apiKey,
|
key: newKey.apiKey, // 返回完整的API Key
|
||||||
name: keyData.name,
|
name: newKey.name,
|
||||||
limit: keyData.limit,
|
tokenLimit: newKey.tokenLimit || limit || 0,
|
||||||
used: 0,
|
used: 0,
|
||||||
createdAt: keyData.createdAt,
|
createdAt: newKey.createdAt,
|
||||||
status: keyData.status
|
isActive: true,
|
||||||
|
usage: {
|
||||||
|
daily: { requests: 0, tokens: 0 },
|
||||||
|
total: { requests: 0, tokens: 0 }
|
||||||
|
},
|
||||||
|
dailyCost: 0
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -555,7 +550,7 @@ router.get('/user/usage-stats', authenticateUser, async (req, res) => {
|
|||||||
const redis = require('../models/redis')
|
const redis = require('../models/redis')
|
||||||
|
|
||||||
// 获取用户的API Keys
|
// 获取用户的API Keys
|
||||||
const allKeysPattern = 'api_key:*'
|
const allKeysPattern = 'apikey:*'
|
||||||
const keys = await redis.getClient().keys(allKeysPattern)
|
const keys = await redis.getClient().keys(allKeysPattern)
|
||||||
|
|
||||||
let totalUsage = 0
|
let totalUsage = 0
|
||||||
@@ -600,4 +595,88 @@ router.get('/user/usage-stats', authenticateUser, async (req, res) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新用户API Key
|
||||||
|
*/
|
||||||
|
router.put('/user/api-keys/:keyId', authenticateUser, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { username } = req.user
|
||||||
|
const { keyId } = req.params
|
||||||
|
const updates = req.body
|
||||||
|
|
||||||
|
// 验证用户只能编辑自己的API Key
|
||||||
|
const apiKeyService = require('../services/apiKeyService')
|
||||||
|
const allApiKeys = await apiKeyService.getAllApiKeys()
|
||||||
|
const apiKey = allApiKeys.find((key) => key.id === keyId && key.owner === username)
|
||||||
|
|
||||||
|
if (!apiKey) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: 'API Key 不存在或无权限'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 限制用户只能修改特定字段
|
||||||
|
const allowedFields = ['name', 'description', 'isActive']
|
||||||
|
const filteredUpdates = {}
|
||||||
|
for (const [key, value] of Object.entries(updates)) {
|
||||||
|
if (allowedFields.includes(key)) {
|
||||||
|
filteredUpdates[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await apiKeyService.updateApiKey(keyId, filteredUpdates)
|
||||||
|
|
||||||
|
logger.info(`用户 ${username} 更新了 API Key: ${keyId}`)
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'API Key 更新成功'
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('更新用户API Key失败:', error)
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '更新 API Key 失败'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除用户API Key
|
||||||
|
*/
|
||||||
|
router.delete('/user/api-keys/:keyId', authenticateUser, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { username } = req.user
|
||||||
|
const { keyId } = req.params
|
||||||
|
|
||||||
|
// 验证用户只能删除自己的API Key
|
||||||
|
const apiKeyService = require('../services/apiKeyService')
|
||||||
|
const allApiKeys = await apiKeyService.getAllApiKeys()
|
||||||
|
const apiKey = allApiKeys.find((key) => key.id === keyId && key.owner === username)
|
||||||
|
|
||||||
|
if (!apiKey) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: 'API Key 不存在或无权限'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
await apiKeyService.deleteApiKey(keyId)
|
||||||
|
|
||||||
|
logger.info(`用户 ${username} 删除了 API Key: ${keyId}`)
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'API Key 删除成功'
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('删除用户API Key失败:', error)
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '删除 API Key 失败'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
module.exports = router
|
module.exports = router
|
||||||
|
|||||||
@@ -32,7 +32,9 @@ class ApiKeyService {
|
|||||||
enableClientRestriction = false,
|
enableClientRestriction = false,
|
||||||
allowedClients = [],
|
allowedClients = [],
|
||||||
dailyCostLimit = 0,
|
dailyCostLimit = 0,
|
||||||
tags = []
|
tags = [],
|
||||||
|
owner = null,
|
||||||
|
ownerType = null
|
||||||
} = options
|
} = options
|
||||||
|
|
||||||
// 生成简单的API Key (64字符十六进制)
|
// 生成简单的API Key (64字符十六进制)
|
||||||
@@ -66,7 +68,9 @@ class ApiKeyService {
|
|||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
lastUsedAt: '',
|
lastUsedAt: '',
|
||||||
expiresAt: expiresAt || '',
|
expiresAt: expiresAt || '',
|
||||||
createdBy: 'admin' // 可以根据需要扩展用户系统
|
createdBy: 'admin', // 可以根据需要扩展用户系统
|
||||||
|
owner: owner || '',
|
||||||
|
ownerType: ownerType || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存API Key数据并建立哈希映射
|
// 保存API Key数据并建立哈希映射
|
||||||
@@ -99,7 +103,9 @@ class ApiKeyService {
|
|||||||
tags: JSON.parse(keyData.tags || '[]'),
|
tags: JSON.parse(keyData.tags || '[]'),
|
||||||
createdAt: keyData.createdAt,
|
createdAt: keyData.createdAt,
|
||||||
expiresAt: keyData.expiresAt,
|
expiresAt: keyData.expiresAt,
|
||||||
createdBy: keyData.createdBy
|
createdBy: keyData.createdBy,
|
||||||
|
owner: keyData.owner,
|
||||||
|
ownerType: keyData.ownerType
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -294,11 +300,21 @@ class ApiKeyService {
|
|||||||
// 📝 更新API Key
|
// 📝 更新API Key
|
||||||
async updateApiKey(keyId, updates) {
|
async updateApiKey(keyId, updates) {
|
||||||
try {
|
try {
|
||||||
|
logger.debug(`🔧 Updating API key ${keyId} with:`, updates)
|
||||||
|
|
||||||
const keyData = await redis.getApiKey(keyId)
|
const keyData = await redis.getApiKey(keyId)
|
||||||
if (!keyData || Object.keys(keyData).length === 0) {
|
if (!keyData || Object.keys(keyData).length === 0) {
|
||||||
|
logger.error(`❌ API key not found: ${keyId}`)
|
||||||
throw new Error('API key not found')
|
throw new Error('API key not found')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.debug(`📋 Current API key data:`, {
|
||||||
|
id: keyData.id,
|
||||||
|
name: keyData.name,
|
||||||
|
owner: keyData.owner,
|
||||||
|
ownerType: keyData.ownerType
|
||||||
|
})
|
||||||
|
|
||||||
// 允许更新的字段
|
// 允许更新的字段
|
||||||
const allowedUpdates = [
|
const allowedUpdates = [
|
||||||
'name',
|
'name',
|
||||||
@@ -344,7 +360,10 @@ class ApiKeyService {
|
|||||||
// 更新时不需要重新建立哈希映射,因为API Key本身没有变化
|
// 更新时不需要重新建立哈希映射,因为API Key本身没有变化
|
||||||
await redis.setApiKey(keyId, updatedData)
|
await redis.setApiKey(keyId, updatedData)
|
||||||
|
|
||||||
logger.success(`📝 Updated API key: ${keyId}`)
|
logger.success(`📝 Updated API key: ${keyId}`, {
|
||||||
|
updatedFields: Object.keys(updates),
|
||||||
|
newName: updatedData.name
|
||||||
|
})
|
||||||
|
|
||||||
return { success: true }
|
return { success: true }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
54
test-fixed-auto-link.js
Normal file
54
test-fixed-auto-link.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
const jwt = require('jsonwebtoken');
|
||||||
|
const config = require('./config/config');
|
||||||
|
const fetch = require('node-fetch');
|
||||||
|
|
||||||
|
// 模拟创建一个包含displayName的JWT token
|
||||||
|
const userInfo = {
|
||||||
|
type: 'ad_user',
|
||||||
|
username: 'zhangji',
|
||||||
|
displayName: '张佶',
|
||||||
|
email: 'zhangji@weidian.com',
|
||||||
|
groups: ['CN=Weidian-IT组,OU=Weidian Groups,OU=微店,DC=corp,DC=weidian-inc,DC=com'],
|
||||||
|
loginTime: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
const token = jwt.sign(userInfo, config.security.jwtSecret, {
|
||||||
|
expiresIn: '8h'
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('测试修正后的自动关联功能');
|
||||||
|
console.log('用户displayName: 张佶');
|
||||||
|
|
||||||
|
async function testFixedAutoLink() {
|
||||||
|
try {
|
||||||
|
console.log('\n=== 测试获取用户API Keys ===');
|
||||||
|
|
||||||
|
const response = await fetch('http://localhost:3000/admin/ldap/user/api-keys', {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
console.log('\n结果:', JSON.stringify(result, null, 2));
|
||||||
|
|
||||||
|
if (result.success && result.apiKeys && result.apiKeys.length > 0) {
|
||||||
|
console.log('\n✅ 成功!找到了关联的API Key:');
|
||||||
|
result.apiKeys.forEach(key => {
|
||||||
|
console.log(`- ID: ${key.id}`);
|
||||||
|
console.log(`- Name: ${key.name}`);
|
||||||
|
console.log(`- Key: ${key.key.substring(0, 10)}...${key.key.substring(key.key.length-10)}`);
|
||||||
|
console.log(`- Limit: ${key.limit}`);
|
||||||
|
console.log(`- Status: ${key.status}`);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log('\n❌ 没有找到关联的API Key');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('测试错误:', error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
testFixedAutoLink();
|
||||||
@@ -33,12 +33,31 @@
|
|||||||
>名称</label
|
>名称</label
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
class="form-input w-full cursor-not-allowed bg-gray-100 text-sm dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400"
|
v-model="form.name"
|
||||||
disabled
|
class="form-input w-full text-sm"
|
||||||
|
maxlength="100"
|
||||||
|
placeholder="请输入API Key名称"
|
||||||
|
required
|
||||||
type="text"
|
type="text"
|
||||||
:value="form.name"
|
|
||||||
/>
|
/>
|
||||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400 sm:mt-2">名称不可修改</p>
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400 sm:mt-2">最多100个字符</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
class="mb-1.5 block text-xs font-semibold text-gray-700 dark:text-gray-300 sm:mb-3 sm:text-sm"
|
||||||
|
>描述</label
|
||||||
|
>
|
||||||
|
<textarea
|
||||||
|
v-model="form.description"
|
||||||
|
class="form-input w-full text-sm"
|
||||||
|
maxlength="500"
|
||||||
|
placeholder="请输入API Key描述(可选)"
|
||||||
|
rows="3"
|
||||||
|
></textarea>
|
||||||
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400 sm:mt-2">
|
||||||
|
最多500个字符(可选)
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 标签 -->
|
<!-- 标签 -->
|
||||||
@@ -632,6 +651,7 @@ const unselectedTags = computed(() => {
|
|||||||
// 表单数据
|
// 表单数据
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
name: '',
|
name: '',
|
||||||
|
description: '',
|
||||||
tokenLimit: '',
|
tokenLimit: '',
|
||||||
rateLimitWindow: '',
|
rateLimitWindow: '',
|
||||||
rateLimitRequests: '',
|
rateLimitRequests: '',
|
||||||
@@ -707,6 +727,8 @@ const updateApiKey = async () => {
|
|||||||
try {
|
try {
|
||||||
// 准备提交的数据
|
// 准备提交的数据
|
||||||
const data = {
|
const data = {
|
||||||
|
name: form.name,
|
||||||
|
description: form.description,
|
||||||
tokenLimit:
|
tokenLimit:
|
||||||
form.tokenLimit !== '' && form.tokenLimit !== null ? parseInt(form.tokenLimit) : 0,
|
form.tokenLimit !== '' && form.tokenLimit !== null ? parseInt(form.tokenLimit) : 0,
|
||||||
rateLimitWindow:
|
rateLimitWindow:
|
||||||
@@ -893,6 +915,7 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
form.name = props.apiKey.name
|
form.name = props.apiKey.name
|
||||||
|
form.description = props.apiKey.description || ''
|
||||||
form.tokenLimit = props.apiKey.tokenLimit || ''
|
form.tokenLimit = props.apiKey.tokenLimit || ''
|
||||||
form.rateLimitWindow = props.apiKey.rateLimitWindow || ''
|
form.rateLimitWindow = props.apiKey.rateLimitWindow || ''
|
||||||
form.rateLimitRequests = props.apiKey.rateLimitRequests || ''
|
form.rateLimitRequests = props.apiKey.rateLimitRequests || ''
|
||||||
|
|||||||
@@ -26,18 +26,15 @@
|
|||||||
API Key 将用于访问 Claude Relay Service
|
API Key 将用于访问 Claude Relay Service
|
||||||
</p>
|
</p>
|
||||||
<form class="mx-auto max-w-md space-y-3" @submit.prevent="createApiKey">
|
<form class="mx-auto max-w-md space-y-3" @submit.prevent="createApiKey">
|
||||||
<input
|
<p class="text-center text-sm text-gray-600 dark:text-gray-400">
|
||||||
v-model="newKeyForm.name"
|
API Key 名称将自动设置为您的用户名
|
||||||
class="w-full rounded-xl border border-gray-300 bg-white px-4 py-3 text-gray-800 placeholder-gray-500 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200 dark:placeholder-gray-400"
|
</p>
|
||||||
placeholder="API Key 名称(可选)"
|
|
||||||
type="text"
|
|
||||||
/>
|
|
||||||
<input
|
<input
|
||||||
v-model.number="newKeyForm.limit"
|
v-model.number="newKeyForm.limit"
|
||||||
class="w-full rounded-xl border border-gray-300 bg-white px-4 py-3 text-gray-800 placeholder-gray-500 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200 dark:placeholder-gray-400"
|
class="w-full rounded-xl border border-gray-300 bg-white px-4 py-3 text-gray-800 placeholder-gray-500 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200 dark:placeholder-gray-400"
|
||||||
max="1000000"
|
max="1000000"
|
||||||
min="1000"
|
min="0"
|
||||||
placeholder="使用额度(默认 100,000)"
|
placeholder="使用额度(0表示无限制)"
|
||||||
type="number"
|
type="number"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
@@ -82,51 +79,69 @@
|
|||||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||||
创建时间:{{ formatDate(apiKey.createdAt) }}
|
创建时间:{{ formatDate(apiKey.createdAt) }}
|
||||||
</p>
|
</p>
|
||||||
<div class="mt-2 flex items-center gap-2">
|
<div class="mt-2 flex items-center justify-between">
|
||||||
<span
|
<span
|
||||||
class="inline-flex items-center gap-1 rounded-full px-2 py-1 text-xs font-medium"
|
class="inline-flex items-center gap-1 rounded-full px-2 py-1 text-xs font-medium"
|
||||||
:class="
|
:class="
|
||||||
apiKey.status === 'active'
|
apiKey.isActive
|
||||||
? 'bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-400'
|
? 'bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-400'
|
||||||
: 'bg-red-100 text-red-800 dark:bg-red-900/20 dark:text-red-400'
|
: 'bg-red-100 text-red-800 dark:bg-red-900/20 dark:text-red-400'
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<i
|
<i :class="apiKey.isActive ? 'fas fa-check-circle' : 'fas fa-times-circle'" />
|
||||||
:class="
|
{{ apiKey.isActive ? '活跃' : '已禁用' }}
|
||||||
apiKey.status === 'active' ? 'fas fa-check-circle' : 'fas fa-times-circle'
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
{{ apiKey.status === 'active' ? '活跃' : '已禁用' }}
|
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
class="rounded-lg px-3 py-1 text-xs font-medium text-blue-600 transition-colors hover:bg-blue-50 dark:text-blue-400 dark:hover:bg-blue-900/20"
|
||||||
|
@click="editApiKey(apiKey)"
|
||||||
|
>
|
||||||
|
<i class="fas fa-edit mr-1" />编辑
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
:class="[
|
||||||
|
apiKey.isActive
|
||||||
|
? 'text-orange-600 hover:bg-orange-50 dark:text-orange-400 dark:hover:bg-orange-900/20'
|
||||||
|
: 'text-green-600 hover:bg-green-50 dark:text-green-400 dark:hover:bg-green-900/20',
|
||||||
|
'rounded-lg px-3 py-1 text-xs font-medium transition-colors'
|
||||||
|
]"
|
||||||
|
@click="toggleApiKeyStatus(apiKey)"
|
||||||
|
>
|
||||||
|
<i :class="['fas mr-1', apiKey.isActive ? 'fa-ban' : 'fa-check-circle']" />
|
||||||
|
{{ apiKey.isActive ? '禁用' : '激活' }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="rounded-lg px-3 py-1 text-xs font-medium text-red-600 transition-colors hover:bg-red-50 dark:text-red-400 dark:hover:bg-red-900/20"
|
||||||
|
@click="deleteApiKey(apiKey)"
|
||||||
|
>
|
||||||
|
<i class="fas fa-trash mr-1" />删除
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- API Key 显示 -->
|
<!-- API Key 显示 - 历史Key无法显示原始内容 -->
|
||||||
<div class="mb-4 space-y-3">
|
<div class="mb-4 space-y-3">
|
||||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
API Key
|
API Key
|
||||||
</label>
|
</label>
|
||||||
<div class="flex gap-2">
|
<div
|
||||||
<input
|
class="rounded-xl border border-amber-300 bg-amber-50 p-4 dark:border-amber-600 dark:bg-amber-900/20"
|
||||||
class="flex-1 rounded-xl border border-gray-300 bg-gray-50 px-4 py-3 font-mono text-sm text-gray-600 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400"
|
>
|
||||||
readonly
|
<div class="flex items-center gap-2 text-amber-800 dark:text-amber-200">
|
||||||
type="text"
|
<i class="fas fa-info-circle" />
|
||||||
:value="showApiKey[apiKey.id] ? apiKey.key : maskApiKey(apiKey.key)"
|
<span class="text-sm">
|
||||||
/>
|
已关联的历史API
|
||||||
<button
|
Key无法显示原始内容,仅在创建时可见。如需查看完整Key,请联系管理员或创建新Key。
|
||||||
class="rounded-xl border border-gray-300 px-4 py-3 text-gray-600 transition-colors hover:bg-gray-100 dark:border-gray-600 dark:text-gray-400 dark:hover:bg-gray-700"
|
</span>
|
||||||
@click="toggleApiKeyVisibility(apiKey.id)"
|
</div>
|
||||||
>
|
<div class="mt-2 text-xs text-amber-600 dark:text-amber-400">
|
||||||
<i :class="showApiKey[apiKey.id] ? 'fas fa-eye-slash' : 'fas fa-eye'" />
|
Key ID: {{ apiKey.id }}
|
||||||
</button>
|
</div>
|
||||||
<button
|
|
||||||
class="rounded-xl border border-gray-300 px-4 py-3 text-gray-600 transition-colors hover:bg-gray-100 dark:border-gray-600 dark:text-gray-400 dark:hover:bg-gray-700"
|
|
||||||
@click="copyApiKey(apiKey.key)"
|
|
||||||
>
|
|
||||||
<i class="fas fa-copy" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -136,52 +151,86 @@
|
|||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<i class="fas fa-chart-bar text-blue-500" />
|
<i class="fas fa-chart-bar text-blue-500" />
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm font-medium text-blue-800 dark:text-blue-300">已使用</p>
|
<p class="text-sm font-medium text-blue-800 dark:text-blue-300">今日请求</p>
|
||||||
<p class="text-xl font-bold text-blue-900 dark:text-blue-100">
|
<p class="text-xl font-bold text-blue-900 dark:text-blue-100">
|
||||||
{{ apiKey.used?.toLocaleString() || 0 }}
|
{{ apiKey.usage?.daily?.requests?.toLocaleString() || 0 }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-xl bg-green-50 p-4 dark:bg-green-900/20">
|
<div class="rounded-xl bg-green-50 p-4 dark:bg-green-900/20">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<i class="fas fa-battery-three-quarters text-green-500" />
|
<i class="fas fa-coins text-green-500" />
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm font-medium text-green-800 dark:text-green-300">总额度</p>
|
<p class="text-sm font-medium text-green-800 dark:text-green-300">今日Token</p>
|
||||||
<p class="text-xl font-bold text-green-900 dark:text-green-100">
|
<p class="text-xl font-bold text-green-900 dark:text-green-100">
|
||||||
{{ apiKey.limit?.toLocaleString() || 0 }}
|
{{ apiKey.usage?.daily?.tokens?.toLocaleString() || 0 }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-xl bg-purple-50 p-4 dark:bg-purple-900/20">
|
<div class="rounded-xl bg-purple-50 p-4 dark:bg-purple-900/20">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<i class="fas fa-percentage text-purple-500" />
|
<i class="fas fa-dollar-sign text-purple-500" />
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm font-medium text-purple-800 dark:text-purple-300">使用率</p>
|
<p class="text-sm font-medium text-purple-800 dark:text-purple-300">今日费用</p>
|
||||||
<p class="text-xl font-bold text-purple-900 dark:text-purple-100">
|
<p class="text-xl font-bold text-purple-900 dark:text-purple-100">
|
||||||
{{ calculateUsagePercentage(apiKey.used, apiKey.limit) }}%
|
${{ (apiKey.dailyCost || 0).toFixed(4) }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 使用进度条 -->
|
<!-- Token 额度进度条 -->
|
||||||
<div class="mt-4">
|
<div v-if="apiKey.tokenLimit > 0" class="mt-4">
|
||||||
<div class="flex items-center justify-between text-sm text-gray-600 dark:text-gray-400">
|
<div class="flex items-center justify-between text-sm text-gray-600 dark:text-gray-400">
|
||||||
<span>使用进度</span>
|
<span>Token 使用进度</span>
|
||||||
<span>{{ calculateUsagePercentage(apiKey.used, apiKey.limit) }}%</span>
|
<span>
|
||||||
|
{{ apiKey.usage?.total?.tokens?.toLocaleString() || 0 }} /
|
||||||
|
{{ apiKey.tokenLimit?.toLocaleString() || 0 }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-2 h-3 overflow-hidden rounded-full bg-gray-200 dark:bg-gray-700">
|
<div class="mt-2 h-3 overflow-hidden rounded-full bg-gray-200 dark:bg-gray-700">
|
||||||
<div
|
<div
|
||||||
class="h-full rounded-full bg-gradient-to-r from-blue-500 to-purple-600 transition-all duration-500"
|
class="h-full rounded-full bg-gradient-to-r from-blue-500 to-purple-600 transition-all duration-500"
|
||||||
:style="{
|
:style="{
|
||||||
width: `${Math.min(calculateUsagePercentage(apiKey.used, apiKey.limit), 100)}%`
|
width: `${Math.min(calculateTokenUsagePercentage(apiKey.usage?.total?.tokens || 0, apiKey.tokenLimit || 0), 100)}%`
|
||||||
}"
|
}"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 费用限制进度条 -->
|
||||||
|
<div v-if="apiKey.dailyCostLimit > 0" class="mt-4">
|
||||||
|
<div class="flex items-center justify-between text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
<span>每日费用限制</span>
|
||||||
|
<span>
|
||||||
|
${{ (apiKey.dailyCost || 0).toFixed(4) }} / ${{
|
||||||
|
(apiKey.dailyCostLimit || 0).toFixed(2)
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 h-3 overflow-hidden rounded-full bg-gray-200 dark:bg-gray-700">
|
||||||
|
<div
|
||||||
|
class="h-full rounded-full bg-gradient-to-r from-green-500 to-red-500 transition-all duration-500"
|
||||||
|
:style="{
|
||||||
|
width: `${Math.min(calculateCostUsagePercentage(apiKey.dailyCost || 0, apiKey.dailyCostLimit || 0), 100)}%`
|
||||||
|
}"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 查看详细统计按钮 -->
|
||||||
|
<div class="mt-4">
|
||||||
|
<button
|
||||||
|
class="flex w-full items-center justify-center gap-2 rounded-lg bg-gradient-to-r from-indigo-500 to-purple-600 px-4 py-2.5 text-sm font-medium text-white transition-all duration-200 hover:from-indigo-600 hover:to-purple-700 hover:shadow-lg"
|
||||||
|
@click="showUsageDetails(apiKey)"
|
||||||
|
>
|
||||||
|
<i class="fas fa-chart-line" />
|
||||||
|
查看详细统计
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -210,11 +259,27 @@
|
|||||||
>
|
>
|
||||||
<i class="fas fa-check-circle mr-2" />{{ successMessage }}
|
<i class="fas fa-check-circle mr-2" />{{ successMessage }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 使用详情模态框 -->
|
||||||
|
<UsageDetailModal
|
||||||
|
:api-key="selectedApiKeyForDetail || {}"
|
||||||
|
:show="showUsageDetailModal"
|
||||||
|
@close="showUsageDetailModal = false"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 新API Key模态框 -->
|
||||||
|
<NewApiKeyModal
|
||||||
|
v-if="showNewApiKeyModal"
|
||||||
|
:api-key="newApiKeyData"
|
||||||
|
@close="showNewApiKeyModal = false"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, reactive } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
|
import UsageDetailModal from '@/components/apikeys/UsageDetailModal.vue'
|
||||||
|
import NewApiKeyModal from '@/components/apikeys/NewApiKeyModal.vue'
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
userInfo: {
|
userInfo: {
|
||||||
@@ -229,11 +294,17 @@ const createLoading = ref(false)
|
|||||||
const error = ref('')
|
const error = ref('')
|
||||||
const successMessage = ref('')
|
const successMessage = ref('')
|
||||||
const apiKeys = ref([])
|
const apiKeys = ref([])
|
||||||
const showApiKey = reactive({})
|
|
||||||
|
// 使用详情模态框相关
|
||||||
|
const showUsageDetailModal = ref(false)
|
||||||
|
const selectedApiKeyForDetail = ref(null)
|
||||||
|
|
||||||
|
// 新API Key模态框相关
|
||||||
|
const showNewApiKeyModal = ref(false)
|
||||||
|
const newApiKeyData = ref(null)
|
||||||
|
|
||||||
const newKeyForm = ref({
|
const newKeyForm = ref({
|
||||||
name: '',
|
limit: 0
|
||||||
limit: 100000
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 获取用户的 API Keys
|
// 获取用户的 API Keys
|
||||||
@@ -278,21 +349,23 @@ const createApiKey = async () => {
|
|||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
name: newKeyForm.value.name || 'AD用户密钥',
|
limit: newKeyForm.value.limit || 0
|
||||||
limit: newKeyForm.value.limit || 100000
|
// name字段由后端根据用户displayName自动生成
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const result = await response.json()
|
const result = await response.json()
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
successMessage.value = 'API Key 创建成功'
|
// 显示新API Key模态框
|
||||||
apiKeys.value = [result.apiKey]
|
newApiKeyData.value = result.apiKey
|
||||||
newKeyForm.value = { name: '', limit: 100000 }
|
showNewApiKeyModal.value = true
|
||||||
|
|
||||||
// 3秒后清除成功消息
|
// 更新API Keys列表
|
||||||
setTimeout(() => {
|
apiKeys.value = [result.apiKey]
|
||||||
successMessage.value = ''
|
newKeyForm.value = { limit: 0 }
|
||||||
}, 3000)
|
|
||||||
|
// 清除错误信息
|
||||||
|
error.value = ''
|
||||||
} else {
|
} else {
|
||||||
error.value = result.message || 'API Key 创建失败'
|
error.value = result.message || 'API Key 创建失败'
|
||||||
}
|
}
|
||||||
@@ -304,37 +377,98 @@ const createApiKey = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 切换 API Key 显示/隐藏
|
// 计算Token使用百分比
|
||||||
const toggleApiKeyVisibility = (keyId) => {
|
const calculateTokenUsagePercentage = (used, limit) => {
|
||||||
showApiKey[keyId] = !showApiKey[keyId]
|
if (!limit || limit === 0) return 0
|
||||||
|
return Math.round((used / limit) * 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 复制 API Key
|
// 计算费用使用百分比
|
||||||
const copyApiKey = async (apiKey) => {
|
const calculateCostUsagePercentage = (used, limit) => {
|
||||||
try {
|
if (!limit || limit === 0) return 0
|
||||||
await navigator.clipboard.writeText(apiKey)
|
return Math.round((used / limit) * 100)
|
||||||
successMessage.value = 'API Key 已复制到剪贴板'
|
}
|
||||||
setTimeout(() => {
|
|
||||||
successMessage.value = ''
|
// 编辑API Key(简化版,只允许修改名称和描述)
|
||||||
}, 2000)
|
const editApiKey = (apiKey) => {
|
||||||
} catch (err) {
|
const newName = prompt('请输入新的API Key名称:', apiKey.name)
|
||||||
console.error('复制失败:', err)
|
if (newName !== null && newName.trim() !== '') {
|
||||||
error.value = '复制失败,请手动选择复制'
|
updateApiKey(apiKey.id, { name: newName.trim() })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 遮盖 API Key
|
// 切换API Key状态
|
||||||
const maskApiKey = (key) => {
|
const toggleApiKeyStatus = async (apiKey) => {
|
||||||
if (!key) return ''
|
const action = apiKey.isActive ? '禁用' : '激活'
|
||||||
const start = key.substring(0, 8)
|
if (confirm(`确定要${action}这个API Key吗?`)) {
|
||||||
const end = key.substring(key.length - 8)
|
await updateApiKey(apiKey.id, { isActive: !apiKey.isActive })
|
||||||
return `${start}${'*'.repeat(20)}${end}`
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算使用百分比
|
// 删除API Key
|
||||||
const calculateUsagePercentage = (used, limit) => {
|
const deleteApiKey = async (apiKey) => {
|
||||||
if (!limit || limit === 0) return 0
|
if (confirm(`确定要删除API Key "${apiKey.name}" 吗?删除后将无法恢复!`)) {
|
||||||
return Math.round((used / limit) * 100)
|
try {
|
||||||
|
const token = localStorage.getItem('user_token')
|
||||||
|
const response = await fetch(`/admin/ldap/user/api-keys/${apiKey.id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await response.json()
|
||||||
|
if (result.success) {
|
||||||
|
successMessage.value = 'API Key 删除成功'
|
||||||
|
// 从本地数组中移除
|
||||||
|
const index = apiKeys.value.findIndex((k) => k.id === apiKey.id)
|
||||||
|
if (index > -1) {
|
||||||
|
apiKeys.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
successMessage.value = ''
|
||||||
|
}, 3000)
|
||||||
|
} else {
|
||||||
|
error.value = result.message || 'API Key 删除失败'
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('删除 API Key 错误:', err)
|
||||||
|
error.value = '网络错误,请重试'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新API Key
|
||||||
|
const updateApiKey = async (keyId, updates) => {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('user_token')
|
||||||
|
const response = await fetch(`/admin/ldap/user/api-keys/${keyId}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(updates)
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await response.json()
|
||||||
|
if (result.success) {
|
||||||
|
successMessage.value = 'API Key 更新成功'
|
||||||
|
// 更新本地数据
|
||||||
|
const apiKey = apiKeys.value.find((k) => k.id === keyId)
|
||||||
|
if (apiKey) {
|
||||||
|
Object.assign(apiKey, updates)
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
successMessage.value = ''
|
||||||
|
}, 3000)
|
||||||
|
} else {
|
||||||
|
error.value = result.message || 'API Key 更新失败'
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('更新 API Key 错误:', err)
|
||||||
|
error.value = '网络错误,请重试'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 格式化日期
|
// 格式化日期
|
||||||
@@ -350,6 +484,12 @@ const formatDate = (dateString) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 显示使用详情
|
||||||
|
const showUsageDetails = (apiKey) => {
|
||||||
|
selectedApiKeyForDetail.value = apiKey
|
||||||
|
showUsageDetailModal.value = true
|
||||||
|
}
|
||||||
|
|
||||||
// 初始化
|
// 初始化
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchApiKeys()
|
fetchApiKeys()
|
||||||
|
|||||||
@@ -51,14 +51,7 @@
|
|||||||
@click="currentTab = 'api-keys'"
|
@click="currentTab = 'api-keys'"
|
||||||
>
|
>
|
||||||
<i class="fas fa-key mr-1 md:mr-2" />
|
<i class="fas fa-key mr-1 md:mr-2" />
|
||||||
<span class="text-sm md:text-base">API Keys</span>
|
<span class="text-sm md:text-base">API Keys 管理</span>
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
:class="['tab-pill-button', currentTab === 'dashboard' ? 'active' : '']"
|
|
||||||
@click="currentTab = 'dashboard'"
|
|
||||||
>
|
|
||||||
<i class="fas fa-chart-line mr-1 md:mr-2" />
|
|
||||||
<span class="text-sm md:text-base">使用统计</span>
|
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
:class="['tab-pill-button', currentTab === 'tutorial' ? 'active' : '']"
|
:class="['tab-pill-button', currentTab === 'tutorial' ? 'active' : '']"
|
||||||
@@ -76,11 +69,6 @@
|
|||||||
<UserApiKeysView :user-info="userInfo" />
|
<UserApiKeysView :user-info="userInfo" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 使用统计 -->
|
|
||||||
<div v-if="currentTab === 'dashboard'" class="tab-content">
|
|
||||||
<UserStatsView :user-info="userInfo" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 使用教程 -->
|
<!-- 使用教程 -->
|
||||||
<div v-if="currentTab === 'tutorial'" class="tab-content">
|
<div v-if="currentTab === 'tutorial'" class="tab-content">
|
||||||
<div class="glass-strong rounded-3xl shadow-xl">
|
<div class="glass-strong rounded-3xl shadow-xl">
|
||||||
@@ -97,7 +85,6 @@ import LogoTitle from '@/components/common/LogoTitle.vue'
|
|||||||
import ThemeToggle from '@/components/common/ThemeToggle.vue'
|
import ThemeToggle from '@/components/common/ThemeToggle.vue'
|
||||||
import TutorialView from './TutorialView.vue'
|
import TutorialView from './TutorialView.vue'
|
||||||
import UserApiKeysView from '@/components/user/UserApiKeysView.vue'
|
import UserApiKeysView from '@/components/user/UserApiKeysView.vue'
|
||||||
import UserStatsView from '@/components/user/UserStatsView.vue'
|
|
||||||
const themeStore = useThemeStore()
|
const themeStore = useThemeStore()
|
||||||
|
|
||||||
// 当前标签页
|
// 当前标签页
|
||||||
|
|||||||
Reference in New Issue
Block a user