mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
feat: 完善 Antigravity OAuth 功能与权限校验
新增功能: - 实现 Antigravity OAuth 账户支持与路径分流 - 支持 /antigravity/api 路径自动分流到 Antigravity OAuth 账户 - 支持 gemini-antigravity 平台类型的账户创建和管理 修复问题: - 修复 OAuthFlow 组件中 gemini-antigravity 平台授权页面空白的问题 - 修复 EditApiKeyModal 中 Redis 返回字符串格式 permissions 导致的 400 错误 - 统一使用 hasPermission 函数进行权限校验,支持数组格式 优化改进: - 添加 Antigravity 调试环境变量说明
This commit is contained in:
@@ -122,12 +122,18 @@ async function handleMessagesRequest(req, res) {
|
||||
try {
|
||||
const startTime = Date.now()
|
||||
|
||||
// Claude 服务权限校验,阻止未授权的 Key
|
||||
if (!apiKeyService.hasPermission(req.apiKey.permissions, 'claude')) {
|
||||
const forcedVendor = req._anthropicVendor || null
|
||||
const requiredService =
|
||||
forcedVendor === 'gemini-cli' || forcedVendor === 'antigravity' ? 'gemini' : 'claude'
|
||||
|
||||
if (!apiKeyService.hasPermission(req.apiKey?.permissions, requiredService)) {
|
||||
return res.status(403).json({
|
||||
error: {
|
||||
type: 'permission_error',
|
||||
message: '此 API Key 无权访问 Claude 服务'
|
||||
message:
|
||||
requiredService === 'gemini'
|
||||
? '此 API Key 无权访问 Gemini 服务'
|
||||
: '此 API Key 无权访问 Claude 服务'
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -176,7 +182,6 @@ async function handleMessagesRequest(req, res) {
|
||||
}
|
||||
}
|
||||
|
||||
const forcedVendor = req._anthropicVendor || null
|
||||
logger.api('📥 /v1/messages request received', {
|
||||
model: req.body.model || null,
|
||||
forcedVendor,
|
||||
@@ -192,34 +197,10 @@ async function handleMessagesRequest(req, res) {
|
||||
|
||||
// /v1/messages 的扩展:按路径强制分流到 Gemini OAuth 账户(避免 model 前缀混乱)
|
||||
if (forcedVendor === 'gemini-cli' || forcedVendor === 'antigravity') {
|
||||
const permissions = req.apiKey?.permissions || 'all'
|
||||
if (permissions !== 'all' && permissions !== 'gemini') {
|
||||
return res.status(403).json({
|
||||
error: {
|
||||
type: 'permission_error',
|
||||
message: '此 API Key 无权访问 Gemini 服务'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const baseModel = (req.body.model || '').trim()
|
||||
return await handleAnthropicMessagesToGemini(req, res, { vendor: forcedVendor, baseModel })
|
||||
}
|
||||
|
||||
// Claude 服务权限校验,阻止未授权的 Key(默认路径保持不变)
|
||||
if (
|
||||
req.apiKey.permissions &&
|
||||
req.apiKey.permissions !== 'all' &&
|
||||
req.apiKey.permissions !== 'claude'
|
||||
) {
|
||||
return res.status(403).json({
|
||||
error: {
|
||||
type: 'permission_error',
|
||||
message: '此 API Key 无权访问 Claude 服务'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 检查是否为流式请求
|
||||
const isStream = req.body.stream === true
|
||||
|
||||
@@ -1250,8 +1231,7 @@ router.get('/v1/models', authenticateApiKey, async (req, res) => {
|
||||
//(通过 v1internal:fetchAvailableModels),避免依赖静态 modelService 列表。
|
||||
const forcedVendor = req._anthropicVendor || null
|
||||
if (forcedVendor === 'antigravity') {
|
||||
const permissions = req.apiKey?.permissions || 'all'
|
||||
if (permissions !== 'all' && permissions !== 'gemini') {
|
||||
if (!apiKeyService.hasPermission(req.apiKey?.permissions, 'gemini')) {
|
||||
return res.status(403).json({
|
||||
error: {
|
||||
type: 'permission_error',
|
||||
@@ -1444,34 +1424,25 @@ router.get('/v1/organizations/:org_id/usage', authenticateApiKey, async (req, re
|
||||
router.post('/v1/messages/count_tokens', authenticateApiKey, async (req, res) => {
|
||||
// 按路径强制分流到 Gemini OAuth 账户(避免 model 前缀混乱)
|
||||
const forcedVendor = req._anthropicVendor || null
|
||||
if (forcedVendor === 'gemini-cli' || forcedVendor === 'antigravity') {
|
||||
const permissions = req.apiKey?.permissions || 'all'
|
||||
if (permissions !== 'all' && permissions !== 'gemini') {
|
||||
return res.status(403).json({
|
||||
error: {
|
||||
type: 'permission_error',
|
||||
message: 'This API key does not have permission to access Gemini'
|
||||
}
|
||||
})
|
||||
}
|
||||
const requiredService =
|
||||
forcedVendor === 'gemini-cli' || forcedVendor === 'antigravity' ? 'gemini' : 'claude'
|
||||
|
||||
return await handleAnthropicCountTokensToGemini(req, res, { vendor: forcedVendor })
|
||||
}
|
||||
|
||||
// 检查权限
|
||||
if (
|
||||
req.apiKey.permissions &&
|
||||
req.apiKey.permissions !== 'all' &&
|
||||
req.apiKey.permissions !== 'claude'
|
||||
) {
|
||||
if (!apiKeyService.hasPermission(req.apiKey?.permissions, requiredService)) {
|
||||
return res.status(403).json({
|
||||
error: {
|
||||
type: 'permission_error',
|
||||
message: 'This API key does not have permission to access Claude'
|
||||
message:
|
||||
requiredService === 'gemini'
|
||||
? 'This API key does not have permission to access Gemini'
|
||||
: 'This API key does not have permission to access Claude'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (requiredService === 'gemini') {
|
||||
return await handleAnthropicCountTokensToGemini(req, res, { vendor: forcedVendor })
|
||||
}
|
||||
|
||||
// 🔗 会话绑定验证(与 messages 端点保持一致)
|
||||
const originalSessionId = claudeRelayConfigService.extractOriginalSessionId(req.body)
|
||||
const sessionValidation = await claudeRelayConfigService.validateNewSession(
|
||||
|
||||
@@ -46,11 +46,11 @@ async function routeToBackend(req, res, requestedModel) {
|
||||
logger.info(`🔀 Routing request - Model: ${requestedModel}, Backend: ${backend}`)
|
||||
|
||||
// 检查权限
|
||||
const permissions = req.apiKey.permissions || 'all'
|
||||
const { permissions } = req.apiKey
|
||||
|
||||
if (backend === 'claude') {
|
||||
// Claude 后端:通过 OpenAI 兼容层
|
||||
if (permissions !== 'all' && permissions !== 'claude') {
|
||||
if (!apiKeyService.hasPermission(permissions, 'claude')) {
|
||||
return res.status(403).json({
|
||||
error: {
|
||||
message: 'This API key does not have permission to access Claude',
|
||||
@@ -62,7 +62,7 @@ async function routeToBackend(req, res, requestedModel) {
|
||||
await handleChatCompletion(req, res, req.apiKey)
|
||||
} else if (backend === 'openai') {
|
||||
// OpenAI 后端
|
||||
if (permissions !== 'all' && permissions !== 'openai') {
|
||||
if (!apiKeyService.hasPermission(permissions, 'openai')) {
|
||||
return res.status(403).json({
|
||||
error: {
|
||||
message: 'This API key does not have permission to access OpenAI',
|
||||
|
||||
@@ -21,7 +21,10 @@ const SYSTEM_REMINDER_PREFIX = '<system-reminder>'
|
||||
const TOOLS_DUMP_ENV = 'ANTHROPIC_DEBUG_TOOLS_DUMP'
|
||||
const TOOLS_DUMP_FILENAME = 'anthropic-tools-dump.jsonl'
|
||||
const TEXT_TOOL_FALLBACK_ENV = 'ANTHROPIC_TEXT_TOOL_FALLBACK'
|
||||
const TOOL_ERROR_CONTINUE_ENV = 'ANTHROPIC_TOOL_ERROR_CONTINUE'
|
||||
const THOUGHT_SIGNATURE_FALLBACK = 'skip_thought_signature_validator'
|
||||
const TOOL_ERROR_CONTINUE_PROMPT =
|
||||
'Tool calls may fail (e.g., missing prerequisites). When a tool result indicates an error, do not stop: briefly explain the cause and continue with an alternative approach or the remaining steps.'
|
||||
|
||||
function ensureAntigravityProjectId(account) {
|
||||
if (account.projectId) {
|
||||
@@ -710,12 +713,13 @@ function convertAnthropicMessagesToGeminiContents(
|
||||
if (vendor === 'antigravity') {
|
||||
const toolCallId = typeof toolUseId === 'string' && toolUseId ? toolUseId : undefined
|
||||
const result = parsedResponse !== null ? parsedResponse : raw || ''
|
||||
const response = part.is_error === true ? { result, is_error: true } : { result }
|
||||
|
||||
parts.push({
|
||||
functionResponse: {
|
||||
...(toolCallId ? { id: toolCallId } : {}),
|
||||
name: toolName,
|
||||
response: { result }
|
||||
response
|
||||
}
|
||||
})
|
||||
} else {
|
||||
@@ -761,6 +765,10 @@ function buildGeminiRequestFromAnthropic(body, baseModel, { vendor = null } = {}
|
||||
)
|
||||
const systemParts = buildSystemParts(body.system)
|
||||
|
||||
if (vendor === 'antigravity' && isEnvEnabled(process.env[TOOL_ERROR_CONTINUE_ENV])) {
|
||||
systemParts.push({ text: TOOL_ERROR_CONTINUE_PROMPT })
|
||||
}
|
||||
|
||||
const temperature = typeof body.temperature === 'number' ? body.temperature : 1
|
||||
const maxTokens = Number.isFinite(body.max_tokens) ? body.max_tokens : 4096
|
||||
|
||||
|
||||
Reference in New Issue
Block a user