mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 09:38:02 +00:00
refacto: 重构admin.js
This commit is contained in:
401
src/routes/admin/system.js
Normal file
401
src/routes/admin/system.js
Normal file
@@ -0,0 +1,401 @@
|
||||
const express = require('express')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const axios = require('axios')
|
||||
const claudeCodeHeadersService = require('../../services/claudeCodeHeadersService')
|
||||
const claudeAccountService = require('../../services/claudeAccountService')
|
||||
const redis = require('../../models/redis')
|
||||
const { authenticateAdmin } = require('../../middleware/auth')
|
||||
const logger = require('../../utils/logger')
|
||||
const config = require('../../../config/config')
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
// ==================== Claude Code Headers 管理 ====================
|
||||
|
||||
// 获取所有 Claude Code headers
|
||||
router.get('/claude-code-headers', authenticateAdmin, async (req, res) => {
|
||||
try {
|
||||
const allHeaders = await claudeCodeHeadersService.getAllAccountHeaders()
|
||||
|
||||
// 获取所有 Claude 账号信息
|
||||
const accounts = await claudeAccountService.getAllAccounts()
|
||||
const accountMap = {}
|
||||
accounts.forEach((account) => {
|
||||
accountMap[account.id] = account.name
|
||||
})
|
||||
|
||||
// 格式化输出
|
||||
const formattedData = Object.entries(allHeaders).map(([accountId, data]) => ({
|
||||
accountId,
|
||||
accountName: accountMap[accountId] || 'Unknown',
|
||||
version: data.version,
|
||||
userAgent: data.headers['user-agent'],
|
||||
updatedAt: data.updatedAt,
|
||||
headers: data.headers
|
||||
}))
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data: formattedData
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('❌ Failed to get Claude Code headers:', error)
|
||||
return res
|
||||
.status(500)
|
||||
.json({ error: 'Failed to get Claude Code headers', message: error.message })
|
||||
}
|
||||
})
|
||||
|
||||
// 🗑️ 清除指定账号的 Claude Code headers
|
||||
router.delete('/claude-code-headers/:accountId', authenticateAdmin, async (req, res) => {
|
||||
try {
|
||||
const { accountId } = req.params
|
||||
await claudeCodeHeadersService.clearAccountHeaders(accountId)
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
message: `Claude Code headers cleared for account ${accountId}`
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('❌ Failed to clear Claude Code headers:', error)
|
||||
return res
|
||||
.status(500)
|
||||
.json({ error: 'Failed to clear Claude Code headers', message: error.message })
|
||||
}
|
||||
})
|
||||
|
||||
// ==================== 系统更新检查 ====================
|
||||
|
||||
// 版本比较函数
|
||||
function compareVersions(current, latest) {
|
||||
const parseVersion = (v) => {
|
||||
const parts = v.split('.').map(Number)
|
||||
return {
|
||||
major: parts[0] || 0,
|
||||
minor: parts[1] || 0,
|
||||
patch: parts[2] || 0
|
||||
}
|
||||
}
|
||||
|
||||
const currentV = parseVersion(current)
|
||||
const latestV = parseVersion(latest)
|
||||
|
||||
if (currentV.major !== latestV.major) {
|
||||
return currentV.major - latestV.major
|
||||
}
|
||||
if (currentV.minor !== latestV.minor) {
|
||||
return currentV.minor - latestV.minor
|
||||
}
|
||||
return currentV.patch - latestV.patch
|
||||
}
|
||||
|
||||
router.get('/check-updates', authenticateAdmin, async (req, res) => {
|
||||
// 读取当前版本
|
||||
const versionPath = path.join(__dirname, '../../../VERSION')
|
||||
let currentVersion = '1.0.0'
|
||||
try {
|
||||
currentVersion = fs.readFileSync(versionPath, 'utf8').trim()
|
||||
} catch (err) {
|
||||
logger.warn('⚠️ Could not read VERSION file:', err.message)
|
||||
}
|
||||
|
||||
try {
|
||||
// 从缓存获取
|
||||
const cacheKey = 'version_check_cache'
|
||||
const cached = await redis.getClient().get(cacheKey)
|
||||
|
||||
if (cached && !req.query.force) {
|
||||
const cachedData = JSON.parse(cached)
|
||||
const cacheAge = Date.now() - cachedData.timestamp
|
||||
|
||||
// 缓存有效期1小时
|
||||
if (cacheAge < 3600000) {
|
||||
// 实时计算 hasUpdate,不使用缓存的值
|
||||
const hasUpdate = compareVersions(currentVersion, cachedData.latest) < 0
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data: {
|
||||
current: currentVersion,
|
||||
latest: cachedData.latest,
|
||||
hasUpdate, // 实时计算,不用缓存
|
||||
releaseInfo: cachedData.releaseInfo,
|
||||
cached: true
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 请求 GitHub API
|
||||
const githubRepo = 'wei-shaw/claude-relay-service'
|
||||
const response = await axios.get(`https://api.github.com/repos/${githubRepo}/releases/latest`, {
|
||||
headers: {
|
||||
Accept: 'application/vnd.github.v3+json',
|
||||
'User-Agent': 'Claude-Relay-Service'
|
||||
},
|
||||
timeout: 10000
|
||||
})
|
||||
|
||||
const release = response.data
|
||||
const latestVersion = release.tag_name.replace(/^v/, '')
|
||||
|
||||
// 比较版本
|
||||
const hasUpdate = compareVersions(currentVersion, latestVersion) < 0
|
||||
|
||||
const releaseInfo = {
|
||||
name: release.name,
|
||||
body: release.body,
|
||||
publishedAt: release.published_at,
|
||||
htmlUrl: release.html_url
|
||||
}
|
||||
|
||||
// 缓存结果(不缓存 hasUpdate,因为它应该实时计算)
|
||||
await redis.getClient().set(
|
||||
cacheKey,
|
||||
JSON.stringify({
|
||||
latest: latestVersion,
|
||||
releaseInfo,
|
||||
timestamp: Date.now()
|
||||
}),
|
||||
'EX',
|
||||
3600
|
||||
) // 1小时过期
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data: {
|
||||
current: currentVersion,
|
||||
latest: latestVersion,
|
||||
hasUpdate,
|
||||
releaseInfo,
|
||||
cached: false
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
// 改进错误日志记录
|
||||
const errorDetails = {
|
||||
message: error.message || 'Unknown error',
|
||||
code: error.code,
|
||||
response: error.response
|
||||
? {
|
||||
status: error.response.status,
|
||||
statusText: error.response.statusText,
|
||||
data: error.response.data
|
||||
}
|
||||
: null,
|
||||
request: error.request ? 'Request was made but no response received' : null
|
||||
}
|
||||
|
||||
logger.error('❌ Failed to check for updates:', errorDetails.message)
|
||||
|
||||
// 处理 404 错误 - 仓库或版本不存在
|
||||
if (error.response && error.response.status === 404) {
|
||||
return res.json({
|
||||
success: true,
|
||||
data: {
|
||||
current: currentVersion,
|
||||
latest: currentVersion,
|
||||
hasUpdate: false,
|
||||
releaseInfo: {
|
||||
name: 'No releases found',
|
||||
body: 'The GitHub repository has no releases yet.',
|
||||
publishedAt: new Date().toISOString(),
|
||||
htmlUrl: '#'
|
||||
},
|
||||
warning: 'GitHub repository has no releases'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 如果是网络错误,尝试返回缓存的数据
|
||||
if (error.code === 'ECONNREFUSED' || error.code === 'ETIMEDOUT' || error.code === 'ENOTFOUND') {
|
||||
const cacheKey = 'version_check_cache'
|
||||
const cached = await redis.getClient().get(cacheKey)
|
||||
|
||||
if (cached) {
|
||||
const cachedData = JSON.parse(cached)
|
||||
// 实时计算 hasUpdate
|
||||
const hasUpdate = compareVersions(currentVersion, cachedData.latest) < 0
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data: {
|
||||
current: currentVersion,
|
||||
latest: cachedData.latest,
|
||||
hasUpdate, // 实时计算
|
||||
releaseInfo: cachedData.releaseInfo,
|
||||
cached: true,
|
||||
warning: 'Using cached data due to network error'
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 其他错误返回当前版本信息
|
||||
return res.json({
|
||||
success: true,
|
||||
data: {
|
||||
current: currentVersion,
|
||||
latest: currentVersion,
|
||||
hasUpdate: false,
|
||||
releaseInfo: {
|
||||
name: 'Update check failed',
|
||||
body: `Unable to check for updates: ${error.message || 'Unknown error'}`,
|
||||
publishedAt: new Date().toISOString(),
|
||||
htmlUrl: '#'
|
||||
},
|
||||
error: true,
|
||||
warning: error.message || 'Failed to check for updates'
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// ==================== OEM 设置管理 ====================
|
||||
|
||||
// 获取OEM设置(公开接口,用于显示)
|
||||
// 注意:这个端点没有 authenticateAdmin 中间件,因为前端登录页也需要访问
|
||||
router.get('/oem-settings', async (req, res) => {
|
||||
try {
|
||||
const client = redis.getClient()
|
||||
const oemSettings = await client.get('oem:settings')
|
||||
|
||||
// 默认设置
|
||||
const defaultSettings = {
|
||||
siteName: 'Claude Relay Service',
|
||||
siteIcon: '',
|
||||
siteIconData: '', // Base64编码的图标数据
|
||||
showAdminButton: true, // 是否显示管理后台按钮
|
||||
updatedAt: new Date().toISOString()
|
||||
}
|
||||
|
||||
let settings = defaultSettings
|
||||
if (oemSettings) {
|
||||
try {
|
||||
settings = { ...defaultSettings, ...JSON.parse(oemSettings) }
|
||||
} catch (err) {
|
||||
logger.warn('⚠️ Failed to parse OEM settings, using defaults:', err.message)
|
||||
}
|
||||
}
|
||||
|
||||
// 添加 LDAP 启用状态到响应中
|
||||
return res.json({
|
||||
success: true,
|
||||
data: {
|
||||
...settings,
|
||||
ldapEnabled: config.ldap && config.ldap.enabled === true
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('❌ Failed to get OEM settings:', error)
|
||||
return res.status(500).json({ error: 'Failed to get OEM settings', message: error.message })
|
||||
}
|
||||
})
|
||||
|
||||
// 更新OEM设置
|
||||
router.put('/oem-settings', authenticateAdmin, async (req, res) => {
|
||||
try {
|
||||
const { siteName, siteIcon, siteIconData, showAdminButton } = req.body
|
||||
|
||||
// 验证输入
|
||||
if (!siteName || typeof siteName !== 'string' || siteName.trim().length === 0) {
|
||||
return res.status(400).json({ error: 'Site name is required' })
|
||||
}
|
||||
|
||||
if (siteName.length > 100) {
|
||||
return res.status(400).json({ error: 'Site name must be less than 100 characters' })
|
||||
}
|
||||
|
||||
// 验证图标数据大小(如果是base64)
|
||||
if (siteIconData && siteIconData.length > 500000) {
|
||||
// 约375KB
|
||||
return res.status(400).json({ error: 'Icon file must be less than 350KB' })
|
||||
}
|
||||
|
||||
// 验证图标URL(如果提供)
|
||||
if (siteIcon && !siteIconData) {
|
||||
// 简单验证URL格式
|
||||
try {
|
||||
new URL(siteIcon)
|
||||
} catch (err) {
|
||||
return res.status(400).json({ error: 'Invalid icon URL format' })
|
||||
}
|
||||
}
|
||||
|
||||
const settings = {
|
||||
siteName: siteName.trim(),
|
||||
siteIcon: (siteIcon || '').trim(),
|
||||
siteIconData: (siteIconData || '').trim(), // Base64数据
|
||||
showAdminButton: showAdminButton !== false, // 默认为true
|
||||
updatedAt: new Date().toISOString()
|
||||
}
|
||||
|
||||
const client = redis.getClient()
|
||||
await client.set('oem:settings', JSON.stringify(settings))
|
||||
|
||||
logger.info(`✅ OEM settings updated: ${siteName}`)
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
message: 'OEM settings updated successfully',
|
||||
data: settings
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('❌ Failed to update OEM settings:', error)
|
||||
return res.status(500).json({ error: 'Failed to update OEM settings', message: error.message })
|
||||
}
|
||||
})
|
||||
|
||||
// ==================== Claude Code 版本管理 ====================
|
||||
|
||||
router.get('/claude-code-version', authenticateAdmin, async (req, res) => {
|
||||
try {
|
||||
const CACHE_KEY = 'claude_code_user_agent:daily'
|
||||
|
||||
// 获取缓存的统一User-Agent
|
||||
const unifiedUserAgent = await redis.client.get(CACHE_KEY)
|
||||
const ttl = unifiedUserAgent ? await redis.client.ttl(CACHE_KEY) : 0
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
userAgent: unifiedUserAgent,
|
||||
isActive: !!unifiedUserAgent,
|
||||
ttlSeconds: ttl,
|
||||
lastUpdated: unifiedUserAgent ? new Date().toISOString() : null
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('❌ Get unified Claude Code User-Agent error:', error)
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to get User-Agent information',
|
||||
error: error.message
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 🗑️ 清除统一Claude Code User-Agent缓存
|
||||
router.post('/claude-code-version/clear', authenticateAdmin, async (req, res) => {
|
||||
try {
|
||||
const CACHE_KEY = 'claude_code_user_agent:daily'
|
||||
|
||||
// 删除缓存的统一User-Agent
|
||||
await redis.client.del(CACHE_KEY)
|
||||
|
||||
logger.info(`🗑️ Admin manually cleared unified Claude Code User-Agent cache`)
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Unified User-Agent cache cleared successfully'
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('❌ Clear unified User-Agent cache error:', error)
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to clear cache',
|
||||
error: error.message
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
Reference in New Issue
Block a user