配置段/环境变量、Launcher、健康检查 API、OpenAPI 与前后端文案同步;兼容探测旧 SimpleLite 进程名。 Co-authored-by: Cursor <cursoragent@cursor.com>
80 lines
3.5 KiB
TypeScript
80 lines
3.5 KiB
TypeScript
import axios, { AxiosError, type AxiosInstance, type InternalAxiosRequestConfig } from 'axios'
|
||
|
||
const baseURL = (import.meta.env.VITE_API_BASE as string | undefined) ?? '/api'
|
||
|
||
// AR-5 (会话 45):withCredentials=true 让后端下发的 httpOnly Cookie (simple.auth.token)
|
||
// 自动随请求带回。Platform.Server `/api/auth/login` 同时下发 Cookie 和返回 token 字段,
|
||
// 过渡期内 Authorization: Bearer header 仍然兼容(旧浏览器 / 跨进程脚本)。
|
||
const http: AxiosInstance = axios.create({
|
||
baseURL,
|
||
timeout: 15000,
|
||
headers: { 'Content-Type': 'application/json' },
|
||
withCredentials: true
|
||
})
|
||
|
||
http.interceptors.request.use((config: InternalAxiosRequestConfig) => {
|
||
// FormData 必须由浏览器自动带 multipart boundary;清掉默认 application/json。
|
||
if (typeof FormData !== 'undefined' && config.data instanceof FormData) {
|
||
config.headers.delete('Content-Type')
|
||
}
|
||
// 双轨:优先用 localStorage 里的 token(过渡期 fallback),Cookie 会自动带;
|
||
// 后端首先看 Authorization: Bearer,没有再看 Cookie,两路任一通过即可。
|
||
const token = localStorage.getItem('simple.auth.token')
|
||
if (token) {
|
||
config.headers.set('Authorization', `Bearer ${token}`)
|
||
}
|
||
const scope = localStorage.getItem('simple.auth.scope')
|
||
if (scope) {
|
||
config.headers.set('X-Scope', scope)
|
||
}
|
||
return config
|
||
})
|
||
|
||
/**
|
||
* 把 axios 异常翻译成更友好的中文文案,特别是 Simple3 链路:
|
||
*
|
||
* /api/sl/projection/** → Platform.Server YARP → Simple3 EmbedIO :8222
|
||
*
|
||
* 如果 Simple3 没启动 / 端口未监听,YARP 一定回 502;超时一般是 504。把这两种
|
||
* 情况单独识别,写成「Simple3 未连接,请先启动 Simple3 (端口 8222)」之类,
|
||
* 比直接抛 `Request failed with status code 502` 友好得多,也避免用户怀疑前端 bug。
|
||
*/
|
||
function describeError(err: AxiosError): string {
|
||
const status = err.response?.status
|
||
const url = err.config?.url ?? ''
|
||
const isSimple3 = url.startsWith('/sl/') || url.startsWith('sl/')
|
||
if (status === 502 || status === 504 || err.code === 'ECONNABORTED' || err.code === 'ERR_NETWORK') {
|
||
if (isSimple3) {
|
||
return 'Simple3 未连接:请先启动 Simple3 后端(监听端口 8222)后重试。'
|
||
}
|
||
return `网关无法连接到下游服务(${status ?? err.code ?? 'network'})。`
|
||
}
|
||
// 后端 envelope 模式:{ success: false, message: 'xxx' }
|
||
const data = err.response?.data as { message?: string } | undefined
|
||
if (data?.message) return data.message
|
||
return err.message
|
||
}
|
||
|
||
http.interceptors.response.use(
|
||
(resp) => resp,
|
||
(err: AxiosError) => {
|
||
if (err.response?.status === 401) {
|
||
// 会话 45 HC-1:401 全量清 state,避免老 user/scope/perm 残留导致 UI 误判。
|
||
try {
|
||
localStorage.removeItem('simple.auth.token')
|
||
localStorage.removeItem('simple.auth.user')
|
||
localStorage.removeItem('simple.auth.scope')
|
||
localStorage.removeItem('simple.auth.runMode')
|
||
localStorage.removeItem('simple.auth.perm')
|
||
} catch { /* 某些隐私模式 / iframe 沙箱可能禁用 localStorage */ }
|
||
if (location.pathname !== '/login') location.assign('/login')
|
||
}
|
||
// 用 Object.defineProperty 覆盖 message,避免后续 `(err as Error).message` 还是看到原文。
|
||
const friendly = describeError(err)
|
||
try { Object.defineProperty(err, 'message', { value: friendly, configurable: true }) } catch { /* ignore */ }
|
||
return Promise.reject(err)
|
||
}
|
||
)
|
||
|
||
export default http
|