Files
claude-relay-service/src/routes/admin/system.js
2025-11-27 22:16:45 +08:00

402 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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