mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 09:38:02 +00:00
feat: 新增Droid cli支持
This commit is contained in:
@@ -5,6 +5,7 @@ const claudeConsoleAccountService = require('../services/claudeConsoleAccountSer
|
||||
const bedrockAccountService = require('../services/bedrockAccountService')
|
||||
const ccrAccountService = require('../services/ccrAccountService')
|
||||
const geminiAccountService = require('../services/geminiAccountService')
|
||||
const droidAccountService = require('../services/droidAccountService')
|
||||
const openaiAccountService = require('../services/openaiAccountService')
|
||||
const openaiResponsesAccountService = require('../services/openaiResponsesAccountService')
|
||||
const azureOpenaiAccountService = require('../services/azureOpenaiAccountService')
|
||||
@@ -13,6 +14,11 @@ const redis = require('../models/redis')
|
||||
const { authenticateAdmin } = require('../middleware/auth')
|
||||
const logger = require('../utils/logger')
|
||||
const oauthHelper = require('../utils/oauthHelper')
|
||||
const {
|
||||
startDeviceAuthorization,
|
||||
pollDeviceAuthorization,
|
||||
WorkOSDeviceAuthError
|
||||
} = require('../utils/workosOAuthHelper')
|
||||
const CostCalculator = require('../utils/costCalculator')
|
||||
const pricingService = require('../services/pricingService')
|
||||
const claudeCodeHeadersService = require('../services/claudeCodeHeadersService')
|
||||
@@ -8357,4 +8363,215 @@ router.post('/openai-responses-accounts/:id/reset-usage', authenticateAdmin, asy
|
||||
}
|
||||
})
|
||||
|
||||
// 🤖 Droid 账户管理
|
||||
|
||||
// 生成 Droid OAuth 授权链接
|
||||
router.post('/droid-accounts/generate-auth-url', authenticateAdmin, async (req, res) => {
|
||||
try {
|
||||
const { proxy } = req.body || {}
|
||||
const deviceAuth = await startDeviceAuthorization(proxy || null)
|
||||
|
||||
const sessionId = crypto.randomUUID()
|
||||
const expiresAt = new Date(Date.now() + deviceAuth.expiresIn * 1000).toISOString()
|
||||
|
||||
await redis.setOAuthSession(sessionId, {
|
||||
deviceCode: deviceAuth.deviceCode,
|
||||
userCode: deviceAuth.userCode,
|
||||
verificationUri: deviceAuth.verificationUri,
|
||||
verificationUriComplete: deviceAuth.verificationUriComplete,
|
||||
interval: deviceAuth.interval,
|
||||
proxy: proxy || null,
|
||||
createdAt: new Date().toISOString(),
|
||||
expiresAt
|
||||
})
|
||||
|
||||
logger.success('🤖 生成 Droid 设备码授权信息成功', { sessionId })
|
||||
return res.json({
|
||||
success: true,
|
||||
data: {
|
||||
sessionId,
|
||||
userCode: deviceAuth.userCode,
|
||||
verificationUri: deviceAuth.verificationUri,
|
||||
verificationUriComplete: deviceAuth.verificationUriComplete,
|
||||
expiresIn: deviceAuth.expiresIn,
|
||||
interval: deviceAuth.interval,
|
||||
instructions: [
|
||||
'1. 使用下方验证码进入授权页面并确认访问权限。',
|
||||
'2. 在授权页面登录 Factory / Droid 账户并点击允许。',
|
||||
'3. 回到此处点击“完成授权”完成凭证获取。'
|
||||
]
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof WorkOSDeviceAuthError ? error.message : error.message || '未知错误'
|
||||
logger.error('❌ 生成 Droid 设备码授权失败:', message)
|
||||
return res.status(500).json({ error: 'Failed to start Droid device authorization', message })
|
||||
}
|
||||
})
|
||||
|
||||
// 交换 Droid 授权码
|
||||
router.post('/droid-accounts/exchange-code', authenticateAdmin, async (req, res) => {
|
||||
const { sessionId, proxy } = req.body || {}
|
||||
try {
|
||||
if (!sessionId) {
|
||||
return res.status(400).json({ error: 'Session ID is required' })
|
||||
}
|
||||
|
||||
const oauthSession = await redis.getOAuthSession(sessionId)
|
||||
if (!oauthSession) {
|
||||
return res.status(400).json({ error: 'Invalid or expired OAuth session' })
|
||||
}
|
||||
|
||||
if (oauthSession.expiresAt && new Date() > new Date(oauthSession.expiresAt)) {
|
||||
await redis.deleteOAuthSession(sessionId)
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: 'OAuth session has expired, please generate a new authorization URL' })
|
||||
}
|
||||
|
||||
if (!oauthSession.deviceCode) {
|
||||
await redis.deleteOAuthSession(sessionId)
|
||||
return res.status(400).json({ error: 'OAuth session missing device code, please retry' })
|
||||
}
|
||||
|
||||
const proxyConfig = proxy || oauthSession.proxy || null
|
||||
const tokens = await pollDeviceAuthorization(oauthSession.deviceCode, proxyConfig)
|
||||
|
||||
await redis.deleteOAuthSession(sessionId)
|
||||
|
||||
logger.success('🤖 成功获取 Droid 访问令牌', { sessionId })
|
||||
return res.json({ success: true, data: { tokens } })
|
||||
} catch (error) {
|
||||
if (error instanceof WorkOSDeviceAuthError) {
|
||||
if (error.code === 'authorization_pending' || error.code === 'slow_down') {
|
||||
const oauthSession = await redis.getOAuthSession(sessionId)
|
||||
const expiresAt = oauthSession?.expiresAt ? new Date(oauthSession.expiresAt) : null
|
||||
const remainingSeconds =
|
||||
expiresAt instanceof Date && !Number.isNaN(expiresAt.getTime())
|
||||
? Math.max(0, Math.floor((expiresAt.getTime() - Date.now()) / 1000))
|
||||
: null
|
||||
|
||||
return res.json({
|
||||
success: false,
|
||||
pending: true,
|
||||
error: error.code,
|
||||
message: error.message,
|
||||
retryAfter: error.retryAfter || Number(oauthSession?.interval) || 5,
|
||||
expiresIn: remainingSeconds
|
||||
})
|
||||
}
|
||||
|
||||
if (error.code === 'expired_token') {
|
||||
await redis.deleteOAuthSession(sessionId)
|
||||
return res.status(400).json({
|
||||
error: 'Device code expired',
|
||||
message: '授权已过期,请重新生成设备码并再次授权'
|
||||
})
|
||||
}
|
||||
|
||||
logger.error('❌ Droid 授权失败:', error.message)
|
||||
return res.status(500).json({
|
||||
error: 'Failed to exchange Droid authorization code',
|
||||
message: error.message,
|
||||
errorCode: error.code
|
||||
})
|
||||
}
|
||||
|
||||
logger.error('❌ 交换 Droid 授权码失败:', error)
|
||||
return res.status(500).json({
|
||||
error: 'Failed to exchange Droid authorization code',
|
||||
message: error.message
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 获取所有 Droid 账户
|
||||
router.get('/droid-accounts', authenticateAdmin, async (req, res) => {
|
||||
try {
|
||||
const accounts = await droidAccountService.getAllAccounts()
|
||||
|
||||
// 添加使用统计
|
||||
const accountsWithStats = await Promise.all(
|
||||
accounts.map(async (account) => {
|
||||
try {
|
||||
const usageStats = await redis.getAccountUsageStats(account.id, 'droid')
|
||||
return {
|
||||
...account,
|
||||
schedulable: account.schedulable === 'true',
|
||||
usage: {
|
||||
daily: usageStats.daily,
|
||||
total: usageStats.total,
|
||||
averages: usageStats.averages
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to get stats for Droid account ${account.id}:`, error.message)
|
||||
return {
|
||||
...account,
|
||||
usage: {
|
||||
daily: { tokens: 0, requests: 0 },
|
||||
total: { tokens: 0, requests: 0 },
|
||||
averages: { rpm: 0, tpm: 0 }
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
return res.json({ success: true, data: accountsWithStats })
|
||||
} catch (error) {
|
||||
logger.error('Failed to get Droid accounts:', error)
|
||||
return res.status(500).json({ error: 'Failed to get Droid accounts', message: error.message })
|
||||
}
|
||||
})
|
||||
|
||||
// 创建 Droid 账户
|
||||
router.post('/droid-accounts', authenticateAdmin, async (req, res) => {
|
||||
try {
|
||||
const account = await droidAccountService.createAccount(req.body)
|
||||
logger.success(`Created Droid account: ${account.name} (${account.id})`)
|
||||
return res.json({ success: true, data: account })
|
||||
} catch (error) {
|
||||
logger.error('Failed to create Droid account:', error)
|
||||
return res.status(500).json({ error: 'Failed to create Droid account', message: error.message })
|
||||
}
|
||||
})
|
||||
|
||||
// 更新 Droid 账户
|
||||
router.put('/droid-accounts/:id', authenticateAdmin, async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params
|
||||
const account = await droidAccountService.updateAccount(id, req.body)
|
||||
return res.json({ success: true, data: account })
|
||||
} catch (error) {
|
||||
logger.error(`Failed to update Droid account ${req.params.id}:`, error)
|
||||
return res.status(500).json({ error: 'Failed to update Droid account', message: error.message })
|
||||
}
|
||||
})
|
||||
|
||||
// 删除 Droid 账户
|
||||
router.delete('/droid-accounts/:id', authenticateAdmin, async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params
|
||||
await droidAccountService.deleteAccount(id)
|
||||
return res.json({ success: true, message: 'Droid account deleted successfully' })
|
||||
} catch (error) {
|
||||
logger.error(`Failed to delete Droid account ${req.params.id}:`, error)
|
||||
return res.status(500).json({ error: 'Failed to delete Droid account', message: error.message })
|
||||
}
|
||||
})
|
||||
|
||||
// 刷新 Droid 账户 token
|
||||
router.post('/droid-accounts/:id/refresh-token', authenticateAdmin, async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params
|
||||
const result = await droidAccountService.refreshAccessToken(id)
|
||||
return res.json({ success: true, data: result })
|
||||
} catch (error) {
|
||||
logger.error(`Failed to refresh Droid account token ${req.params.id}:`, error)
|
||||
return res.status(500).json({ error: 'Failed to refresh token', message: error.message })
|
||||
}
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
|
||||
135
src/routes/droidRoutes.js
Normal file
135
src/routes/droidRoutes.js
Normal file
@@ -0,0 +1,135 @@
|
||||
const express = require('express')
|
||||
const { authenticateApiKey } = require('../middleware/auth')
|
||||
const droidRelayService = require('../services/droidRelayService')
|
||||
const logger = require('../utils/logger')
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
/**
|
||||
* Droid API 转发路由
|
||||
*
|
||||
* 支持多种 Factory.ai 端点:
|
||||
* - /droid/claude - Anthropic (Claude) Messages API
|
||||
* - /droid/openai - OpenAI Responses API
|
||||
* - /droid/chat - OpenAI Chat Completions API (通用)
|
||||
*/
|
||||
|
||||
// Claude (Anthropic) 端点 - /v1/messages
|
||||
router.post('/claude/v1/messages', authenticateApiKey, async (req, res) => {
|
||||
try {
|
||||
const result = await droidRelayService.relayRequest(
|
||||
req.body,
|
||||
req.apiKey,
|
||||
req,
|
||||
res,
|
||||
req.headers,
|
||||
{ endpointType: 'anthropic' }
|
||||
)
|
||||
|
||||
// 如果是流式响应,已经在 relayService 中处理了
|
||||
if (result.streaming) {
|
||||
return
|
||||
}
|
||||
|
||||
// 非流式响应
|
||||
res.status(result.statusCode).set(result.headers).send(result.body)
|
||||
} catch (error) {
|
||||
logger.error('Droid Claude relay error:', error)
|
||||
res.status(500).json({
|
||||
error: 'internal_server_error',
|
||||
message: error.message
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// OpenAI 端点 - /v1/responses
|
||||
router.post('/openai/v1/responses', authenticateApiKey, async (req, res) => {
|
||||
try {
|
||||
const result = await droidRelayService.relayRequest(
|
||||
req.body,
|
||||
req.apiKey,
|
||||
req,
|
||||
res,
|
||||
req.headers,
|
||||
{ endpointType: 'openai' }
|
||||
)
|
||||
|
||||
if (result.streaming) {
|
||||
return
|
||||
}
|
||||
|
||||
res.status(result.statusCode).set(result.headers).send(result.body)
|
||||
} catch (error) {
|
||||
logger.error('Droid OpenAI relay error:', error)
|
||||
res.status(500).json({
|
||||
error: 'internal_server_error',
|
||||
message: error.message
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 通用 OpenAI Chat Completions 端点
|
||||
router.post('/chat/v1/chat/completions', authenticateApiKey, async (req, res) => {
|
||||
try {
|
||||
const result = await droidRelayService.relayRequest(
|
||||
req.body,
|
||||
req.apiKey,
|
||||
req,
|
||||
res,
|
||||
req.headers,
|
||||
{ endpointType: 'common' }
|
||||
)
|
||||
|
||||
if (result.streaming) {
|
||||
return
|
||||
}
|
||||
|
||||
res.status(result.statusCode).set(result.headers).send(result.body)
|
||||
} catch (error) {
|
||||
logger.error('Droid Chat relay error:', error)
|
||||
res.status(500).json({
|
||||
error: 'internal_server_error',
|
||||
message: error.message
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 模型列表端点(兼容性)
|
||||
router.get('/*/v1/models', authenticateApiKey, async (req, res) => {
|
||||
try {
|
||||
// 返回可用的模型列表
|
||||
const models = [
|
||||
{
|
||||
id: 'claude-opus-4-1-20250805',
|
||||
object: 'model',
|
||||
created: Date.now(),
|
||||
owned_by: 'anthropic'
|
||||
},
|
||||
{
|
||||
id: 'claude-sonnet-4-5-20250929',
|
||||
object: 'model',
|
||||
created: Date.now(),
|
||||
owned_by: 'anthropic'
|
||||
},
|
||||
{
|
||||
id: 'gpt-5-2025-08-07',
|
||||
object: 'model',
|
||||
created: Date.now(),
|
||||
owned_by: 'openai'
|
||||
}
|
||||
]
|
||||
|
||||
res.json({
|
||||
object: 'list',
|
||||
data: models
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Droid models list error:', error)
|
||||
res.status(500).json({
|
||||
error: 'internal_server_error',
|
||||
message: error.message
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
Reference in New Issue
Block a user