b70557a077
- 单设备登录:登录成功即 last-login-wins 绑定 users.machine;非超管旧 token 在下一次受保护请求抛 4011 下线,超管豁免;仅认 token 内签名 deviceId,不用请求头。 前端两端接入踢下线跳转(?kicked=1 提示),V118 清空历史 machine - 站内通知:新增 notification 模块(任务失败扫描 / 密钥欠费 / 下游服务探测三类来源), 前后台铃铛组件 + 轮询;后台列表按主管数据范围(UserDataScopeSupport)过滤;V116 建表 均已于 2026-09-13 部署上线,此次补提交源码(此前仅存在于已部署 JAR/构建产物中)
158 lines
4.2 KiB
TypeScript
158 lines
4.2 KiB
TypeScript
import axios, {
|
|
AxiosError,
|
|
type AxiosInstance,
|
|
type AxiosRequestConfig,
|
|
type AxiosResponse,
|
|
type InternalAxiosRequestConfig,
|
|
} from 'axios'
|
|
import { handleKicked, isKickedPayload } from '../auth/kick-handler.ts'
|
|
|
|
export interface LegacyApiSuccess<T> {
|
|
success: true
|
|
msg?: string
|
|
data?: T
|
|
}
|
|
|
|
export interface LegacyApiFailure {
|
|
success: false
|
|
error?: string
|
|
}
|
|
|
|
export type ApiResponse<T> = LegacyApiSuccess<T> | LegacyApiFailure
|
|
export interface JavaApiResponse<T> {
|
|
success: boolean
|
|
message: string
|
|
data: T | null
|
|
}
|
|
export type RequestOptions<D = unknown> = AxiosRequestConfig<D>
|
|
|
|
export function extractErrorMessage(error: unknown) {
|
|
if (error instanceof AxiosError) {
|
|
const data = error.response?.data
|
|
|
|
if (data && typeof data === 'object') {
|
|
for (const field of ['error', 'message', 'msg'] as const) {
|
|
if (field in data && typeof (data as Record<string, unknown>)[field] === 'string') {
|
|
const value = ((data as Record<string, string>)[field]).trim()
|
|
if (value) {
|
|
return value
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (typeof data === 'string' && data.trim()) {
|
|
return data
|
|
}
|
|
|
|
return error.message || '请求失败'
|
|
}
|
|
|
|
return error instanceof Error ? error.message : '请求失败'
|
|
}
|
|
|
|
/** 从 axios 错误对象里取响应体(兼容真实 AxiosError 与测试桩)。 */
|
|
function responsePayloadOf(error: unknown): unknown {
|
|
return (error as { response?: { data?: unknown } } | null | undefined)?.response?.data
|
|
}
|
|
|
|
/** 响应成功分支:Java 业务错误是 HTTP 200 + body.code,互踢下线必须在这里也判。 */
|
|
export function onResponseFulfilled(response: AxiosResponse): AxiosResponse {
|
|
if (isKickedPayload(response.data)) {
|
|
handleKicked()
|
|
}
|
|
return response
|
|
}
|
|
|
|
/** 响应失败分支:折叠为 Error 之前先判互踢下线(HTTP 401 等状态码路径)。 */
|
|
export function onResponseRejected(error: unknown): Promise<never> {
|
|
if (isKickedPayload(responsePayloadOf(error))) {
|
|
handleKicked()
|
|
}
|
|
return Promise.reject(new Error(extractErrorMessage(error)))
|
|
}
|
|
|
|
function createHttpClient(): AxiosInstance {
|
|
const instance = axios.create({
|
|
withCredentials: true,
|
|
timeout: 30000,
|
|
headers: {
|
|
'X-Requested-With': 'XMLHttpRequest',
|
|
},
|
|
})
|
|
|
|
instance.interceptors.request.use((config: InternalAxiosRequestConfig) => {
|
|
// 携带本地登录令牌:桌面端同源 cookie 仍可用;dev(5173) 无 cookie 时靠 Bearer 过 Java 鉴权
|
|
try {
|
|
if (config.url && config.url.indexOf('/newApi') === 0 && typeof window !== 'undefined') {
|
|
const token = window.localStorage.getItem('aiimage_auth_token') || ''
|
|
if (token) {
|
|
config.headers.Authorization = `Bearer ${token}`
|
|
}
|
|
}
|
|
} catch {
|
|
/* 忽略读取令牌异常 */
|
|
}
|
|
return config
|
|
})
|
|
|
|
instance.interceptors.response.use(onResponseFulfilled, onResponseRejected)
|
|
|
|
return instance
|
|
}
|
|
|
|
export const http = createHttpClient()
|
|
|
|
export async function request<T = unknown, D = unknown>(config: RequestOptions<D>): Promise<T> {
|
|
const response = await http.request<T, AxiosResponse<T>, D>(config)
|
|
return response.data
|
|
}
|
|
|
|
export function get<T = unknown>(url: string, config?: RequestOptions) {
|
|
return request<T>({
|
|
url,
|
|
method: 'GET',
|
|
...config,
|
|
})
|
|
}
|
|
|
|
export function post<T = unknown, D = unknown>(url: string, data?: D, config?: RequestOptions<D>) {
|
|
return request<T, D>({
|
|
url,
|
|
method: 'POST',
|
|
data,
|
|
...config,
|
|
})
|
|
}
|
|
|
|
export function put<T = unknown, D = unknown>(url: string, data?: D, config?: RequestOptions<D>) {
|
|
return request<T, D>({
|
|
url,
|
|
method: 'PUT',
|
|
data,
|
|
...config,
|
|
})
|
|
}
|
|
|
|
export function del<T = unknown>(url: string, config?: RequestOptions) {
|
|
return request<T>({
|
|
url,
|
|
method: 'DELETE',
|
|
...config,
|
|
})
|
|
}
|
|
|
|
export const requestJson = request
|
|
export const requestGetJson = get
|
|
export const requestPostJson = post
|
|
export const requestPutJson = put
|
|
export const requestDeleteJson = del
|
|
|
|
export async function unwrapJavaResponse<T>(promise: Promise<JavaApiResponse<T>>) {
|
|
const response = await promise
|
|
if (!response.success) {
|
|
throw new Error(response.message || '请求失败')
|
|
}
|
|
return response.data as T
|
|
}
|