M0-M4: 推倒重来基线(基建+用户/密钥/核心代理+前端+管理后台+三协议互转)
- 后端 Go+Gin+GORM: 配置(OT_ env)/SQLite/Postgres 双驱动、用户体系(argon2id+JWT access/refresh)、 API Key(sk- 48位, 仅存 SHA-256 哈希) - 代理网关: /v1/chat/completions、/v1/responses、/v1/messages、/v1/models;错误按客户端协议返回 - 三协议互转(convert 包): Chat↔Messages↔Responses 请求/响应 + 流式 SSE 逐事件转换(直通优先) - 用量计费: 异步批量记账、余额扣减、balance_logs、usage_daily 日聚合 - 管理 API: 用户/渠道 CRUD+测试+模型导入/模型定价+绑定/统计/系统配置 - 前端 Vue3+TS+Tailwind(taste-skill 设计 tokens): Landing/登录注册/控制台/管理后台, 自建组件+Phosphor 图标+自建 SVG 趋势图, 已过 web-design-guidelines 复查 - mock 上游: OpenAI+Anthropic 双协议模拟(含流式) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+30
-46
@@ -1,60 +1,44 @@
|
||||
// API 客户端:统一 baseURL、token 注入、401 刷新兜底。
|
||||
import axios from 'axios'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
// 代理端点(/v1/*,Bearer API Key)与管理 API(/api/v1)baseURL 不同,分开实例
|
||||
const proxyClient = axios.create({ baseURL: '/v1', timeout: 30000 })
|
||||
|
||||
const client = axios.create({
|
||||
export const http = axios.create({
|
||||
baseURL: '/api/v1',
|
||||
timeout: 20000,
|
||||
withCredentials: true, // refresh cookie
|
||||
timeout: 15000,
|
||||
withCredentials: true,
|
||||
})
|
||||
|
||||
export { proxyClient }
|
||||
|
||||
client.interceptors.request.use((config) => {
|
||||
const token = localStorage.getItem('ot_access')
|
||||
if (token) config.headers.Authorization = `Bearer ${token}`
|
||||
http.interceptors.request.use((config) => {
|
||||
const auth = useAuthStore()
|
||||
if (auth.accessToken) {
|
||||
config.headers.Authorization = `Bearer ${auth.accessToken}`
|
||||
}
|
||||
return config
|
||||
})
|
||||
|
||||
let refreshing: Promise<string | null> | null = null
|
||||
|
||||
client.interceptors.response.use(
|
||||
(res) => res,
|
||||
async (err) => {
|
||||
const original = err.config
|
||||
// 401 且非刷新请求本身:尝试刷新一次
|
||||
if (err.response?.status === 401 && !original?._retried && !original?.url?.includes('/auth/')) {
|
||||
original._retried = true
|
||||
refreshing = refreshing ?? refreshAccess()
|
||||
const token = await refreshing
|
||||
refreshing = null
|
||||
if (token) {
|
||||
localStorage.setItem('ot_access', token)
|
||||
original.headers.Authorization = `Bearer ${token}`
|
||||
return client(original)
|
||||
http.interceptors.response.use(
|
||||
(resp) => resp,
|
||||
async (error) => {
|
||||
if (error.response?.status === 401) {
|
||||
const auth = useAuthStore()
|
||||
// 尝试用 refresh cookie 换新 token 后再试一次
|
||||
if (auth.accessToken && !error.config?._retried) {
|
||||
error.config._retried = true
|
||||
try {
|
||||
await auth.refresh()
|
||||
return http.request(error.config)
|
||||
} catch {
|
||||
auth.clear()
|
||||
}
|
||||
} else {
|
||||
auth.clear()
|
||||
}
|
||||
}
|
||||
return Promise.reject(err)
|
||||
return Promise.reject(error)
|
||||
},
|
||||
)
|
||||
|
||||
async function refreshAccess(): Promise<string | null> {
|
||||
try {
|
||||
const { data } = await client.post('/auth/refresh')
|
||||
return data.data?.access_token ?? null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
// 统一取后端错误信息
|
||||
export function errMsg(err: unknown): string {
|
||||
const e = err as { response?: { data?: { error?: { message?: string } } } }
|
||||
return e?.response?.data?.error?.message ?? '请求失败,请稍后重试'
|
||||
}
|
||||
|
||||
// 响应壳:{ data: {...} } 或 { error: {...} }
|
||||
export function unwrap<T>(p: Promise<{ data: { data?: T; error?: { message?: string } } }>): Promise<T> {
|
||||
return p.then((res) => {
|
||||
if (res.data.error) throw new Error(res.data.error.message || 'request failed')
|
||||
return res.data.data as T
|
||||
})
|
||||
}
|
||||
|
||||
export default client
|
||||
|
||||
Reference in New Issue
Block a user