From 1e61e7a31d5d07f78ca0dec1eae51a45ab830729 Mon Sep 17 00:00:00 2001 From: VanZheng Date: Wed, 6 Aug 2025 00:45:28 +0800 Subject: [PATCH] feat: add comprehensive Makefile for project management (#1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 修复timeRange=7days时的数据加载和显示问题 ## 修复内容 ### 后端修复 (src/routes/admin.js) - 添加 /admin/usage-costs 接口对 period=7days 的支持 - 实现7天时间范围的成本统计,汇总最近7天的daily数据 - 修复时区处理不一致导致的数据过滤错误 ### 前端修复 (web/admin-spa/src/stores/dashboard.js) - 修改 loadDashboardData() 支持动态 timeRange 参数 - 根据时间范围动态调整 usage-costs 查询参数 - 消除硬编码的 period=today 和 period=all ### 前端修复 (web/admin-spa/src/views/ApiKeysView.vue) - 修正API Key详情统计的period计算逻辑 - 7days时间范围现在正确传递 period=daily 而非 monthly - 确保列表数据和详情统计使用一致的时间范围 ## 解决的问题 - 选择"最近7天"时数据显示不准确或缺失 - API Key详情展开时period参数错误 - 成本统计不跟随时间范围选择变化 - 时区计算不一致导致的边界问题 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * feat: add comprehensive Makefile for project management - Add common development commands (install, setup, dev, start) - Add frontend build commands (build-web, build-all) - Add service management with daemon support - Add Docker deployment commands - Add CLI management tools shortcuts - Add maintenance and monitoring commands - Include Chinese descriptions for better UX 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --------- Co-authored-by: Claude --- Makefile | 259 ++++++++++++++++++++++++ src/routes/admin.js | 91 ++++++++- web/admin-spa/src/stores/dashboard.js | 19 +- web/admin-spa/src/views/ApiKeysView.vue | 2 +- 4 files changed, 366 insertions(+), 5 deletions(-) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..22a701a5 --- /dev/null +++ b/Makefile @@ -0,0 +1,259 @@ +# Claude Relay Service Makefile +# 功能完整的 AI API 中转服务,支持 Claude 和 Gemini 双平台 + +.PHONY: help install setup dev start test lint clean docker-up docker-down service-start service-stop service-status logs cli-admin cli-keys cli-accounts cli-status + +# 默认目标:显示帮助信息 +help: + @echo "Claude Relay Service - AI API 中转服务" + @echo "" + @echo "可用命令:" + @echo "" + @echo " 📦 安装和初始化:" + @echo " install - 安装项目依赖" + @echo " install-web - 安装Web界面依赖" + @echo " setup - 生成配置文件和管理员凭据" + @echo " clean - 清理依赖和构建文件" + @echo "" + @echo " 🎨 前端构建:" + @echo " build-web - 构建 Web 管理界面" + @echo " build-all - 构建完整项目(后端+前端)" + @echo "" + @echo " 🚀 开发和运行:" + @echo " dev - 开发模式运行(热重载)" + @echo " start - 生产模式运行" + @echo " test - 运行测试套件" + @echo " lint - 代码风格检查" + @echo "" + @echo " 🐳 Docker 部署:" + @echo " docker-up - 启动 Docker 服务" + @echo " docker-up-full - 启动 Docker 服务(包含监控)" + @echo " docker-down - 停止 Docker 服务" + @echo " docker-logs - 查看 Docker 日志" + @echo "" + @echo " 🔧 服务管理:" + @echo " service-start - 前台启动服务" + @echo " service-daemon - 后台启动服务(守护进程)" + @echo " service-stop - 停止服务" + @echo " service-restart - 重启服务" + @echo " service-restart-daemon - 重启服务(守护进程)" + @echo " service-status - 查看服务状态" + @echo " logs - 查看应用日志" + @echo " logs-follow - 实时查看日志" + @echo "" + @echo " ⚙️ CLI 管理工具:" + @echo " cli-admin - 管理员操作" + @echo " cli-keys - API Key 管理" + @echo " cli-accounts - Claude 账户管理" + @echo " cli-status - 系统状态查看" + @echo "" + @echo " 💡 快速开始:" + @echo " make setup && make dev" + @echo "" + +# 安装和初始化 +install: + @echo "📦 安装项目依赖..." + npm install + +install-web: + @echo "📦 安装 Web 界面依赖..." + npm run install:web + +# 前端构建 +build-web: + @echo "🎨 构建 Web 管理界面..." + npm run build:web + +build-all: install install-web build-web + @echo "🎉 完整项目构建完成!" + +setup: + @echo "⚙️ 初始化项目配置和管理员凭据..." + @if [ ! -f config/config.js ]; then cp config/config.example.js config/config.js; fi + @if [ ! -f .env ]; then cp .env.example .env; fi + npm run setup + +clean: + @echo "🧹 清理依赖和构建文件..." + rm -rf node_modules + rm -rf web/node_modules + rm -rf web/admin-spa/dist + rm -rf web/admin-spa/node_modules + rm -rf logs/*.log + +# 开发和运行 +dev: + @echo "🚀 启动开发模式(热重载)..." + npm run dev + +start: + @echo "🚀 启动生产模式..." + npm start + +test: + @echo "🧪 运行测试套件..." + npm test + +lint: + @echo "🔍 执行代码风格检查..." + npm run lint + +# Docker 部署 +docker-up: + @echo "🐳 启动 Docker 服务..." + docker-compose up -d + +docker-up-full: + @echo "🐳 启动 Docker 服务(包含监控)..." + docker-compose --profile monitoring up -d + +docker-down: + @echo "🛑 停止 Docker 服务..." + docker-compose down + +docker-logs: + @echo "📋 查看 Docker 服务日志..." + docker-compose logs -f + +# 服务管理 +service-start: + @echo "🚀 前台启动服务..." + npm run service:start + +service-daemon: + @echo "🔧 后台启动服务(守护进程)..." + npm run service:start:daemon + +service-stop: + @echo "🛑 停止服务..." + npm run service:stop + +service-restart: + @echo "🔄 重启服务..." + npm run service:restart + +service-restart-daemon: + @echo "🔄 重启服务(守护进程)..." + npm run service:restart:daemon + +service-status: + @echo "📊 查看服务状态..." + npm run service:status + +logs: + @echo "📋 查看应用日志..." + npm run service:logs + +logs-follow: + @echo "📋 实时查看日志..." + npm run service:logs:follow + +# CLI 管理工具 +cli-admin: + @echo "👤 启动管理员操作 CLI..." + npm run cli admin + +cli-keys: + @echo "🔑 启动 API Key 管理 CLI..." + npm run cli keys + +cli-accounts: + @echo "👥 启动 Claude 账户管理 CLI..." + npm run cli accounts + +cli-status: + @echo "📊 查看系统状态..." + npm run cli status + +# 开发辅助命令 +check-config: + @echo "🔍 检查配置文件..." + @if [ ! -f config/config.js ]; then echo "❌ config/config.js 不存在,请运行 'make setup'"; exit 1; fi + @if [ ! -f .env ]; then echo "❌ .env 不存在,请运行 'make setup'"; exit 1; fi + @echo "✅ 配置文件检查通过" + +health-check: + @echo "🏥 执行健康检查..." + @curl -s http://localhost:3000/health || echo "❌ 服务未运行或不可访问" + +# 快速启动组合命令 +quick-start: setup dev + +quick-daemon: setup service-daemon + @echo "🎉 服务已在后台启动!" + @echo "运行 'make service-status' 查看状态" + @echo "运行 'make logs-follow' 查看实时日志" + +# 全栈开发环境 +dev-full: install install-web build-web setup dev + @echo "🚀 全栈开发环境启动!" + +# 完整部署流程 +deploy: clean install install-web build-web setup test lint docker-up + @echo "🎉 部署完成!" + @echo "访问 Web 管理界面: http://localhost:3000/web" + @echo "API 端点: http://localhost:3000/api/v1/messages" + +# 生产部署准备 +production-build: clean install install-web build-web + @echo "🚀 生产环境构建完成!" + +# 维护命令 +backup-redis: + @echo "💾 备份 Redis 数据..." + @docker exec claude-relay-service-redis-1 redis-cli BGSAVE || echo "❌ Redis 备份失败" + +restore-redis: + @echo "♻️ 恢复 Redis 数据..." + @echo "请手动恢复 Redis 数据文件" + +# 监控和日志 +monitor: + @echo "📊 启动监控面板..." + @echo "Grafana: http://localhost:3001" + @echo "Redis Commander: http://localhost:8081" + +tail-logs: + @echo "📋 实时查看日志..." + tail -f logs/claude-relay-*.log + +# 开发工具 +format: + @echo "🎨 格式化代码..." + npm run lint -- --fix + +check-deps: + @echo "🔍 检查依赖更新..." + npm outdated + +update-deps: + @echo "⬆️ 更新依赖..." + npm update + +# 测试相关 +test-coverage: + @echo "📊 运行测试覆盖率..." + npm test -- --coverage + +test-watch: + @echo "👀 监视模式运行测试..." + npm test -- --watch + +# Git 相关 +git-status: + @echo "📋 Git 状态..." + git status --short + +git-pull: + @echo "⬇️ 拉取最新代码..." + git pull origin main + +# 安全检查 +security-audit: + @echo "🔒 执行安全审计..." + npm audit + +security-fix: + @echo "🔧 修复安全漏洞..." + npm audit fix \ No newline at end of file diff --git a/src/routes/admin.js b/src/routes/admin.js index 843b3c6b..ddebc163 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -2724,7 +2724,7 @@ router.get('/api-keys-usage-trend', authenticateAdmin, async (req, res) => { // 计算总体使用费用 router.get('/usage-costs', authenticateAdmin, async (req, res) => { try { - const { period = 'all' } = req.query; // all, today, monthly + const { period = 'all' } = req.query; // all, today, monthly, 7days logger.info(`💰 Calculating usage costs for period: ${period}`); @@ -2752,6 +2752,95 @@ router.get('/usage-costs', authenticateAdmin, async (req, res) => { pattern = `usage:model:daily:*:${today}`; } else if (period === 'monthly') { pattern = `usage:model:monthly:*:${currentMonth}`; + } else if (period === '7days') { + // 最近7天:汇总daily数据 + const modelUsageMap = new Map(); + + // 获取最近7天的所有daily统计数据 + for (let i = 0; i < 7; i++) { + const date = new Date(); + date.setDate(date.getDate() - i); + const tzDate = redis.getDateInTimezone(date); + const dateStr = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}-${String(tzDate.getUTCDate()).padStart(2, '0')}`; + const dayPattern = `usage:model:daily:*:${dateStr}`; + + const dayKeys = await client.keys(dayPattern); + + for (const key of dayKeys) { + const modelMatch = key.match(/usage:model:daily:(.+):\d{4}-\d{2}-\d{2}$/); + if (!modelMatch) continue; + + const model = modelMatch[1]; + const data = await client.hgetall(key); + + if (data && Object.keys(data).length > 0) { + if (!modelUsageMap.has(model)) { + modelUsageMap.set(model, { + inputTokens: 0, + outputTokens: 0, + cacheCreateTokens: 0, + cacheReadTokens: 0 + }); + } + + const modelUsage = modelUsageMap.get(model); + modelUsage.inputTokens += parseInt(data.inputTokens) || 0; + modelUsage.outputTokens += parseInt(data.outputTokens) || 0; + modelUsage.cacheCreateTokens += parseInt(data.cacheCreateTokens) || 0; + modelUsage.cacheReadTokens += parseInt(data.cacheReadTokens) || 0; + } + } + } + + // 计算7天统计的费用 + logger.info(`💰 Processing ${modelUsageMap.size} unique models for 7days cost calculation`); + + for (const [model, usage] of modelUsageMap) { + const usageData = { + input_tokens: usage.inputTokens, + output_tokens: usage.outputTokens, + cache_creation_input_tokens: usage.cacheCreateTokens, + cache_read_input_tokens: usage.cacheReadTokens + }; + + const costResult = CostCalculator.calculateCost(usageData, model); + totalCosts.inputCost += costResult.costs.input; + totalCosts.outputCost += costResult.costs.output; + totalCosts.cacheCreateCost += costResult.costs.cacheWrite; + totalCosts.cacheReadCost += costResult.costs.cacheRead; + totalCosts.totalCost += costResult.costs.total; + + logger.info(`💰 Model ${model} (7days): ${usage.inputTokens + usage.outputTokens + usage.cacheCreateTokens + usage.cacheReadTokens} tokens, cost: ${costResult.formatted.total}`); + + // 记录模型费用 + modelCosts[model] = { + model, + requests: 0, // 7天汇总数据没有请求数统计 + usage: usageData, + costs: costResult.costs, + formatted: costResult.formatted, + usingDynamicPricing: costResult.usingDynamicPricing + }; + } + + // 返回7天统计结果 + return res.json({ + success: true, + data: { + period, + totalCosts: { + ...totalCosts, + formatted: { + inputCost: CostCalculator.formatCost(totalCosts.inputCost), + outputCost: CostCalculator.formatCost(totalCosts.outputCost), + cacheCreateCost: CostCalculator.formatCost(totalCosts.cacheCreateCost), + cacheReadCost: CostCalculator.formatCost(totalCosts.cacheReadCost), + totalCost: CostCalculator.formatCost(totalCosts.totalCost) + } + }, + modelCosts: Object.values(modelCosts) + } + }); } else { // 全部时间,先尝试从Redis获取所有历史模型统计数据(只使用monthly数据避免重复计算) const allModelKeys = await client.keys('usage:model:monthly:*:*'); diff --git a/web/admin-spa/src/stores/dashboard.js b/web/admin-spa/src/stores/dashboard.js index 8e2f0ada..bbbce544 100644 --- a/web/admin-spa/src/stores/dashboard.js +++ b/web/admin-spa/src/stores/dashboard.js @@ -120,13 +120,26 @@ export const useDashboardStore = defineStore('dashboard', () => { } // 方法 - async function loadDashboardData() { + async function loadDashboardData(timeRange = null) { loading.value = true try { + // 根据timeRange动态设置costs查询参数 + let costsParams = { today: 'today', all: 'all' } + + if (timeRange) { + const periodMapping = { + 'today': { today: 'today', all: 'today' }, + '7days': { today: '7days', all: '7days' }, + 'monthly': { today: 'monthly', all: 'monthly' }, + 'all': { today: 'today', all: 'all' } + } + costsParams = periodMapping[timeRange] || costsParams + } + const [dashboardResponse, todayCostsResponse, totalCostsResponse] = await Promise.all([ apiClient.get('/admin/dashboard'), - apiClient.get('/admin/usage-costs?period=today'), - apiClient.get('/admin/usage-costs?period=all') + apiClient.get(`/admin/usage-costs?period=${costsParams.today}`), + apiClient.get(`/admin/usage-costs?period=${costsParams.all}`) ]) if (dashboardResponse.success) { diff --git a/web/admin-spa/src/views/ApiKeysView.vue b/web/admin-spa/src/views/ApiKeysView.vue index 2b4168b0..0a57e716 100644 --- a/web/admin-spa/src/views/ApiKeysView.vue +++ b/web/admin-spa/src/views/ApiKeysView.vue @@ -1306,7 +1306,7 @@ const loadApiKeyModelStats = async (keyId, forceReload = false) => { params.append('endDate', filter.customEnd) params.append('period', 'custom') } else { - const period = filter.preset === 'today' ? 'daily' : 'monthly' + const period = filter.preset === 'today' ? 'daily' : filter.preset === '7days' ? 'daily' : 'monthly' params.append('period', period) }