mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 00:53:33 +00:00
✨ 新功能 - 支持通过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刷新失败告警
211 lines
5.1 KiB
JavaScript
211 lines
5.1 KiB
JavaScript
// API 配置
|
|
import { APP_CONFIG, getLoginUrl } from './app'
|
|
|
|
// 开发环境使用 /webapi 前缀,生产环境不使用前缀
|
|
export const API_PREFIX = APP_CONFIG.apiPrefix
|
|
|
|
// 创建完整的 API URL
|
|
export function createApiUrl(path) {
|
|
// 确保路径以 / 开头
|
|
if (!path.startsWith('/')) {
|
|
path = '/' + path
|
|
}
|
|
return API_PREFIX + path
|
|
}
|
|
|
|
// API 请求的基础配置
|
|
export function getRequestConfig(token) {
|
|
const config = {
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
}
|
|
|
|
if (token) {
|
|
config.headers['Authorization'] = `Bearer ${token}`
|
|
}
|
|
|
|
return config
|
|
}
|
|
|
|
// 统一的 API 请求类
|
|
class ApiClient {
|
|
constructor() {
|
|
this.baseURL = API_PREFIX
|
|
}
|
|
|
|
// 获取认证 token
|
|
getAuthToken() {
|
|
const authToken = localStorage.getItem('authToken')
|
|
return authToken || null
|
|
}
|
|
|
|
// 构建请求配置
|
|
buildConfig(options = {}) {
|
|
const config = {
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
...options.headers
|
|
},
|
|
...options
|
|
}
|
|
|
|
// 添加认证 token
|
|
const token = this.getAuthToken()
|
|
if (token) {
|
|
config.headers['Authorization'] = `Bearer ${token}`
|
|
}
|
|
|
|
return config
|
|
}
|
|
|
|
// 处理响应
|
|
async handleResponse(response) {
|
|
// 401 未授权,需要重新登录
|
|
if (response.status === 401) {
|
|
// 如果当前已经在登录页面,不要再次跳转
|
|
const currentPath = window.location.pathname + window.location.hash
|
|
const isLoginPage = currentPath.includes('/login') || currentPath.endsWith('/')
|
|
|
|
if (!isLoginPage) {
|
|
localStorage.removeItem('authToken')
|
|
// 使用统一的登录URL
|
|
window.location.href = getLoginUrl()
|
|
}
|
|
throw new Error('Unauthorized')
|
|
}
|
|
|
|
// 尝试解析 JSON
|
|
const contentType = response.headers.get('content-type')
|
|
if (contentType && contentType.includes('application/json')) {
|
|
const data = await response.json()
|
|
|
|
// 如果响应不成功,抛出错误
|
|
if (!response.ok) {
|
|
// 创建一个包含完整错误信息的错误对象
|
|
const error = new Error(data.message || `HTTP ${response.status}`)
|
|
// 保留完整的响应数据,以便错误处理时可以访问详细信息
|
|
error.response = {
|
|
status: response.status,
|
|
data: data
|
|
}
|
|
// 为了向后兼容,也保留原始的 message
|
|
error.message = data.message || error.message
|
|
throw error
|
|
}
|
|
|
|
return data
|
|
}
|
|
|
|
// 非 JSON 响应
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
|
}
|
|
|
|
return response
|
|
}
|
|
|
|
// GET 请求
|
|
async get(url, options = {}) {
|
|
// 处理查询参数
|
|
let fullUrl = createApiUrl(url)
|
|
if (options.params) {
|
|
const params = new URLSearchParams(options.params)
|
|
fullUrl += '?' + params.toString()
|
|
}
|
|
|
|
// 移除 params 避免传递给 fetch
|
|
// eslint-disable-next-line no-unused-vars
|
|
const { params, ...configOptions } = options
|
|
const config = this.buildConfig({
|
|
...configOptions,
|
|
method: 'GET'
|
|
})
|
|
|
|
try {
|
|
const response = await fetch(fullUrl, config)
|
|
return await this.handleResponse(response)
|
|
} catch (error) {
|
|
console.error('API GET Error:', error)
|
|
throw error
|
|
}
|
|
}
|
|
|
|
// POST 请求
|
|
async post(url, data = null, options = {}) {
|
|
const fullUrl = createApiUrl(url)
|
|
const config = this.buildConfig({
|
|
...options,
|
|
method: 'POST',
|
|
body: data ? JSON.stringify(data) : undefined
|
|
})
|
|
|
|
try {
|
|
const response = await fetch(fullUrl, config)
|
|
return await this.handleResponse(response)
|
|
} catch (error) {
|
|
console.error('API POST Error:', error)
|
|
throw error
|
|
}
|
|
}
|
|
|
|
// PUT 请求
|
|
async put(url, data = null, options = {}) {
|
|
const fullUrl = createApiUrl(url)
|
|
const config = this.buildConfig({
|
|
...options,
|
|
method: 'PUT',
|
|
body: data ? JSON.stringify(data) : undefined
|
|
})
|
|
|
|
try {
|
|
const response = await fetch(fullUrl, config)
|
|
return await this.handleResponse(response)
|
|
} catch (error) {
|
|
console.error('API PUT Error:', error)
|
|
throw error
|
|
}
|
|
}
|
|
|
|
// PATCH 请求
|
|
async patch(url, data = null, options = {}) {
|
|
const fullUrl = createApiUrl(url)
|
|
const config = this.buildConfig({
|
|
...options,
|
|
method: 'PATCH',
|
|
body: data ? JSON.stringify(data) : undefined
|
|
})
|
|
|
|
try {
|
|
const response = await fetch(fullUrl, config)
|
|
return await this.handleResponse(response)
|
|
} catch (error) {
|
|
console.error('API PATCH Error:', error)
|
|
throw error
|
|
}
|
|
}
|
|
|
|
// DELETE 请求
|
|
async delete(url, options = {}) {
|
|
const fullUrl = createApiUrl(url)
|
|
const { data, ...restOptions } = options
|
|
|
|
const config = this.buildConfig({
|
|
...restOptions,
|
|
method: 'DELETE',
|
|
body: data ? JSON.stringify(data) : undefined
|
|
})
|
|
|
|
try {
|
|
const response = await fetch(fullUrl, config)
|
|
return await this.handleResponse(response)
|
|
} catch (error) {
|
|
console.error('API DELETE Error:', error)
|
|
throw error
|
|
}
|
|
}
|
|
}
|
|
|
|
// 导出单例实例
|
|
export const apiClient = new ApiClient()
|