feat: Codex账号管理优化与API Key激活机制

 新功能
- 支持通过refreshToken新增Codex账号,创建时立即验证token有效性
- API Key新增首次使用自动激活机制,支持activation模式设置有效期
- 前端账号表单增加token验证功能,确保账号创建成功

🐛 修复
- 修复Codex token刷新失败问题,增加分布式锁防止并发刷新
- 优化token刷新错误处理,提供更详细的错误信息和建议
- 修复OpenAI账号token过期检测和自动刷新逻辑

📝 文档更新
- 更新README中Codex使用说明,改为config.toml配置方式
- 优化Cherry Studio等第三方工具接入文档
- 添加详细的配置示例和账号类型说明

🎨 界面优化
- 改进账号创建表单UI,支持手动和OAuth两种模式
- 优化API Key过期时间编辑弹窗,支持激活操作
- 调整教程页面布局,提升移动端响应式体验

💡 代码改进
- 重构token刷新服务,增强错误处理和重试机制
- 优化代理配置处理,确保OAuth请求正确使用代理
- 改进webhook通知,增加token刷新失败告警
This commit is contained in:
shaw
2025-09-06 17:39:05 +08:00
parent 0e746b1056
commit d2f3f6866c
19 changed files with 1231 additions and 463 deletions

View File

@@ -3,7 +3,6 @@ const axios = require('axios')
const router = express.Router()
const logger = require('../utils/logger')
const { authenticateApiKey } = require('../middleware/auth')
const claudeAccountService = require('../services/claudeAccountService')
const unifiedOpenAIScheduler = require('../services/unifiedOpenAIScheduler')
const openaiAccountService = require('../services/openaiAccountService')
const apiKeyService = require('../services/apiKeyService')
@@ -35,13 +34,31 @@ async function getOpenAIAuthToken(apiKeyData, sessionId = null, requestedModel =
}
// 获取账户详情
const account = await openaiAccountService.getAccount(result.accountId)
let account = await openaiAccountService.getAccount(result.accountId)
if (!account || !account.accessToken) {
throw new Error(`OpenAI account ${result.accountId} has no valid accessToken`)
}
// 解密 accessToken
const accessToken = claudeAccountService._decryptSensitiveData(account.accessToken)
// 检查 token 是否过期并自动刷新(双重保护)
if (openaiAccountService.isTokenExpired(account)) {
if (account.refreshToken) {
logger.info(`🔄 Token expired, auto-refreshing for account ${account.name} (fallback)`)
try {
await openaiAccountService.refreshAccountToken(result.accountId)
// 重新获取更新后的账户
account = await openaiAccountService.getAccount(result.accountId)
logger.info(`✅ Token refreshed successfully in route handler`)
} catch (refreshError) {
logger.error(`Failed to refresh token for ${account.name}:`, refreshError)
throw new Error(`Token expired and refresh failed: ${refreshError.message}`)
}
} else {
throw new Error(`Token expired and no refresh token available for account ${account.name}`)
}
}
// 解密 accessTokenaccount.accessToken 是加密的)
const accessToken = openaiAccountService.decrypt(account.accessToken)
if (!accessToken) {
throw new Error('Failed to decrypt OpenAI accessToken')
}
@@ -161,7 +178,7 @@ router.post('/responses', authenticateApiKey, async (req, res) => {
// 配置请求选项
const axiosConfig = {
headers,
timeout: 60000,
timeout: 60 * 1000 * 10,
validateStatus: () => true
}