feat: 迁入 MiGu.Server、平台前端与车辆列表 reflection 回退
从 Simple-FR 拆出 Platform.Server 并重命名为 MiGu.Server;frontends 源码与构建脚本迁入本仓库。地图监控在 projection/cars 失败或为空时回退 reflection 车辆列表;Simple 仓库已移除旧 Platform.Server。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
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) => {
|
||||
// 双轨:优先用 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 异常翻译成更友好的中文文案,特别是 SimpleLite 链路:
|
||||
*
|
||||
* /api/sl/projection/** → Platform.Server YARP → SimpleLite EmbedIO :8222
|
||||
*
|
||||
* 如果 SimpleLite 没启动 / 端口未监听,YARP 一定回 502;超时一般是 504。把这两种
|
||||
* 情况单独识别,写成「SimpleLite 未连接,请先启动 SimpleLite (端口 8222)」之类,
|
||||
* 比直接抛 `Request failed with status code 502` 友好得多,也避免用户怀疑前端 bug。
|
||||
*/
|
||||
function describeError(err: AxiosError): string {
|
||||
const status = err.response?.status
|
||||
const url = err.config?.url ?? ''
|
||||
const isSimpleLite = url.startsWith('/sl/') || url.startsWith('sl/')
|
||||
if (status === 502 || status === 504 || err.code === 'ECONNABORTED' || err.code === 'ERR_NETWORK') {
|
||||
if (isSimpleLite) {
|
||||
return 'SimpleLite 未连接:请先启动 SimpleLite 后端(监听端口 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
|
||||
Reference in New Issue
Block a user