diff --git a/CLAUDE.md b/CLAUDE.md index 6abbc57b..568ba60b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## 项目概述 -Claude Relay Service 是一个功能完整的 Claude API 中转服务,支持多账户管理、API Key 认证、代理配置和现代化 Web 管理界面。该服务作为客户端(如 SillyTavern)与 Anthropic API 之间的中间件,提供认证、限流、监控等功能。 +Claude Relay Service 是一个功能完整的 AI API 中转服务,支持 Claude 和 Gemini 双平台。提供多账户管理、API Key 认证、代理配置和现代化 Web 管理界面。该服务作为客户端(如 SillyTavern、Claude Code、Gemini CLI)与 AI API 之间的中间件,提供认证、限流、监控等功能。 ## 核心架构 @@ -19,6 +19,7 @@ Claude Relay Service 是一个功能完整的 Claude API 中转服务,支持 ### 主要服务组件 - **claudeRelayService.js**: 核心代理服务,处理请求转发和流式响应 - **claudeAccountService.js**: Claude账户管理,OAuth token刷新和账户选择 +- **geminiAccountService.js**: Gemini账户管理,Google OAuth token刷新和账户选择 - **apiKeyService.js**: API Key管理,验证、限流和使用统计 - **oauthHelper.js**: OAuth工具,PKCE流程实现和代理支持 @@ -124,6 +125,11 @@ npm run setup # 自动生成密钥并创建管理员账户 2. **授权码无效**: 确保复制了完整的Authorization Code,没有遗漏字符 3. **Token刷新失败**: 检查refreshToken有效性和代理配置 +### Gemini Token刷新问题 +1. **刷新失败**: 确保 refresh_token 有效且未过期 +2. **错误日志**: 查看 `logs/token-refresh-error.log` 获取详细错误信息 +3. **测试脚本**: 运行 `node scripts/test-gemini-refresh.js` 测试 token 刷新 + ### 常见开发问题 1. **Redis连接失败**: 确认Redis服务运行,检查连接配置 2. **管理员登录失败**: 检查init.json同步到Redis,运行npm run setup @@ -217,4 +223,9 @@ npm run cli accounts refresh # 管理员操作 npm run cli admin create -- --username admin2 npm run cli admin reset-password -- --username admin -``` \ No newline at end of file +``` +# important-instruction-reminders +Do what has been asked; nothing more, nothing less. +NEVER create files unless they're absolutely necessary for achieving your goal. +ALWAYS prefer editing an existing file to creating a new one. +NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User. \ No newline at end of file diff --git a/scripts/test-gemini-refresh.js b/scripts/test-gemini-refresh.js new file mode 100644 index 00000000..c0588735 --- /dev/null +++ b/scripts/test-gemini-refresh.js @@ -0,0 +1,91 @@ +#!/usr/bin/env node + +/** + * 测试 Gemini token 刷新功能 + */ + +const path = require('path'); +const dotenv = require('dotenv'); + +// 加载环境变量 +dotenv.config({ path: path.join(__dirname, '..', '.env') }); + +const redis = require('../src/models/redis'); +const geminiAccountService = require('../src/services/geminiAccountService'); +const logger = require('../src/utils/logger'); + +async function testGeminiTokenRefresh() { + try { + console.log('🚀 开始测试 Gemini token 刷新功能...\n'); + + // 1. 连接 Redis + console.log('📡 连接 Redis...'); + await redis.connect(); + console.log('✅ Redis 连接成功\n'); + + // 2. 获取所有 Gemini 账户 + console.log('🔍 获取 Gemini 账户列表...'); + const accounts = await geminiAccountService.getAllAccounts(); + const geminiAccounts = accounts.filter(acc => acc.platform === 'gemini'); + + if (geminiAccounts.length === 0) { + console.log('❌ 没有找到 Gemini 账户'); + process.exit(1); + } + + console.log(`✅ 找到 ${geminiAccounts.length} 个 Gemini 账户\n`); + + // 3. 测试每个账户的 token 刷新 + for (const account of geminiAccounts) { + console.log(`\n📋 测试账户: ${account.name} (${account.id})`); + console.log(` 状态: ${account.status}`); + console.log(` 是否有 refresh token: ${account.refreshToken ? '是' : '否'}`); + + if (!account.refreshToken || account.refreshToken === '[ENCRYPTED]') { + console.log(' ⚠️ 跳过:无 refresh token\n'); + continue; + } + + try { + // 获取完整账户信息(包括解密的 token) + const fullAccount = await geminiAccountService.getAccount(account.id); + + if (!fullAccount.refreshToken) { + console.log(' ⚠️ 跳过:无法获取 refresh token\n'); + continue; + } + + console.log(' 🔄 开始刷新 token...'); + const startTime = Date.now(); + + // 执行 token 刷新 + const newTokens = await geminiAccountService.refreshAccountToken(account.id); + + const duration = Date.now() - startTime; + console.log(` ✅ Token 刷新成功!耗时: ${duration}ms`); + console.log(` 📅 新的过期时间: ${new Date(newTokens.expiry_date).toLocaleString()}`); + console.log(` 🔑 Access Token: ${newTokens.access_token.substring(0, 20)}...`); + + // 验证账户状态已更新 + const updatedAccount = await geminiAccountService.getAccount(account.id); + console.log(` 📊 账户状态: ${updatedAccount.status}`); + + } catch (error) { + console.log(` ❌ Token 刷新失败: ${error.message}`); + console.log(` 🔍 错误详情:`, error); + } + } + + console.log('\n✅ 测试完成!'); + + } catch (error) { + console.error('❌ 测试失败:', error); + } finally { + // 断开 Redis 连接 + await redis.disconnect(); + process.exit(0); + } +} + +// 运行测试 +testGeminiTokenRefresh(); \ No newline at end of file diff --git a/src/services/geminiAccountService.js b/src/services/geminiAccountService.js index f38721a7..c2336e43 100644 --- a/src/services/geminiAccountService.js +++ b/src/services/geminiAccountService.js @@ -168,22 +168,36 @@ async function refreshAccessToken(refreshToken) { const oAuth2Client = createOAuth2Client(); try { + // 设置 refresh_token oAuth2Client.setCredentials({ refresh_token: refreshToken }); - const { credentials } = await oAuth2Client.refreshAccessToken(); + // 调用 refreshAccessToken 获取新的 tokens + const response = await oAuth2Client.refreshAccessToken(); + const credentials = response.credentials; + + // 检查是否成功获取了新的 access_token + if (!credentials || !credentials.access_token) { + throw new Error('No access token returned from refresh'); + } + + logger.info(`🔄 Successfully refreshed Gemini token. New expiry: ${new Date(credentials.expiry_date).toISOString()}`); return { access_token: credentials.access_token, - refresh_token: credentials.refresh_token || refreshToken, + refresh_token: credentials.refresh_token || refreshToken, // 保留原 refresh_token 如果没有返回新的 scope: credentials.scope || OAUTH_SCOPES.join(' '), token_type: credentials.token_type || 'Bearer', - expiry_date: credentials.expiry_date + expiry_date: credentials.expiry_date || Date.now() + 3600000 // 默认1小时过期 }; } catch (error) { - logger.error('Error refreshing access token:', error); - throw new Error('Failed to refresh access token'); + logger.error('Error refreshing access token:', { + message: error.message, + code: error.code, + response: error.response?.data + }); + throw new Error(`Failed to refresh access token: ${error.message}`); } } @@ -625,11 +639,17 @@ async function refreshAccountToken(accountId) { logger.error(`Failed to refresh token for account ${accountId}:`, error); - // 标记账户为错误状态 - await updateAccount(accountId, { - status: 'error', - errorMessage: error.message - }); + // 标记账户为错误状态(只有在账户存在时) + if (account) { + try { + await updateAccount(accountId, { + status: 'error', + errorMessage: error.message + }); + } catch (updateError) { + logger.error(`Failed to update account status after refresh error:`, updateError); + } + } throw error; } finally {