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:
Sakurasan
2026-08-15 15:34:06 +08:00
co-authored by Claude
parent b25e9ec8a7
commit ec4de8d913
92 changed files with 6203 additions and 3064 deletions
+30 -46
View File
@@ -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