feat: 增强 Gemini 服务支持并添加统一调度器

- 新增 unifiedGeminiScheduler.js 统一账户调度服务
- 增强 geminiRoutes.js 支持更多 Gemini API 端点
- 优化 geminiAccountService.js 账户管理和 token 刷新机制
- 添加对 v1internal 端点的完整支持(loadCodeAssist、onboardUser、countTokens、generateContent、streamGenerateContent)
- 改进错误处理和流式响应管理

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
千羽
2025-08-04 14:47:03 +09:00
parent 6d27dd7c94
commit 33837c23aa
3 changed files with 1100 additions and 128 deletions

View File

@@ -53,7 +53,7 @@ function decrypt(text) {
// IV 是固定长度的 32 个十六进制字符16 字节)
const ivHex = text.substring(0, 32);
const encryptedHex = text.substring(33); // 跳过冒号
const iv = Buffer.from(ivHex, 'hex');
const encryptedText = Buffer.from(encryptedHex, 'hex');
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
@@ -82,11 +82,11 @@ async function generateAuthUrl(state = null, redirectUri = null) {
// 使用新的 redirect URI
const finalRedirectUri = redirectUri || 'https://codeassist.google.com/authcode';
const oAuth2Client = createOAuth2Client(finalRedirectUri);
// 生成 PKCE code verifier
const codeVerifier = await oAuth2Client.generateCodeVerifierAsync();
const stateValue = state || crypto.randomBytes(32).toString('hex');
const authUrl = oAuth2Client.generateAuthUrl({
redirect_uri: finalRedirectUri,
access_type: 'offline',
@@ -96,7 +96,7 @@ async function generateAuthUrl(state = null, redirectUri = null) {
state: stateValue,
prompt: 'select_account'
});
return {
authUrl,
state: stateValue,
@@ -109,28 +109,28 @@ async function generateAuthUrl(state = null, redirectUri = null) {
async function pollAuthorizationStatus(sessionId, maxAttempts = 60, interval = 2000) {
let attempts = 0;
const client = redisClient.getClientSafe();
while (attempts < maxAttempts) {
try {
const sessionData = await client.get(`oauth_session:${sessionId}`);
if (!sessionData) {
throw new Error('OAuth session not found');
}
const session = JSON.parse(sessionData);
if (session.code) {
// 授权码已获取,交换 tokens
const tokens = await exchangeCodeForTokens(session.code);
// 清理 session
await client.del(`oauth_session:${sessionId}`);
return {
success: true,
tokens
};
}
if (session.error) {
// 授权失败
await client.del(`oauth_session:${sessionId}`);
@@ -139,7 +139,7 @@ async function pollAuthorizationStatus(sessionId, maxAttempts = 60, interval = 2
error: session.error
};
}
// 等待下一次轮询
await new Promise(resolve => setTimeout(resolve, interval));
attempts++;
@@ -148,7 +148,7 @@ async function pollAuthorizationStatus(sessionId, maxAttempts = 60, interval = 2
throw error;
}
}
// 超时
await client.del(`oauth_session:${sessionId}`);
return {
@@ -160,20 +160,20 @@ async function pollAuthorizationStatus(sessionId, maxAttempts = 60, interval = 2
// 交换授权码获取 tokens (支持 PKCE)
async function exchangeCodeForTokens(code, redirectUri = null, codeVerifier = null) {
const oAuth2Client = createOAuth2Client(redirectUri);
try {
const tokenParams = {
code: code,
redirect_uri: redirectUri
};
// 如果提供了 codeVerifier添加到参数中
if (codeVerifier) {
tokenParams.codeVerifier = codeVerifier;
}
const { tokens } = await oAuth2Client.getToken(tokenParams);
// 转换为兼容格式
return {
access_token: tokens.access_token,
@@ -191,24 +191,24 @@ async function exchangeCodeForTokens(code, redirectUri = null, codeVerifier = nu
// 刷新访问令牌
async function refreshAccessToken(refreshToken) {
const oAuth2Client = createOAuth2Client();
try {
// 设置 refresh_token
oAuth2Client.setCredentials({
refresh_token: refreshToken
});
// 调用 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 如果没有返回新的
@@ -230,34 +230,34 @@ async function refreshAccessToken(refreshToken) {
async function createAccount(accountData) {
const id = uuidv4();
const now = new Date().toISOString();
// 处理凭证数据
let geminiOauth = null;
let accessToken = '';
let refreshToken = '';
let expiresAt = '';
if (accountData.geminiOauth || accountData.accessToken) {
// 如果提供了完整的 OAuth 数据
if (accountData.geminiOauth) {
geminiOauth = typeof accountData.geminiOauth === 'string'
? accountData.geminiOauth
geminiOauth = typeof accountData.geminiOauth === 'string'
? accountData.geminiOauth
: JSON.stringify(accountData.geminiOauth);
const oauthData = typeof accountData.geminiOauth === 'string'
const oauthData = typeof accountData.geminiOauth === 'string'
? JSON.parse(accountData.geminiOauth)
: accountData.geminiOauth;
accessToken = oauthData.access_token || '';
refreshToken = oauthData.refresh_token || '';
expiresAt = oauthData.expiry_date
expiresAt = oauthData.expiry_date
? new Date(oauthData.expiry_date).toISOString()
: '';
} else {
// 如果只提供了 access token
accessToken = accountData.accessToken;
refreshToken = accountData.refreshToken || '';
// 构造完整的 OAuth 数据
geminiOauth = JSON.stringify({
access_token: accessToken,
@@ -266,11 +266,11 @@ async function createAccount(accountData) {
token_type: accountData.tokenType || 'Bearer',
expiry_date: accountData.expiryDate || Date.now() + 3600000 // 默认1小时
});
expiresAt = new Date(accountData.expiryDate || Date.now() + 3600000).toISOString();
}
}
const account = {
id,
platform: 'gemini', // 标识为 Gemini 账户
@@ -279,39 +279,39 @@ async function createAccount(accountData) {
accountType: accountData.accountType || 'shared',
isActive: 'true',
status: 'active',
// OAuth 相关字段(加密存储)
geminiOauth: geminiOauth ? encrypt(geminiOauth) : '',
accessToken: accessToken ? encrypt(accessToken) : '',
refreshToken: refreshToken ? encrypt(refreshToken) : '',
expiresAt,
scopes: accountData.scopes || OAUTH_SCOPES.join(' '),
// 代理设置
proxy: accountData.proxy ? JSON.stringify(accountData.proxy) : '',
// 项目编号Google Cloud/Workspace 账号需要)
projectId: accountData.projectId || '',
// 时间戳
createdAt: now,
updatedAt: now,
lastUsedAt: '',
lastRefreshAt: ''
};
// 保存到 Redis
const client = redisClient.getClientSafe();
await client.hset(
`${GEMINI_ACCOUNT_KEY_PREFIX}${id}`,
account
);
// 如果是共享账户,添加到共享账户集合
if (account.accountType === 'shared') {
await client.sadd(SHARED_GEMINI_ACCOUNTS_KEY, id);
}
logger.info(`Created Gemini account: ${id}`);
return account;
}
@@ -320,11 +320,11 @@ async function createAccount(accountData) {
async function getAccount(accountId) {
const client = redisClient.getClientSafe();
const accountData = await client.hgetall(`${GEMINI_ACCOUNT_KEY_PREFIX}${accountId}`);
if (!accountData || Object.keys(accountData).length === 0) {
return null;
}
// 解密敏感字段
if (accountData.geminiOauth) {
accountData.geminiOauth = decrypt(accountData.geminiOauth);
@@ -335,7 +335,7 @@ async function getAccount(accountId) {
if (accountData.refreshToken) {
accountData.refreshToken = decrypt(accountData.refreshToken);
}
return accountData;
}
@@ -345,20 +345,20 @@ async function updateAccount(accountId, updates) {
if (!existingAccount) {
throw new Error('Account not found');
}
const now = new Date().toISOString();
updates.updatedAt = now;
// 检查是否新增了 refresh token
// existingAccount.refreshToken 已经是解密后的值了(从 getAccount 返回)
const oldRefreshToken = existingAccount.refreshToken || '';
let needUpdateExpiry = false;
// 加密敏感字段
if (updates.geminiOauth) {
updates.geminiOauth = encrypt(
typeof updates.geminiOauth === 'string'
? updates.geminiOauth
typeof updates.geminiOauth === 'string'
? updates.geminiOauth
: JSON.stringify(updates.geminiOauth)
);
}
@@ -372,7 +372,7 @@ async function updateAccount(accountId, updates) {
needUpdateExpiry = true;
}
}
// 更新账户类型时处理共享账户集合
const client = redisClient.getClientSafe();
if (updates.accountType && updates.accountType !== existingAccount.accountType) {
@@ -382,26 +382,26 @@ async function updateAccount(accountId, updates) {
await client.srem(SHARED_GEMINI_ACCOUNTS_KEY, accountId);
}
}
// 如果新增了 refresh token更新过期时间为10分钟
if (needUpdateExpiry) {
const newExpiry = new Date(Date.now() + (10 * 60 * 1000)).toISOString();
updates.expiresAt = newExpiry;
logger.info(`🔄 New refresh token added for Gemini account ${accountId}, setting expiry to 10 minutes`);
}
// 如果通过 geminiOauth 更新,也要检查是否新增了 refresh token
if (updates.geminiOauth && !oldRefreshToken) {
const oauthData = typeof updates.geminiOauth === 'string'
const oauthData = typeof updates.geminiOauth === 'string'
? JSON.parse(decrypt(updates.geminiOauth))
: updates.geminiOauth;
if (oauthData.refresh_token) {
// 如果 expiry_date 设置的时间过长超过1小时调整为10分钟
const providedExpiry = oauthData.expiry_date || 0;
const now = Date.now();
const oneHour = 60 * 60 * 1000;
if (providedExpiry - now > oneHour) {
const newExpiry = new Date(now + (10 * 60 * 1000)).toISOString();
updates.expiresAt = newExpiry;
@@ -409,12 +409,12 @@ async function updateAccount(accountId, updates) {
}
}
}
await client.hset(
`${GEMINI_ACCOUNT_KEY_PREFIX}${accountId}`,
updates
);
logger.info(`Updated Gemini account: ${accountId}`);
return { ...existingAccount, ...updates };
}
@@ -425,16 +425,16 @@ async function deleteAccount(accountId) {
if (!account) {
throw new Error('Account not found');
}
// 从 Redis 删除
const client = redisClient.getClientSafe();
await client.del(`${GEMINI_ACCOUNT_KEY_PREFIX}${accountId}`);
// 从共享账户集合中移除
if (account.accountType === 'shared') {
await client.srem(SHARED_GEMINI_ACCOUNTS_KEY, accountId);
}
// 清理会话映射
const sessionMappings = await client.keys(`${ACCOUNT_SESSION_MAPPING_PREFIX}*`);
for (const key of sessionMappings) {
@@ -443,7 +443,7 @@ async function deleteAccount(accountId) {
await client.del(key);
}
}
logger.info(`Deleted Gemini account: ${accountId}`);
return true;
}
@@ -453,7 +453,7 @@ async function getAllAccounts() {
const client = redisClient.getClientSafe();
const keys = await client.keys(`${GEMINI_ACCOUNT_KEY_PREFIX}*`);
const accounts = [];
for (const key of keys) {
const accountData = await client.hgetall(key);
if (accountData && Object.keys(accountData).length > 0) {
@@ -466,7 +466,7 @@ async function getAllAccounts() {
});
}
}
return accounts;
}
@@ -478,7 +478,7 @@ async function selectAvailableAccount(apiKeyId, sessionHash = null) {
const mappedAccountId = await client.get(
`${ACCOUNT_SESSION_MAPPING_PREFIX}${sessionHash}`
);
if (mappedAccountId) {
const account = await getAccount(mappedAccountId);
if (account && account.isActive === 'true' && !isTokenExpired(account)) {
@@ -487,25 +487,25 @@ async function selectAvailableAccount(apiKeyId, sessionHash = null) {
}
}
}
// 获取 API Key 信息
const apiKeyData = await client.hgetall(`api_key:${apiKeyId}`);
// 检查是否绑定了 Gemini 账户
if (apiKeyData.geminiAccountId) {
const account = await getAccount(apiKeyData.geminiAccountId);
if (account && account.isActive === 'true') {
// 检查 token 是否过期
const isExpired = isTokenExpired(account);
// 记录token使用情况
logTokenUsage(account.id, account.name, 'gemini', account.expiresAt, isExpired);
if (isExpired) {
await refreshAccountToken(account.id);
return await getAccount(account.id);
}
// 创建粘性会话映射
if (sessionHash) {
await client.setex(
@@ -514,46 +514,46 @@ async function selectAvailableAccount(apiKeyId, sessionHash = null) {
account.id
);
}
return account;
}
}
// 从共享账户池选择
const sharedAccountIds = await client.smembers(SHARED_GEMINI_ACCOUNTS_KEY);
const availableAccounts = [];
for (const accountId of sharedAccountIds) {
const account = await getAccount(accountId);
if (account && account.isActive === 'true' && !isRateLimited(account)) {
availableAccounts.push(account);
}
}
if (availableAccounts.length === 0) {
throw new Error('No available Gemini accounts');
}
// 选择最少使用的账户
availableAccounts.sort((a, b) => {
const aLastUsed = a.lastUsedAt ? new Date(a.lastUsedAt).getTime() : 0;
const bLastUsed = b.lastUsedAt ? new Date(b.lastUsedAt).getTime() : 0;
return aLastUsed - bLastUsed;
});
const selectedAccount = availableAccounts[0];
// 检查并刷新 token
const isExpired = isTokenExpired(selectedAccount);
// 记录token使用情况
logTokenUsage(selectedAccount.id, selectedAccount.name, 'gemini', selectedAccount.expiresAt, isExpired);
if (isExpired) {
await refreshAccountToken(selectedAccount.id);
return await getAccount(selectedAccount.id);
}
// 创建粘性会话映射
if (sessionHash) {
await client.setex(
@@ -562,18 +562,18 @@ async function selectAvailableAccount(apiKeyId, sessionHash = null) {
selectedAccount.id
);
}
return selectedAccount;
}
// 检查 token 是否过期
function isTokenExpired(account) {
if (!account.expiresAt) return true;
const expiryTime = new Date(account.expiresAt).getTime();
const now = Date.now();
const buffer = 10 * 1000; // 10秒缓冲
return now >= (expiryTime - buffer);
}
@@ -583,7 +583,7 @@ function isRateLimited(account) {
const limitedAt = new Date(account.rateLimitedAt).getTime();
const now = Date.now();
const limitDuration = 60 * 60 * 1000; // 1小时
return now < (limitedAt + limitDuration);
}
return false;
@@ -593,28 +593,28 @@ function isRateLimited(account) {
async function refreshAccountToken(accountId) {
let lockAcquired = false;
let account = null;
try {
account = await getAccount(accountId);
if (!account) {
throw new Error('Account not found');
}
if (!account.refreshToken) {
throw new Error('No refresh token available');
}
// 尝试获取分布式锁
lockAcquired = await tokenRefreshService.acquireRefreshLock(accountId, 'gemini');
if (!lockAcquired) {
// 如果无法获取锁,说明另一个进程正在刷新
logger.info(`🔒 Token refresh already in progress for Gemini account: ${account.name} (${accountId})`);
logRefreshSkipped(accountId, account.name, 'gemini', 'already_locked');
// 等待一段时间后返回,期望其他进程已完成刷新
await new Promise(resolve => setTimeout(resolve, 2000));
// 重新获取账户数据(可能已被其他进程刷新)
const updatedAccount = await getAccount(accountId);
if (updatedAccount && updatedAccount.accessToken) {
@@ -627,17 +627,17 @@ async function refreshAccountToken(accountId) {
token_type: 'Bearer'
};
}
throw new Error('Token refresh in progress by another process');
}
// 记录开始刷新
logRefreshStart(accountId, account.name, 'gemini', 'manual_refresh');
logger.info(`🔄 Starting token refresh for Gemini account: ${account.name} (${accountId})`);
// account.refreshToken 已经是解密后的值(从 getAccount 返回)
const newTokens = await refreshAccessToken(account.refreshToken);
// 更新账户信息
const updates = {
accessToken: newTokens.access_token,
@@ -648,9 +648,9 @@ async function refreshAccountToken(accountId) {
status: 'active', // 刷新成功后,将状态更新为 active
errorMessage: '' // 清空错误信息
};
await updateAccount(accountId, updates);
// 记录刷新成功
logRefreshSuccess(accountId, account.name, 'gemini', {
accessToken: newTokens.access_token,
@@ -658,16 +658,16 @@ async function refreshAccountToken(accountId) {
expiresAt: newTokens.expiry_date,
scopes: newTokens.scope
});
logger.info(`Refreshed token for Gemini account: ${accountId} - Access Token: ${maskToken(newTokens.access_token)}`);
return newTokens;
} catch (error) {
// 记录刷新失败
logRefreshError(accountId, account ? account.name : 'Unknown', 'gemini', error);
logger.error(`Failed to refresh token for account ${accountId}:`, error);
// 标记账户为错误状态(只有在账户存在时)
if (account) {
try {
@@ -679,7 +679,7 @@ async function refreshAccountToken(accountId) {
logger.error('Failed to update account status after refresh error:', updateError);
}
}
throw error;
} finally {
// 释放锁
@@ -705,10 +705,340 @@ async function setAccountRateLimited(accountId, isLimited = true) {
rateLimitStatus: '',
rateLimitedAt: ''
};
await updateAccount(accountId, updates);
}
// 获取配置的OAuth客户端 - 参考GeminiCliSimulator的getOauthClient方法
async function getOauthClient(accessToken, refreshToken) {
const client = new OAuth2Client({
clientId: OAUTH_CLIENT_ID,
clientSecret: OAUTH_CLIENT_SECRET,
});
const creds = {
'access_token': accessToken,
'refresh_token': refreshToken,
'scope': 'https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/userinfo.profile openid https://www.googleapis.com/auth/userinfo.email',
'token_type': 'Bearer',
'expiry_date': 1754269905646
};
// 设置凭据
client.setCredentials(creds);
// 验证凭据本地有效性
const { token } = await client.getAccessToken();
if (!token) {
return false;
}
// 验证服务器端token状态检查是否被撤销
await client.getTokenInfo(token);
logger.info('✅ OAuth客户端已创建');
return client;
}
// 调用 Google Code Assist API 的 loadCodeAssist 方法
async function loadCodeAssist(client, projectId = null) {
const axios = require('axios');
const CODE_ASSIST_ENDPOINT = 'https://cloudcode-pa.googleapis.com';
const CODE_ASSIST_API_VERSION = 'v1internal';
const { token } = await client.getAccessToken();
// 创建ClientMetadata
const clientMetadata = {
ideType: 'IDE_UNSPECIFIED',
platform: 'PLATFORM_UNSPECIFIED',
pluginType: 'GEMINI',
duetProject: projectId,
};
const request = {
cloudaicompanionProject: projectId,
metadata: clientMetadata,
};
const response = await axios({
url: `${CODE_ASSIST_ENDPOINT}/${CODE_ASSIST_API_VERSION}:loadCodeAssist`,
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
data: request,
timeout: 30000,
});
logger.info('📋 loadCodeAssist API调用成功');
return response.data;
}
// 获取onboard层级 - 参考GeminiCliSimulator的getOnboardTier方法
function getOnboardTier(loadRes) {
// 用户层级枚举
const UserTierId = {
LEGACY: 'LEGACY',
FREE: 'FREE',
PRO: 'PRO'
};
if (loadRes.currentTier) {
return loadRes.currentTier;
}
for (const tier of loadRes.allowedTiers || []) {
if (tier.isDefault) {
return tier;
}
}
return {
name: '',
description: '',
id: UserTierId.LEGACY,
userDefinedCloudaicompanionProject: true,
};
}
// 调用 Google Code Assist API 的 onboardUser 方法(包含轮询逻辑)
async function onboardUser(client, tierId, projectId, clientMetadata) {
const axios = require('axios');
const CODE_ASSIST_ENDPOINT = 'https://cloudcode-pa.googleapis.com';
const CODE_ASSIST_API_VERSION = 'v1internal';
const { token } = await client.getAccessToken();
const onboardReq = {
tierId: tierId,
cloudaicompanionProject: projectId,
metadata: clientMetadata,
};
logger.info('📋 开始onboardUser API调用', { tierId, projectId });
// 轮询onboardUser直到长运行操作完成
let lroRes = await axios({
url: `${CODE_ASSIST_ENDPOINT}/${CODE_ASSIST_API_VERSION}:onboardUser`,
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
data: onboardReq,
timeout: 30000,
});
let attempts = 0;
const maxAttempts = 12; // 最多等待1分钟5秒 * 12次
while (!lroRes.data.done && attempts < maxAttempts) {
logger.info(`⏳ 等待onboardUser完成... (${attempts + 1}/${maxAttempts})`);
await new Promise(resolve => setTimeout(resolve, 5000));
lroRes = await axios({
url: `${CODE_ASSIST_ENDPOINT}/${CODE_ASSIST_API_VERSION}:onboardUser`,
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
data: onboardReq,
timeout: 30000,
});
attempts++;
}
if (!lroRes.data.done) {
throw new Error('onboardUser操作超时');
}
logger.info('✅ onboardUser API调用完成');
return lroRes.data;
}
// 完整的用户设置流程 - 参考setup.ts的逻辑
async function setupUser(client, initialProjectId = null, clientMetadata = null) {
logger.info('🚀 setupUser 开始', { initialProjectId, hasClientMetadata: !!clientMetadata });
let projectId = initialProjectId || process.env.GOOGLE_CLOUD_PROJECT || null;
logger.info('📋 初始项目ID', { projectId, fromEnv: !!process.env.GOOGLE_CLOUD_PROJECT });
// 默认的ClientMetadata
if (!clientMetadata) {
clientMetadata = {
ideType: 'IDE_UNSPECIFIED',
platform: 'PLATFORM_UNSPECIFIED',
pluginType: 'GEMINI',
duetProject: projectId,
};
logger.info('🔧 使用默认 ClientMetadata');
}
// 调用loadCodeAssist
logger.info('📞 调用 loadCodeAssist...');
const loadRes = await loadCodeAssist(client, projectId);
logger.info('✅ loadCodeAssist 完成', { hasCloudaicompanionProject: !!loadRes.cloudaicompanionProject });
// 如果没有projectId尝试从loadRes获取
if (!projectId && loadRes.cloudaicompanionProject) {
projectId = loadRes.cloudaicompanionProject;
logger.info('📋 从 loadCodeAssist 获取项目ID', { projectId });
}
const tier = getOnboardTier(loadRes);
logger.info('🎯 获取用户层级', { tierId: tier.id, userDefinedProject: tier.userDefinedCloudaicompanionProject });
if (tier.userDefinedCloudaiCompanionProject && !projectId) {
throw new Error('此账号需要设置GOOGLE_CLOUD_PROJECT环境变量或提供projectId');
}
// 调用onboardUser
logger.info('📞 调用 onboardUser...', { tierId: tier.id, projectId });
const lroRes = await onboardUser(client, tier.id, projectId, clientMetadata);
logger.info('✅ onboardUser 完成', { hasDone: !!lroRes.done, hasResponse: !!lroRes.response });
const result = {
projectId: lroRes.response?.cloudaicompanionProject?.id || projectId || '',
userTier: tier.id,
loadRes,
onboardRes: lroRes.response || {}
};
logger.info('🎯 setupUser 完成', { resultProjectId: result.projectId, userTier: result.userTier });
return result;
}
// 调用 Code Assist API 计算 token 数量
async function countTokens(client, contents, model = 'gemini-2.0-flash-exp') {
const axios = require('axios');
const CODE_ASSIST_ENDPOINT = 'https://cloudcode-pa.googleapis.com';
const CODE_ASSIST_API_VERSION = 'v1internal';
const { token } = await client.getAccessToken();
// 按照 gemini-cli 的转换格式构造请求
const request = {
request: {
model: `models/${model}`,
contents: contents
}
};
logger.info('📊 countTokens API调用开始', { model, contentsLength: contents.length });
const response = await axios({
url: `${CODE_ASSIST_ENDPOINT}/${CODE_ASSIST_API_VERSION}:countTokens`,
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
data: request,
timeout: 30000,
});
logger.info('✅ countTokens API调用成功', { totalTokens: response.data.totalTokens });
return response.data;
}
// 调用 Code Assist API 生成内容(非流式)
async function generateContent(client, requestData, userPromptId, projectId = null, sessionId = null) {
const axios = require('axios');
const CODE_ASSIST_ENDPOINT = 'https://cloudcode-pa.googleapis.com';
const CODE_ASSIST_API_VERSION = 'v1internal';
const { token } = await client.getAccessToken();
// 按照 gemini-cli 的转换格式构造请求
const request = {
model: requestData.model,
project: projectId,
user_prompt_id: userPromptId,
request: {
...requestData.request,
session_id: sessionId
}
};
logger.info('🤖 generateContent API调用开始', {
model: requestData.model,
userPromptId,
projectId,
sessionId
});
const response = await axios({
url: `${CODE_ASSIST_ENDPOINT}/${CODE_ASSIST_API_VERSION}:generateContent`,
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
data: request,
timeout: 60000, // 生成内容可能需要更长时间
});
logger.info('✅ generateContent API调用成功');
return response.data;
}
// 调用 Code Assist API 生成内容(流式)
async function generateContentStream(client, requestData, userPromptId, projectId = null, sessionId = null, signal = null) {
const axios = require('axios');
const CODE_ASSIST_ENDPOINT = 'https://cloudcode-pa.googleapis.com';
const CODE_ASSIST_API_VERSION = 'v1internal';
const { token } = await client.getAccessToken();
// 按照 gemini-cli 的转换格式构造请求
const request = {
model: requestData.model,
project: projectId,
user_prompt_id: userPromptId,
request: {
...requestData.request,
session_id: sessionId
}
};
logger.info('🌊 streamGenerateContent API调用开始', {
model: requestData.model,
userPromptId,
projectId,
sessionId
});
const axiosConfig = {
url: `${CODE_ASSIST_ENDPOINT}/${CODE_ASSIST_API_VERSION}:streamGenerateContent`,
method: 'POST',
params: {
alt: 'sse'
},
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
data: request,
responseType: 'stream',
timeout: 60000,
};
// 如果提供了中止信号,添加到配置中
if (signal) {
axiosConfig.signal = signal;
}
const response = await axios(axiosConfig);
logger.info('✅ streamGenerateContent API调用成功开始流式传输');
return response.data; // 返回流对象
}
module.exports = {
generateAuthUrl,
pollAuthorizationStatus,
@@ -724,6 +1054,14 @@ module.exports = {
markAccountUsed,
setAccountRateLimited,
isTokenExpired,
getOauthClient,
loadCodeAssist,
getOnboardTier,
onboardUser,
setupUser,
countTokens,
generateContent,
generateContentStream,
OAUTH_CLIENT_ID,
OAUTH_SCOPES
};