diff --git a/.gitignore b/.gitignore index 6ce0c09a..c8b75b27 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,6 @@ pnpm-debug.log* .env .env.* !.env.example -!.env.production # Claude specific directories .claude/ diff --git a/package.json b/package.json index 3323f475..48d7c604 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,9 @@ "service:restart:d": "node scripts/manage.js restart -d", "service:status": "node scripts/manage.js status", "service:logs": "node scripts/manage.js logs", + "monitor": "bash scripts/monitor-enhanced.sh", + "status": "bash scripts/status-unified.sh", + "status:detail": "bash scripts/status-unified.sh --detail", "test": "jest", "lint": "eslint src/**/*.js cli/**/*.js scripts/**/*.js --fix", "lint:check": "eslint src/**/*.js cli/**/*.js scripts/**/*.js", diff --git a/scripts/monitor-enhanced.sh b/scripts/monitor-enhanced.sh new file mode 100755 index 00000000..fc7c8c40 --- /dev/null +++ b/scripts/monitor-enhanced.sh @@ -0,0 +1,273 @@ +#!/bin/bash + +# Claude Relay Service - 增强版实时监控脚本 +# 结合并发监控和系统状态的完整监控方案 + +# 加载环境变量 +if [ -f .env ]; then + export $(grep -v '^#' .env | xargs) +fi + +echo "🔍 Claude Relay Service - 增强版实时监控" +echo "按 Ctrl+C 退出 | 按 's' 切换详细/简单模式" +echo "========================================" + +# 获取服务配置 +SERVICE_HOST=${HOST:-127.0.0.1} +SERVICE_PORT=${PORT:-3000} + +# 如果HOST是0.0.0.0,客户端应该连接localhost +if [ "$SERVICE_HOST" = "0.0.0.0" ]; then + SERVICE_HOST="127.0.0.1" +fi + +SERVICE_URL="http://${SERVICE_HOST}:${SERVICE_PORT}" + +# 获取Redis配置 +REDIS_HOST=${REDIS_HOST:-127.0.0.1} +REDIS_PORT=${REDIS_PORT:-6379} +REDIS_CMD="redis-cli -h $REDIS_HOST -p $REDIS_PORT" + +if [ ! -z "$REDIS_PASSWORD" ]; then + REDIS_CMD="redis-cli -h $REDIS_HOST -p $REDIS_PORT -a $REDIS_PASSWORD" +fi + +# 检查Redis连接 +if ! $REDIS_CMD ping > /dev/null 2>&1; then + echo "❌ Redis连接失败,请检查Redis服务是否运行" + echo " 配置: $REDIS_HOST:$REDIS_PORT" + exit 1 +fi + +# 显示模式: simple(简单) / detailed(详细) +DISPLAY_MODE="simple" + +# 获取API Key详细信息 +get_api_key_info() { + local api_key_id=$1 + local api_key_name=$($REDIS_CMD hget "apikey:$api_key_id" name 2>/dev/null) + local concurrency_limit=$($REDIS_CMD hget "apikey:$api_key_id" concurrencyLimit 2>/dev/null) + local token_limit=$($REDIS_CMD hget "apikey:$api_key_id" tokenLimit 2>/dev/null) + local created_at=$($REDIS_CMD hget "apikey:$api_key_id" createdAt 2>/dev/null) + + if [ -z "$api_key_name" ]; then + api_key_name="Unknown" + fi + + if [ -z "$concurrency_limit" ] || [ "$concurrency_limit" = "0" ]; then + concurrency_limit="无限制" + fi + + if [ -z "$token_limit" ] || [ "$token_limit" = "0" ]; then + token_limit="无限制" + else + token_limit=$(printf "%'d" $token_limit) + fi + + echo "$api_key_name|$concurrency_limit|$token_limit|$created_at" +} + +# 获取使用统计信息 +get_usage_stats() { + local api_key_id=$1 + local today=$(date '+%Y-%m-%d') + local current_month=$(date '+%Y-%m') + + # 获取总体使用量 + local total_requests=$($REDIS_CMD hget "usage:$api_key_id" totalRequests 2>/dev/null) + local total_tokens=$($REDIS_CMD hget "usage:$api_key_id" totalTokens 2>/dev/null) + + # 获取今日使用量 + local daily_requests=$($REDIS_CMD hget "usage:daily:$api_key_id:$today" requests 2>/dev/null) + local daily_tokens=$($REDIS_CMD hget "usage:daily:$api_key_id:$today" tokens 2>/dev/null) + + total_requests=${total_requests:-0} + total_tokens=${total_tokens:-0} + daily_requests=${daily_requests:-0} + daily_tokens=${daily_tokens:-0} + + echo "$total_requests|$total_tokens|$daily_requests|$daily_tokens" +} + +# 格式化数字 +format_number() { + local num=$1 + if [ "$num" -ge 1000000 ]; then + echo "$(echo "scale=1; $num / 1000000" | bc 2>/dev/null)M" + elif [ "$num" -ge 1000 ]; then + echo "$(echo "scale=1; $num / 1000" | bc 2>/dev/null)K" + else + echo "$num" + fi +} + +# 获取系统信息 +get_system_info() { + # Redis信息 + local redis_info=$($REDIS_CMD info server 2>/dev/null) + local redis_memory_info=$($REDIS_CMD info memory 2>/dev/null) + + local redis_version=$(echo "$redis_info" | grep redis_version | cut -d: -f2 | tr -d '\r' 2>/dev/null) + local redis_uptime=$(echo "$redis_info" | grep uptime_in_seconds | cut -d: -f2 | tr -d '\r' 2>/dev/null) + local used_memory=$(echo "$redis_memory_info" | grep used_memory_human | cut -d: -f2 | tr -d '\r' 2>/dev/null) + + local redis_uptime_hours=0 + if [ ! -z "$redis_uptime" ]; then + redis_uptime_hours=$((redis_uptime / 3600)) + fi + + # 服务状态 + local service_status="unknown" + local service_uptime="0" + if command -v curl > /dev/null 2>&1; then + local health_response=$(curl -s ${SERVICE_URL}/health 2>/dev/null) + if [ $? -eq 0 ]; then + service_status=$(echo "$health_response" | grep -o '"status":"[^"]*"' | cut -d'"' -f4 | head -1) + service_uptime=$(echo "$health_response" | grep -o '"uptime":[^,}]*' | cut -d: -f2 | head -1) + fi + fi + + local service_uptime_hours="0" + if [ ! -z "$service_uptime" ] && [ "$service_uptime" != "null" ]; then + service_uptime_hours=$(echo "scale=1; $service_uptime / 3600" | bc 2>/dev/null) + fi + + echo "$redis_version|$redis_uptime_hours|$used_memory|$service_status|$service_uptime_hours" +} + +# 主监控函数 +monitor_enhanced() { + while true; do + clear + echo "🔍 Claude Relay Service - 增强版实时监控 | $(date '+%Y-%m-%d %H:%M:%S')" + echo "模式: $DISPLAY_MODE | 服务: $SERVICE_URL | Redis: $REDIS_HOST:$REDIS_PORT" + echo "========================================" + + # 获取系统信息 + local system_info=$(get_system_info) + local redis_version=$(echo "$system_info" | cut -d'|' -f1) + local redis_uptime=$(echo "$system_info" | cut -d'|' -f2) + local redis_memory=$(echo "$system_info" | cut -d'|' -f3) + local service_status=$(echo "$system_info" | cut -d'|' -f4) + local service_uptime=$(echo "$system_info" | cut -d'|' -f5) + + # 系统状态概览 + echo "🏥 系统状态概览:" + if [ "$service_status" = "healthy" ]; then + echo " ✅ 服务: 健康 (运行 ${service_uptime}h)" + else + echo " ⚠️ 服务: 异常 ($service_status)" + fi + echo " 📊 Redis: v${redis_version} (运行 ${redis_uptime}h, 内存 ${redis_memory})" + echo "" + + # 获取并发信息 + local concurrency_keys=$($REDIS_CMD --scan --pattern "concurrency:*" 2>/dev/null) + local total_concurrent=0 + local active_keys=0 + local concurrent_details="" + + if [ ! -z "$concurrency_keys" ]; then + for key in $concurrency_keys; do + local count=$($REDIS_CMD get "$key" 2>/dev/null) + if [ ! -z "$count" ] && [ "$count" -gt 0 ]; then + local api_key_id=${key#concurrency:} + local key_info=$(get_api_key_info "$api_key_id") + local key_name=$(echo "$key_info" | cut -d'|' -f1) + local concurrency_limit=$(echo "$key_info" | cut -d'|' -f2) + + concurrent_details="${concurrent_details}${key_name}:${count}/${concurrency_limit} " + total_concurrent=$((total_concurrent + count)) + active_keys=$((active_keys + 1)) + fi + done + fi + + # 并发状态显示 + echo "📊 当前并发状态:" + if [ $total_concurrent -eq 0 ]; then + echo " 💤 无活跃并发连接" + else + echo " 🔥 总并发: $total_concurrent 个连接 ($active_keys 个API Key)" + if [ "$DISPLAY_MODE" = "detailed" ]; then + echo " 📋 详情: $concurrent_details" + fi + fi + echo "" + + # API Key统计 + local total_keys=$($REDIS_CMD keys "apikey:*" 2>/dev/null | grep -v "apikey:hash_map" | wc -l) + local total_accounts=$($REDIS_CMD keys "claude:account:*" 2>/dev/null | wc -l) + + echo "📋 资源统计:" + echo " 🔑 API Keys: $total_keys 个" + echo " 🏢 Claude账户: $total_accounts 个" + + # 详细模式显示更多信息 + if [ "$DISPLAY_MODE" = "detailed" ]; then + echo "" + echo "📈 使用统计 (今日/总计):" + + # 获取所有API Key + local api_keys=$($REDIS_CMD keys "apikey:*" 2>/dev/null | grep -v "apikey:hash_map") + local total_daily_requests=0 + local total_daily_tokens=0 + local total_requests=0 + local total_tokens=0 + + if [ ! -z "$api_keys" ]; then + for key in $api_keys; do + local api_key_id=${key#apikey:} + local key_info=$(get_api_key_info "$api_key_id") + local key_name=$(echo "$key_info" | cut -d'|' -f1) + local usage_info=$(get_usage_stats "$api_key_id") + + local key_total_requests=$(echo "$usage_info" | cut -d'|' -f1) + local key_total_tokens=$(echo "$usage_info" | cut -d'|' -f2) + local key_daily_requests=$(echo "$usage_info" | cut -d'|' -f3) + local key_daily_tokens=$(echo "$usage_info" | cut -d'|' -f4) + + total_daily_requests=$((total_daily_requests + key_daily_requests)) + total_daily_tokens=$((total_daily_tokens + key_daily_tokens)) + total_requests=$((total_requests + key_total_requests)) + total_tokens=$((total_tokens + key_total_tokens)) + + if [ $((key_daily_requests + key_total_requests)) -gt 0 ]; then + echo " 📱 $key_name: ${key_daily_requests}req/$(format_number $key_daily_tokens) | ${key_total_requests}req/$(format_number $key_total_tokens)" + fi + done + fi + + echo " 🌍 系统总计: ${total_daily_requests}req/$(format_number $total_daily_tokens) | ${total_requests}req/$(format_number $total_tokens)" + fi + + echo "" + echo "🔄 刷新间隔: 5秒 | 按 Ctrl+C 退出 | 按 Enter 切换详细/简单模式" + + # 非阻塞读取用户输入 + read -t 5 user_input + if [ $? -eq 0 ]; then + case "$user_input" in + "s"|"S"|"") + if [ "$DISPLAY_MODE" = "simple" ]; then + DISPLAY_MODE="detailed" + else + DISPLAY_MODE="simple" + fi + ;; + esac + fi + done +} + +# 信号处理 +cleanup() { + echo "" + echo "👋 监控已停止" + exit 0 +} + +trap cleanup SIGINT SIGTERM + +# 开始监控 +monitor_enhanced \ No newline at end of file diff --git a/scripts/status-unified.sh b/scripts/status-unified.sh new file mode 100755 index 00000000..878f9c4d --- /dev/null +++ b/scripts/status-unified.sh @@ -0,0 +1,262 @@ +#!/bin/bash + +# Claude Relay Service - 统一状态检查脚本 +# 提供完整的系统状态概览 + +# 加载环境变量 +if [ -f .env ]; then + export $(grep -v '^#' .env | xargs) +fi + +# 参数处理 +DETAIL_MODE=false +if [ "$1" = "--detail" ] || [ "$1" = "-d" ]; then + DETAIL_MODE=true +fi + +echo "🔍 Claude Relay Service - 系统状态检查" +if [ "$DETAIL_MODE" = true ]; then + echo "模式: 详细信息" +else + echo "模式: 概览 (使用 --detail 查看详细信息)" +fi +echo "========================================" + +# 获取服务配置 +SERVICE_HOST=${HOST:-127.0.0.1} +SERVICE_PORT=${PORT:-3000} + +if [ "$SERVICE_HOST" = "0.0.0.0" ]; then + SERVICE_HOST="127.0.0.1" +fi + +SERVICE_URL="http://${SERVICE_HOST}:${SERVICE_PORT}" + +# 获取Redis配置 +REDIS_HOST=${REDIS_HOST:-127.0.0.1} +REDIS_PORT=${REDIS_PORT:-6379} +REDIS_CMD="redis-cli -h $REDIS_HOST -p $REDIS_PORT" + +if [ ! -z "$REDIS_PASSWORD" ]; then + REDIS_CMD="redis-cli -h $REDIS_HOST -p $REDIS_PORT -a $REDIS_PASSWORD" +fi + +# 检查Redis连接 +echo "🔍 连接检查:" +if $REDIS_CMD ping > /dev/null 2>&1; then + echo " ✅ Redis连接正常 ($REDIS_HOST:$REDIS_PORT)" +else + echo " ❌ Redis连接失败 ($REDIS_HOST:$REDIS_PORT)" + exit 1 +fi + +# 检查服务状态 +if command -v curl > /dev/null 2>&1; then + health_response=$(curl -s ${SERVICE_URL}/health 2>/dev/null) + if [ $? -eq 0 ]; then + health_status=$(echo "$health_response" | grep -o '"status":"[^"]*"' | cut -d'"' -f4 | head -1) + if [ "$health_status" = "healthy" ]; then + echo " ✅ 服务状态正常 ($SERVICE_URL)" + else + echo " ⚠️ 服务状态异常: $health_status ($SERVICE_URL)" + fi + else + echo " ❌ 服务无法访问 ($SERVICE_URL)" + fi +else + echo " ⚠️ curl命令不可用,无法检查服务状态" +fi + +echo "" + +# 格式化数字函数 +format_number() { + local num=$1 + if [ "$num" -ge 1000000 ]; then + echo "$(echo "scale=1; $num / 1000000" | bc 2>/dev/null)M" + elif [ "$num" -ge 1000 ]; then + echo "$(echo "scale=1; $num / 1000" | bc 2>/dev/null)K" + else + echo "$num" + fi +} + +# 系统信息 +echo "🏥 系统信息:" + +# Redis信息 +redis_info=$($REDIS_CMD info server 2>/dev/null) +redis_memory_info=$($REDIS_CMD info memory 2>/dev/null) + +redis_version=$(echo "$redis_info" | grep redis_version | cut -d: -f2 | tr -d '\r' 2>/dev/null) +redis_uptime=$(echo "$redis_info" | grep uptime_in_seconds | cut -d: -f2 | tr -d '\r' 2>/dev/null) +used_memory=$(echo "$redis_memory_info" | grep used_memory_human | cut -d: -f2 | tr -d '\r' 2>/dev/null) + +if [ ! -z "$redis_version" ]; then + echo " 📊 Redis版本: $redis_version" +fi + +if [ ! -z "$redis_uptime" ]; then + uptime_hours=$((redis_uptime / 3600)) + echo " ⏱️ Redis运行时间: $uptime_hours 小时" +fi + +if [ ! -z "$used_memory" ]; then + echo " 💾 Redis内存使用: $used_memory" +fi + +# 服务信息 +if command -v curl > /dev/null 2>&1; then + health_response=$(curl -s ${SERVICE_URL}/health 2>/dev/null) + if [ $? -eq 0 ]; then + uptime=$(echo "$health_response" | grep -o '"uptime":[^,}]*' | cut -d: -f2 | head -1) + + if [ ! -z "$uptime" ] && [ "$uptime" != "null" ]; then + uptime_hours=$(echo "scale=1; $uptime / 3600" | bc 2>/dev/null) + if [ ! -z "$uptime_hours" ]; then + echo " ⏰ 服务运行时间: $uptime_hours 小时" + fi + fi + + # 检查端口 + if netstat -ln 2>/dev/null | grep -q ":${SERVICE_PORT} "; then + echo " 🔌 端口${SERVICE_PORT}: 正在监听" + else + echo " ❌ 端口${SERVICE_PORT}: 未监听" + fi + fi +fi + +echo "" + +# 并发状态 +echo "📊 并发状态:" +concurrency_keys=$($REDIS_CMD --scan --pattern "concurrency:*" 2>/dev/null) + +if [ -z "$concurrency_keys" ]; then + echo " 💤 当前无活跃并发连接" +else + total_concurrent=0 + active_keys=0 + + for key in $concurrency_keys; do + count=$($REDIS_CMD get "$key" 2>/dev/null) + if [ ! -z "$count" ] && [ "$count" -gt 0 ]; then + api_key_id=${key#concurrency:} + + if [ "$DETAIL_MODE" = true ]; then + api_key_name=$($REDIS_CMD hget "apikey:$api_key_id" name 2>/dev/null) + concurrency_limit=$($REDIS_CMD hget "apikey:$api_key_id" concurrencyLimit 2>/dev/null) + + if [ -z "$api_key_name" ]; then + api_key_name="Unknown" + fi + + if [ -z "$concurrency_limit" ] || [ "$concurrency_limit" = "0" ]; then + limit_text="无限制" + else + limit_text="$concurrency_limit" + fi + + echo " 🔑 $api_key_name: $count 个并发 (限制: $limit_text)" + fi + + total_concurrent=$((total_concurrent + count)) + active_keys=$((active_keys + 1)) + fi + done + + echo " 📈 总计: $total_concurrent 个活跃并发连接 ($active_keys 个API Key)" +fi + +echo "" + +# 资源统计 +echo "📋 资源统计:" + +total_keys=$($REDIS_CMD keys "apikey:*" 2>/dev/null | grep -v "apikey:hash_map" | wc -l) +total_accounts=$($REDIS_CMD keys "claude:account:*" 2>/dev/null | wc -l) + +echo " 🔑 API Key总数: $total_keys" +echo " 🏢 Claude账户数: $total_accounts" + +# 详细模式下的使用统计 +if [ "$DETAIL_MODE" = true ]; then + echo "" + echo "📈 使用统计:" + + today=$(date '+%Y-%m-%d') + current_month=$(date '+%Y-%m') + + # 系统总体统计 + total_daily_requests=0 + total_daily_tokens=0 + total_requests=0 + total_tokens=0 + + api_keys=$($REDIS_CMD keys "apikey:*" 2>/dev/null | grep -v "apikey:hash_map") + + if [ ! -z "$api_keys" ]; then + echo " 📱 API Key详情:" + + for key in $api_keys; do + api_key_id=${key#apikey:} + + # API Key基本信息 + api_key_name=$($REDIS_CMD hget "apikey:$api_key_id" name 2>/dev/null) + token_limit=$($REDIS_CMD hget "apikey:$api_key_id" tokenLimit 2>/dev/null) + created_at=$($REDIS_CMD hget "apikey:$api_key_id" createdAt 2>/dev/null) + + # 使用统计 + key_total_requests=$($REDIS_CMD hget "usage:$api_key_id" totalRequests 2>/dev/null) + key_total_tokens=$($REDIS_CMD hget "usage:$api_key_id" totalTokens 2>/dev/null) + key_daily_requests=$($REDIS_CMD hget "usage:daily:$api_key_id:$today" requests 2>/dev/null) + key_daily_tokens=$($REDIS_CMD hget "usage:daily:$api_key_id:$today" tokens 2>/dev/null) + + # 默认值处理 + api_key_name=${api_key_name:-"Unknown"} + token_limit=${token_limit:-0} + key_total_requests=${key_total_requests:-0} + key_total_tokens=${key_total_tokens:-0} + key_daily_requests=${key_daily_requests:-0} + key_daily_tokens=${key_daily_tokens:-0} + + # 格式化Token限制 + if [ "$token_limit" = "0" ]; then + limit_text="无限制" + else + limit_text=$(format_number $token_limit) + fi + + # 创建时间格式化 + if [ ! -z "$created_at" ]; then + created_date=$(echo "$created_at" | cut -d'T' -f1) + else + created_date="未知" + fi + + echo " • $api_key_name (创建: $created_date, 限制: $limit_text)" + echo " 今日: ${key_daily_requests}请求 / $(format_number $key_daily_tokens)tokens" + echo " 总计: ${key_total_requests}请求 / $(format_number $key_total_tokens)tokens" + echo "" + + # 累计统计 + total_daily_requests=$((total_daily_requests + key_daily_requests)) + total_daily_tokens=$((total_daily_tokens + key_daily_tokens)) + total_requests=$((total_requests + key_total_requests)) + total_tokens=$((total_tokens + key_total_tokens)) + done + fi + + echo " 🌍 系统总计:" + echo " 今日: ${total_daily_requests}请求 / $(format_number $total_daily_tokens)tokens" + echo " 总计: ${total_requests}请求 / $(format_number $total_tokens)tokens" +fi + +echo "" +echo "✅ 状态检查完成 - $(date '+%Y-%m-%d %H:%M:%S')" + +if [ "$DETAIL_MODE" = false ]; then + echo "" + echo "💡 使用 'npm run status -- --detail' 查看详细信息" +fi \ No newline at end of file diff --git a/src/app.js b/src/app.js index 08709f72..f33b8a63 100644 --- a/src/app.js +++ b/src/app.js @@ -280,13 +280,13 @@ class Application { const health = { status: 'healthy', service: 'claude-relay-service', - version: version, + version, timestamp: new Date().toISOString(), uptime: process.uptime(), memory: { - used: Math.round(memory.heapUsed / 1024 / 1024) + 'MB', - total: Math.round(memory.heapTotal / 1024 / 1024) + 'MB', - external: Math.round(memory.external / 1024 / 1024) + 'MB' + used: `${Math.round(memory.heapUsed / 1024 / 1024)}MB`, + total: `${Math.round(memory.heapTotal / 1024 / 1024)}MB`, + external: `${Math.round(memory.external / 1024 / 1024)}MB` }, components: { redis: redisHealth, @@ -364,7 +364,7 @@ class Application { // 存储到Redis(每次启动都覆盖,确保与 init.json 同步) const adminCredentials = { username: initData.adminUsername, - passwordHash: passwordHash, + passwordHash, createdAt: initData.initializedAt || new Date().toISOString(), lastLogin: null, updatedAt: initData.updatedAt || null diff --git a/src/middleware/auth.js b/src/middleware/auth.js index a82971dc..660bb72d 100644 --- a/src/middleware/auth.js +++ b/src/middleware/auth.js @@ -9,12 +9,13 @@ const authenticateApiKey = async (req, res, next) => { const startTime = Date.now() try { - // 安全提取API Key,支持多种格式 + // 安全提取API Key,支持多种格式(包括Gemini CLI支持) const apiKey = req.headers['x-api-key'] || req.headers['x-goog-api-key'] || req.headers['authorization']?.replace(/^Bearer\s+/i, '') || - req.headers['api-key'] + req.headers['api-key'] || + req.query.key if (!apiKey) { logger.security(`🔒 Missing API key attempt from ${req.ip || 'unknown'}`) diff --git a/src/services/geminiRelayService.js b/src/services/geminiRelayService.js index d636bc92..35423632 100644 --- a/src/services/geminiRelayService.js +++ b/src/services/geminiRelayService.js @@ -441,9 +441,135 @@ async function getAvailableModels(accessToken, proxy, projectId, location = 'us- } } +// Count Tokens API - 用于Gemini CLI兼容性 +async function countTokens({ + model, + content, + accessToken, + proxy, + projectId, + location = 'us-central1' +}) { + // 确保模型名称格式正确 + if (!model.startsWith('models/')) { + model = `models/${model}` + } + + // 转换内容格式 - 支持多种输入格式 + let requestBody + if (Array.isArray(content)) { + // 如果content是数组,直接使用 + requestBody = { contents: content } + } else if (typeof content === 'string') { + // 如果是字符串,转换为Gemini格式 + requestBody = { + contents: [ + { + parts: [{ text: content }] + } + ] + } + } else if (content.parts || content.role) { + // 如果已经是Gemini格式的单个content + requestBody = { contents: [content] } + } else { + // 其他情况,尝试直接使用 + requestBody = { contents: content } + } + + // 构建API URL - countTokens需要使用generativelanguage API + const GENERATIVE_API_BASE = 'https://generativelanguage.googleapis.com/v1beta' + let apiUrl + if (projectId) { + // 使用项目特定的 URL 格式(Google Cloud/Workspace 账号) + apiUrl = `${GENERATIVE_API_BASE}/projects/${projectId}/locations/${location}/${model}:countTokens` + logger.debug( + `Using project-specific countTokens URL with projectId: ${projectId}, location: ${location}` + ) + } else { + // 使用标准 URL 格式(个人 Google 账号) + apiUrl = `${GENERATIVE_API_BASE}/${model}:countTokens` + logger.debug('Using standard countTokens URL without projectId') + } + + const axiosConfig = { + method: 'POST', + url: apiUrl, + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + 'X-Goog-User-Project': projectId || undefined + }, + data: requestBody, + timeout: 30000 + } + + // 添加代理配置 + const proxyAgent = createProxyAgent(proxy) + if (proxyAgent) { + axiosConfig.httpsAgent = proxyAgent + logger.debug('Using proxy for Gemini countTokens request') + } + + try { + logger.debug(`Sending countTokens request to: ${apiUrl}`) + logger.debug(`Request body: ${JSON.stringify(requestBody, null, 2)}`) + const response = await axios(axiosConfig) + + // 返回符合Gemini API格式的响应 + return { + totalTokens: response.data.totalTokens || 0, + totalBillableCharacters: response.data.totalBillableCharacters || 0, + ...response.data + } + } catch (error) { + logger.error(`Gemini countTokens API request failed for URL: ${apiUrl}`) + logger.error( + 'Request config:', + JSON.stringify( + { + url: apiUrl, + headers: axiosConfig.headers, + data: requestBody + }, + null, + 2 + ) + ) + logger.error('Error details:', error.response?.data || error.message) + + // 转换错误格式 + if (error.response) { + const geminiError = error.response.data?.error + const errorObj = new Error( + geminiError?.message || + `Gemini countTokens API request failed (Status: ${error.response.status})` + ) + errorObj.status = error.response.status + errorObj.error = { + message: + geminiError?.message || + `Gemini countTokens API request failed (Status: ${error.response.status})`, + type: geminiError?.code || 'api_error', + code: geminiError?.code + } + throw errorObj + } + + const errorObj = new Error(error.message) + errorObj.status = 500 + errorObj.error = { + message: error.message, + type: 'network_error' + } + throw errorObj + } +} + module.exports = { sendGeminiRequest, getAvailableModels, convertMessagesToGemini, - convertGeminiResponse + convertGeminiResponse, + countTokens }