From ef4f7483d363a62b25c2adea0ea94cb68dfe18c6 Mon Sep 17 00:00:00 2001
From: shaw
Date: Mon, 4 Aug 2025 16:53:11 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84=20Gemini=20=E5=8A=9F?=
=?UTF-8?q?=E8=83=BD=E4=B8=8E=20Claude=20=E4=BF=9D=E6=8C=81=E4=B8=80?=
=?UTF-8?q?=E8=87=B4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 添加 Gemini 账户的 schedulable 字段和调度开关 API
- 实现 Gemini 调度器的模型过滤功能
- 完善 Gemini 数据统计,记录 token 使用量
- 修复 Gemini 流式响应的 SSE 解析和 AbortController 支持
- 在教程页面和 README 中添加 Gemini CLI 环境变量说明
- 修复前端 Gemini 账户调度开关限制
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude
---
README.md | 18 +-
src/routes/admin.js | 24 ++
src/routes/geminiRoutes.js | 5 +-
src/services/geminiAccountService.js | 7 +
src/services/geminiRelayService.js | 76 ++++--
src/services/unifiedClaudeScheduler.js | 6 +-
src/services/unifiedGeminiScheduler.js | 41 +++-
web/admin-spa/src/views/AccountsView.vue | 4 +-
web/admin-spa/src/views/TutorialView.vue | 295 ++++++++++++++++++++++-
9 files changed, 443 insertions(+), 33 deletions(-)
diff --git a/README.md b/README.md
index a376441a..8c96ba88 100644
--- a/README.md
+++ b/README.md
@@ -451,21 +451,33 @@ docker-compose.yml 已包含:
- **客户端限制**: 限制只允许特定客户端使用(如ClaudeCode、Gemini-CLI等)
5. 保存,记下生成的Key
-### 4. 开始使用Claude code
+### 4. 开始使用 Claude Code 和 Gemini CLI
现在你可以用自己的服务替换官方API了:
-**设置环境变量:**
+**Claude Code 设置环境变量:**
```bash
export ANTHROPIC_BASE_URL="http://127.0.0.1:3000/api/" # 根据实际填写你服务器的ip地址或者域名
export ANTHROPIC_AUTH_TOKEN="后台创建的API密钥"
```
-**使用claude:**
+**Gemini CLI 设置环境变量:**
+```bash
+export CODE_ASSIST_ENDPOINT="http://127.0.0.1:3000/gemini" # 根据实际填写你服务器的ip地址或者域名
+export GOOGLE_CLOUD_ACCESS_TOKEN="后台创建的API密钥" # 使用相同的API密钥即可
+export GOOGLE_GENAI_USE_GCA="true"
+```
+
+**使用 Claude Code:**
```bash
claude
```
+**使用 Gemini CLI:**
+```bash
+gemini # 或其他 Gemini CLI 命令
+```
+
### 5. 第三方工具API接入
本服务支持多种API端点格式,方便接入不同的第三方工具(如Cherry Studio等):
diff --git a/src/routes/admin.js b/src/routes/admin.js
index 31fcfba7..a57572ec 100644
--- a/src/routes/admin.js
+++ b/src/routes/admin.js
@@ -1521,6 +1521,30 @@ router.post('/gemini-accounts/:accountId/refresh', authenticateAdmin, async (req
}
});
+// 切换 Gemini 账户调度状态
+router.put('/gemini-accounts/:accountId/toggle-schedulable', authenticateAdmin, async (req, res) => {
+ try {
+ const { accountId } = req.params;
+
+ const account = await geminiAccountService.getAccount(accountId);
+ if (!account) {
+ return res.status(404).json({ error: 'Account not found' });
+ }
+
+ // 将字符串 'true'/'false' 转换为布尔值,然后取反
+ const currentSchedulable = account.schedulable === 'true';
+ const newSchedulable = !currentSchedulable;
+
+ await geminiAccountService.updateAccount(accountId, { schedulable: String(newSchedulable) });
+
+ logger.success(`🔄 Admin toggled Gemini account schedulable status: ${accountId} -> ${newSchedulable ? 'schedulable' : 'not schedulable'}`);
+ res.json({ success: true, schedulable: newSchedulable });
+ } catch (error) {
+ logger.error('❌ Failed to toggle Gemini account schedulable status:', error);
+ res.status(500).json({ error: 'Failed to toggle schedulable status', message: error.message });
+ }
+});
+
// 📊 账户使用统计
// 获取所有账户的使用统计
diff --git a/src/routes/geminiRoutes.js b/src/routes/geminiRoutes.js
index c48a85fc..c92b5a48 100644
--- a/src/routes/geminiRoutes.js
+++ b/src/routes/geminiRoutes.js
@@ -7,7 +7,7 @@ const { sendGeminiRequest, getAvailableModels } = require('../services/geminiRel
const crypto = require('crypto');
const sessionHelper = require('../utils/sessionHelper');
const unifiedGeminiScheduler = require('../services/unifiedGeminiScheduler');
-const { OAuth2Client } = require('google-auth-library');
+// const { OAuth2Client } = require('google-auth-library'); // OAuth2Client is not used in this file
// 生成会话哈希
function generateSessionHash(req) {
@@ -108,7 +108,8 @@ router.post('/messages', authenticateApiKey, async (req, res) => {
proxy: account.proxy,
apiKeyId: apiKeyData.id,
signal: abortController.signal,
- projectId: account.projectId
+ projectId: account.projectId,
+ accountId: account.id
});
if (stream) {
diff --git a/src/services/geminiAccountService.js b/src/services/geminiAccountService.js
index dfb2bb32..348fdf3c 100644
--- a/src/services/geminiAccountService.js
+++ b/src/services/geminiAccountService.js
@@ -279,6 +279,10 @@ async function createAccount(accountData) {
accountType: accountData.accountType || 'shared',
isActive: 'true',
status: 'active',
+
+ // 调度相关
+ schedulable: accountData.schedulable !== undefined ? String(accountData.schedulable) : 'true',
+ priority: accountData.priority || 50, // 调度优先级 (1-100,数字越小优先级越高)
// OAuth 相关字段(加密存储)
geminiOauth: geminiOauth ? encrypt(geminiOauth) : '',
@@ -292,6 +296,9 @@ async function createAccount(accountData) {
// 项目编号(Google Cloud/Workspace 账号需要)
projectId: accountData.projectId || '',
+
+ // 支持的模型列表(可选)
+ supportedModels: accountData.supportedModels || [], // 空数组表示支持所有模型
// 时间戳
createdAt: now,
diff --git a/src/services/geminiRelayService.js b/src/services/geminiRelayService.js
index 3895a619..5e19c3ea 100644
--- a/src/services/geminiRelayService.js
+++ b/src/services/geminiRelayService.js
@@ -3,7 +3,7 @@ const { HttpsProxyAgent } = require('https-proxy-agent');
const { SocksProxyAgent } = require('socks-proxy-agent');
const logger = require('../utils/logger');
const config = require('../../config/config');
-const { recordUsageMetrics } = require('./apiKeyService');
+const apiKeyService = require('./apiKeyService');
// Gemini API 配置
const GEMINI_API_BASE = 'https://cloudcode.googleapis.com/v1';
@@ -123,7 +123,7 @@ function convertGeminiResponse(geminiResponse, model, stream = false) {
}
// 处理流式响应
-async function* handleStreamResponse(response, model, apiKeyId) {
+async function* handleStreamResponse(response, model, apiKeyId, accountId = null) {
let buffer = '';
let totalUsage = {
promptTokenCount: 0,
@@ -168,10 +168,16 @@ async function* handleStreamResponse(response, model, apiKeyId) {
if (data.candidates?.[0]?.finishReason === 'STOP') {
// 记录使用量
if (apiKeyId && totalUsage.totalTokenCount > 0) {
- await recordUsageMetrics(apiKeyId, {
- inputTokens: totalUsage.promptTokenCount,
- outputTokens: totalUsage.candidatesTokenCount,
- model: model
+ await apiKeyService.recordUsage(
+ apiKeyId,
+ totalUsage.promptTokenCount || 0, // inputTokens
+ totalUsage.candidatesTokenCount || 0, // outputTokens
+ 0, // cacheCreateTokens (Gemini 没有这个概念)
+ 0, // cacheReadTokens (Gemini 没有这个概念)
+ model,
+ accountId
+ ).catch(error => {
+ logger.error('❌ Failed to record Gemini usage:', error);
});
}
@@ -206,13 +212,18 @@ async function* handleStreamResponse(response, model, apiKeyId) {
yield 'data: [DONE]\n\n';
} catch (error) {
- logger.error('Stream processing error:', error);
- yield `data: ${JSON.stringify({
- error: {
- message: error.message,
- type: 'stream_error'
- }
- })}\n\n`;
+ // 检查是否是请求被中止
+ if (error.name === 'CanceledError' || error.code === 'ECONNABORTED') {
+ logger.info('Stream request was aborted by client');
+ } else {
+ logger.error('Stream processing error:', error);
+ yield `data: ${JSON.stringify({
+ error: {
+ message: error.message,
+ type: 'stream_error'
+ }
+ })}\n\n`;
+ }
}
}
@@ -226,8 +237,10 @@ async function sendGeminiRequest({
accessToken,
proxy,
apiKeyId,
+ signal,
projectId,
- location = 'us-central1'
+ location = 'us-central1',
+ accountId = null
}) {
// 确保模型名称格式正确
if (!model.startsWith('models/')) {
@@ -281,6 +294,12 @@ async function sendGeminiRequest({
logger.debug('Using proxy for Gemini request');
}
+ // 添加 AbortController 信号支持
+ if (signal) {
+ axiosConfig.signal = signal;
+ logger.debug('AbortController signal attached to request');
+ }
+
if (stream) {
axiosConfig.responseType = 'stream';
}
@@ -290,23 +309,42 @@ async function sendGeminiRequest({
const response = await axios(axiosConfig);
if (stream) {
- return handleStreamResponse(response, model, apiKeyId);
+ return handleStreamResponse(response, model, apiKeyId, accountId);
} else {
// 非流式响应
const openaiResponse = convertGeminiResponse(response.data, model, false);
// 记录使用量
if (apiKeyId && openaiResponse.usage) {
- await recordUsageMetrics(apiKeyId, {
- inputTokens: openaiResponse.usage.prompt_tokens,
- outputTokens: openaiResponse.usage.completion_tokens,
- model: model
+ await apiKeyService.recordUsage(
+ apiKeyId,
+ openaiResponse.usage.prompt_tokens || 0,
+ openaiResponse.usage.completion_tokens || 0,
+ 0, // cacheCreateTokens
+ 0, // cacheReadTokens
+ model,
+ accountId
+ ).catch(error => {
+ logger.error('❌ Failed to record Gemini usage:', error);
});
}
return openaiResponse;
}
} catch (error) {
+ // 检查是否是请求被中止
+ if (error.name === 'CanceledError' || error.code === 'ECONNABORTED') {
+ logger.info('Gemini request was aborted by client');
+ throw {
+ status: 499,
+ error: {
+ message: 'Request canceled by client',
+ type: 'canceled',
+ code: 'request_canceled'
+ }
+ };
+ }
+
logger.error('Gemini API request failed:', error.response?.data || error.message);
// 转换错误格式
diff --git a/src/services/unifiedClaudeScheduler.js b/src/services/unifiedClaudeScheduler.js
index e86d98f0..c2b07b7e 100644
--- a/src/services/unifiedClaudeScheduler.js
+++ b/src/services/unifiedClaudeScheduler.js
@@ -18,7 +18,7 @@ class UnifiedClaudeScheduler {
if (apiKeyData.claudeAccountId.startsWith('group:')) {
const groupId = apiKeyData.claudeAccountId.replace('group:', '');
logger.info(`🎯 API key ${apiKeyData.name} is bound to group ${groupId}, selecting from group`);
- return await this.selectAccountFromGroup(groupId, sessionHash, requestedModel, apiKeyData);
+ return await this.selectAccountFromGroup(groupId, sessionHash, requestedModel);
}
// 普通专属账户
@@ -370,7 +370,7 @@ class UnifiedClaudeScheduler {
}
// 👥 从分组中选择账户
- async selectAccountFromGroup(groupId, sessionHash = null, requestedModel = null, apiKeyData = null) {
+ async selectAccountFromGroup(groupId, sessionHash = null, requestedModel = null) {
try {
// 获取分组信息
const group = await accountGroupService.getGroup(groupId);
@@ -426,7 +426,7 @@ class UnifiedClaudeScheduler {
}
} else if (group.platform === 'gemini') {
// Gemini暂时不支持,预留接口
- logger.warn(`⚠️ Gemini group scheduling not yet implemented`);
+ logger.warn('⚠️ Gemini group scheduling not yet implemented');
continue;
}
diff --git a/src/services/unifiedGeminiScheduler.js b/src/services/unifiedGeminiScheduler.js
index 3860fed4..f4f056b6 100644
--- a/src/services/unifiedGeminiScheduler.js
+++ b/src/services/unifiedGeminiScheduler.js
@@ -95,6 +95,19 @@ class UnifiedGeminiScheduler {
if (boundAccount && boundAccount.isActive === 'true' && boundAccount.status !== 'error') {
const isRateLimited = await this.isAccountRateLimited(boundAccount.id);
if (!isRateLimited) {
+ // 检查模型支持
+ if (requestedModel && boundAccount.supportedModels && boundAccount.supportedModels.length > 0) {
+ // 处理可能带有 models/ 前缀的模型名
+ const normalizedModel = requestedModel.replace('models/', '');
+ const modelSupported = boundAccount.supportedModels.some(model =>
+ model.replace('models/', '') === normalizedModel
+ );
+ if (!modelSupported) {
+ logger.warn(`⚠️ Bound Gemini account ${boundAccount.name} does not support model ${requestedModel}`);
+ return availableAccounts;
+ }
+ }
+
logger.info(`🎯 Using bound dedicated Gemini account: ${boundAccount.name} (${apiKeyData.geminiAccountId})`);
return [{
...boundAccount,
@@ -124,6 +137,19 @@ class UnifiedGeminiScheduler {
continue;
}
+ // 检查模型支持
+ if (requestedModel && account.supportedModels && account.supportedModels.length > 0) {
+ // 处理可能带有 models/ 前缀的模型名
+ const normalizedModel = requestedModel.replace('models/', '');
+ const modelSupported = account.supportedModels.some(model =>
+ model.replace('models/', '') === normalizedModel
+ );
+ if (!modelSupported) {
+ logger.debug(`⏭️ Skipping Gemini account ${account.name} - doesn't support model ${requestedModel}`);
+ continue;
+ }
+ }
+
// 检查是否被限流
const isRateLimited = await this.isAccountRateLimited(account.id);
if (!isRateLimited) {
@@ -269,7 +295,7 @@ class UnifiedGeminiScheduler {
}
// 👥 从分组中选择账户
- async selectAccountFromGroup(groupId, sessionHash = null, requestedModel = null, apiKeyData = null) {
+ async selectAccountFromGroup(groupId, sessionHash = null, requestedModel = null) {
try {
// 获取分组信息
const group = await accountGroupService.getGroup(groupId);
@@ -330,6 +356,19 @@ class UnifiedGeminiScheduler {
continue;
}
+ // 检查模型支持
+ if (requestedModel && account.supportedModels && account.supportedModels.length > 0) {
+ // 处理可能带有 models/ 前缀的模型名
+ const normalizedModel = requestedModel.replace('models/', '');
+ const modelSupported = account.supportedModels.some(model =>
+ model.replace('models/', '') === normalizedModel
+ );
+ if (!modelSupported) {
+ logger.debug(`⏭️ Skipping Gemini account ${account.name} in group - doesn't support model ${requestedModel}`);
+ continue;
+ }
+ }
+
// 检查是否被限流
const isRateLimited = await this.isAccountRateLimited(account.id);
if (!isRateLimited) {
diff --git a/web/admin-spa/src/views/AccountsView.vue b/web/admin-spa/src/views/AccountsView.vue
index 485d4eee..b083da9e 100644
--- a/web/admin-spa/src/views/AccountsView.vue
+++ b/web/admin-spa/src/views/AccountsView.vue
@@ -1050,8 +1050,10 @@ const toggleSchedulable = async (account) => {
endpoint = `/admin/claude-accounts/${account.id}/toggle-schedulable`
} else if (account.platform === 'claude-console') {
endpoint = `/admin/claude-console-accounts/${account.id}/toggle-schedulable`
+ } else if (account.platform === 'gemini') {
+ endpoint = `/admin/gemini-accounts/${account.id}/toggle-schedulable`
} else {
- showToast('Gemini账户暂不支持调度控制', 'warning')
+ showToast('该账户类型暂不支持调度控制', 'warning')
return
}
diff --git a/web/admin-spa/src/views/TutorialView.vue b/web/admin-spa/src/views/TutorialView.vue
index 26a3657d..9a18479e 100644
--- a/web/admin-spa/src/views/TutorialView.vue
+++ b/web/admin-spa/src/views/TutorialView.vue
@@ -338,6 +338,85 @@
+
+
+
+
+
+ 配置 Gemini CLI 环境变量
+
+
+ 如果你使用 Gemini CLI,需要设置以下环境变量:
+
+
+
+
+
+ PowerShell 设置方法
+
+
+ 在 PowerShell 中运行以下命令:
+
+
+
+ $env:CODE_ASSIST_ENDPOINT = "{{ geminiBaseUrl }}"
+
+
+ $env:GOOGLE_CLOUD_ACCESS_TOKEN = "你的API密钥"
+
+
+ $env:GOOGLE_GENAI_USE_GCA = "true"
+
+
+
+ 💡 使用与 Claude Code 相同的 API 密钥即可。
+
+
+
+
+
+ 系统环境变量(永久设置)
+
+
+ 在系统环境变量中添加:
+
+
+
+ 变量名: CODE_ASSIST_ENDPOINT
+ 变量值: {{ geminiBaseUrl }}
+
+
+ 变量名: GOOGLE_CLOUD_ACCESS_TOKEN
+ 变量值: 你的API密钥
+
+
+ 变量名: GOOGLE_GENAI_USE_GCA
+ 变量值: true
+
+
+
+
+
+
+ 验证 Gemini CLI 环境变量
+
+
+ 在 PowerShell 中验证:
+
+
+
+ echo $env:CODE_ASSIST_ENDPOINT
+
+
+ echo $env:GOOGLE_CLOUD_ACCESS_TOKEN
+
+
+ echo $env:GOOGLE_GENAI_USE_GCA
+
+
+
+
+
@@ -657,6 +736,105 @@
+
+
+
+
+
+ 配置 Gemini CLI 环境变量
+
+
+ 如果你使用 Gemini CLI,需要设置以下环境变量:
+
+
+
+
+
+ Terminal 设置方法
+
+
+ 在 Terminal 中运行以下命令:
+
+
+
+ export CODE_ASSIST_ENDPOINT="{{ geminiBaseUrl }}"
+
+
+ export GOOGLE_CLOUD_ACCESS_TOKEN="你的API密钥"
+
+
+ export GOOGLE_GENAI_USE_GCA="true"
+
+
+
+ 💡 使用与 Claude Code 相同的 API 密钥即可。
+
+
+
+
+
+ 永久设置方法
+
+
+ 添加到你的 shell 配置文件:
+
+
+
+ # 对于 zsh (默认)
+
+
+ echo 'export CODE_ASSIST_ENDPOINT="{{ geminiBaseUrl }}"' >> ~/.zshrc
+
+
+ echo 'export GOOGLE_CLOUD_ACCESS_TOKEN="你的API密钥"' >> ~/.zshrc
+
+
+ echo 'export GOOGLE_GENAI_USE_GCA="true"' >> ~/.zshrc
+
+
+ source ~/.zshrc
+
+
+
+
+ # 对于 bash
+
+
+ echo 'export CODE_ASSIST_ENDPOINT="{{ geminiBaseUrl }}"' >> ~/.bash_profile
+
+
+ echo 'export GOOGLE_CLOUD_ACCESS_TOKEN="你的API密钥"' >> ~/.bash_profile
+
+
+ echo 'export GOOGLE_GENAI_USE_GCA="true"' >> ~/.bash_profile
+
+
+ source ~/.bash_profile
+
+
+
+
+
+
+ 验证 Gemini CLI 环境变量
+
+
+ 在 Terminal 中验证:
+
+
+
+ echo $CODE_ASSIST_ENDPOINT
+
+
+ echo $GOOGLE_CLOUD_ACCESS_TOKEN
+
+
+ echo $GOOGLE_GENAI_USE_GCA
+
+
+
+
+
@@ -987,6 +1165,105 @@
+
+
+
+
+
+ 配置 Gemini CLI 环境变量
+
+
+ 如果你使用 Gemini CLI,需要设置以下环境变量:
+
+
+
+
+
+ 终端设置方法
+
+
+ 在终端中运行以下命令:
+
+
+
+ export CODE_ASSIST_ENDPOINT="{{ geminiBaseUrl }}"
+
+
+ export GOOGLE_CLOUD_ACCESS_TOKEN="你的API密钥"
+
+
+ export GOOGLE_GENAI_USE_GCA="true"
+
+
+
+ 💡 使用与 Claude Code 相同的 API 密钥即可。
+
+
+
+
+
+ 永久设置方法
+
+
+ 添加到你的 shell 配置文件:
+
+
+
+ # 对于 bash (默认)
+
+
+ echo 'export CODE_ASSIST_ENDPOINT="{{ geminiBaseUrl }}"' >> ~/.bashrc
+
+
+ echo 'export GOOGLE_CLOUD_ACCESS_TOKEN="你的API密钥"' >> ~/.bashrc
+
+
+ echo 'export GOOGLE_GENAI_USE_GCA="true"' >> ~/.bashrc
+
+
+ source ~/.bashrc
+
+
+
+
+ # 对于 zsh
+
+
+ echo 'export CODE_ASSIST_ENDPOINT="{{ geminiBaseUrl }}"' >> ~/.zshrc
+
+
+ echo 'export GOOGLE_CLOUD_ACCESS_TOKEN="你的API密钥"' >> ~/.zshrc
+
+
+ echo 'export GOOGLE_GENAI_USE_GCA="true"' >> ~/.zshrc
+
+
+ source ~/.zshrc
+
+
+
+
+
+
+ 验证 Gemini CLI 环境变量
+
+
+ 在终端中验证:
+
+
+
+ echo $CODE_ASSIST_ENDPOINT
+
+
+ echo $GOOGLE_CLOUD_ACCESS_TOKEN
+
+
+ echo $GOOGLE_GENAI_USE_GCA
+
+
+
+
+
@@ -1130,8 +1407,8 @@ const tutorialSystems = [
{ key: 'linux', name: 'Linux / WSL2', icon: 'fab fa-linux' },
]
-// 当前基础URL
-const currentBaseUrl = computed(() => {
+// 获取基础URL前缀
+const getBaseUrlPrefix = () => {
// 更健壮的获取 origin 的方法,兼容旧版浏览器和特殊环境
let origin = ''
@@ -1163,11 +1440,21 @@ const currentBaseUrl = computed(() => {
} else {
// 最后的降级方案,使用相对路径
console.warn('无法获取完整的 origin,将使用相对路径')
- return '/api'
+ return ''
}
}
- return origin + '/api'
+ return origin
+}
+
+// 当前基础URL - Claude Code
+const currentBaseUrl = computed(() => {
+ return getBaseUrlPrefix() + '/api'
+})
+
+// Gemini CLI 基础URL
+const geminiBaseUrl = computed(() => {
+ return getBaseUrlPrefix() + '/gemini'
})