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, // 是否显示管理后台按钮 apiStatsNotice: { enabled: false, title: '', content: '' }, 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, apiStatsNotice } = 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 apiStatsNotice: { enabled: apiStatsNotice?.enabled === true, title: (apiStatsNotice?.title || '').trim().slice(0, 100), content: (apiStatsNotice?.content || '').trim().slice(0, 2000) }, 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