feat(P3): Docker健康检查 + Worker优雅重启 + 前端错误监控
This commit is contained in:
@@ -1,99 +1,104 @@
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
|
||||
export class ApiError extends Error {
|
||||
status: number
|
||||
payload?: unknown
|
||||
|
||||
constructor(message: string, status: number, payload?: unknown) {
|
||||
super(message)
|
||||
this.name = 'ApiError'
|
||||
this.status = status
|
||||
this.payload = payload
|
||||
}
|
||||
}
|
||||
|
||||
type ApiErrorBody = { error?: string }
|
||||
|
||||
const API_BASE = (import.meta.env.VITE_API_BASE as string | undefined) ?? '/api'
|
||||
|
||||
export async function postJson<T>(path: string, body: unknown, options?: { authRequired?: boolean }): Promise<T> {
|
||||
const auth = useAuthStore()
|
||||
const token = auth.token
|
||||
|
||||
if (options?.authRequired && !token) {
|
||||
throw new ApiError('需要先登录(缺少 token)', 401)
|
||||
}
|
||||
|
||||
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
|
||||
if (token) headers.Authorization = `Bearer ${token}`
|
||||
|
||||
const res = await fetch(`${API_BASE}${path}`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(body ?? {}),
|
||||
})
|
||||
|
||||
const text = await res.text()
|
||||
let data: unknown = null
|
||||
if (text) {
|
||||
try {
|
||||
data = JSON.parse(text)
|
||||
} catch {
|
||||
data = text
|
||||
}
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
if (res.status === 401) {
|
||||
auth.clearToken()
|
||||
}
|
||||
const msg =
|
||||
data && typeof data === 'object' && (data as ApiErrorBody).error
|
||||
? String((data as ApiErrorBody).error)
|
||||
: `请求失败 (${res.status})`
|
||||
throw new ApiError(msg, res.status, data)
|
||||
}
|
||||
|
||||
return data as T
|
||||
}
|
||||
|
||||
export async function postForm<T>(path: string, body: FormData, options?: { authRequired?: boolean }): Promise<T> {
|
||||
const auth = useAuthStore()
|
||||
const token = auth.token
|
||||
|
||||
if (options?.authRequired && !token) {
|
||||
throw new ApiError('需要先登录(缺少 token)', 401)
|
||||
}
|
||||
|
||||
const headers: Record<string, string> = {}
|
||||
if (token) headers.Authorization = `Bearer ${token}`
|
||||
|
||||
const res = await fetch(`${API_BASE}${path}`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body,
|
||||
})
|
||||
|
||||
const text = await res.text()
|
||||
let data: unknown = null
|
||||
if (text) {
|
||||
try {
|
||||
data = JSON.parse(text)
|
||||
} catch {
|
||||
data = text
|
||||
}
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
if (res.status === 401) {
|
||||
auth.clearToken()
|
||||
}
|
||||
const msg =
|
||||
data && typeof data === 'object' && (data as ApiErrorBody).error
|
||||
? String((data as ApiErrorBody).error)
|
||||
: `请求失败 (${res.status})`
|
||||
throw new ApiError(msg, res.status, data)
|
||||
}
|
||||
|
||||
return data as T
|
||||
}
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
import { reportError } from '../utils/error-reporter'
|
||||
|
||||
export class ApiError extends Error {
|
||||
status: number
|
||||
payload?: unknown
|
||||
|
||||
constructor(message: string, status: number, payload?: unknown) {
|
||||
super(message)
|
||||
this.name = 'ApiError'
|
||||
this.status = status
|
||||
this.payload = payload
|
||||
}
|
||||
}
|
||||
|
||||
type ApiErrorBody = { error?: string }
|
||||
|
||||
const API_BASE = (import.meta.env.VITE_API_BASE as string | undefined) ?? '/api'
|
||||
|
||||
export async function postJson<T>(path: string, body: unknown, options?: { authRequired?: boolean }): Promise<T> {
|
||||
const auth = useAuthStore()
|
||||
const token = auth.token
|
||||
|
||||
if (options?.authRequired && !token) {
|
||||
throw new ApiError('需要先登录(缺少 token)', 401)
|
||||
}
|
||||
|
||||
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
|
||||
if (token) headers.Authorization = `Bearer ${token}`
|
||||
|
||||
const res = await fetch(`${API_BASE}${path}`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(body ?? {}),
|
||||
})
|
||||
|
||||
const text = await res.text()
|
||||
let data: unknown = null
|
||||
if (text) {
|
||||
try {
|
||||
data = JSON.parse(text)
|
||||
} catch {
|
||||
data = text
|
||||
}
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
if (res.status === 401) {
|
||||
auth.clearToken()
|
||||
}
|
||||
const msg =
|
||||
data && typeof data === 'object' && (data as ApiErrorBody).error
|
||||
? String((data as ApiErrorBody).error)
|
||||
: `请求失败 (${res.status})`
|
||||
const apiErr = new ApiError(msg, res.status, data)
|
||||
reportError(apiErr, { path, status: res.status })
|
||||
throw apiErr
|
||||
}
|
||||
|
||||
return data as T
|
||||
}
|
||||
|
||||
export async function postForm<T>(path: string, body: FormData, options?: { authRequired?: boolean }): Promise<T> {
|
||||
const auth = useAuthStore()
|
||||
const token = auth.token
|
||||
|
||||
if (options?.authRequired && !token) {
|
||||
throw new ApiError('需要先登录(缺少 token)', 401)
|
||||
}
|
||||
|
||||
const headers: Record<string, string> = {}
|
||||
if (token) headers.Authorization = `Bearer ${token}`
|
||||
|
||||
const res = await fetch(`${API_BASE}${path}`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body,
|
||||
})
|
||||
|
||||
const text = await res.text()
|
||||
let data: unknown = null
|
||||
if (text) {
|
||||
try {
|
||||
data = JSON.parse(text)
|
||||
} catch {
|
||||
data = text
|
||||
}
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
if (res.status === 401) {
|
||||
auth.clearToken()
|
||||
}
|
||||
const msg =
|
||||
data && typeof data === 'object' && (data as ApiErrorBody).error
|
||||
? String((data as ApiErrorBody).error)
|
||||
: `请求失败 (${res.status})`
|
||||
const apiErr = new ApiError(msg, res.status, data)
|
||||
reportError(apiErr, { path, status: res.status })
|
||||
throw apiErr
|
||||
}
|
||||
|
||||
return data as T
|
||||
}
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import './style.css'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
app.mount('#app')
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import './style.css'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import { reportError } from './utils/error-reporter'
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
|
||||
app.config.errorHandler = (err, _instance, info) => {
|
||||
reportError(err instanceof Error ? err : new Error(String(err)), { info })
|
||||
}
|
||||
|
||||
app.mount('#app')
|
||||
|
||||
18
frontend/src/utils/error-reporter.ts
Normal file
18
frontend/src/utils/error-reporter.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export function reportError(error: Error, context?: Record<string, unknown>) {
|
||||
if (import.meta.env.DEV) {
|
||||
console.error('[ErrorReporter]', error.message, context)
|
||||
return
|
||||
}
|
||||
fetch('/api/error-report', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
context,
|
||||
timestamp: new Date().toISOString(),
|
||||
}),
|
||||
}).catch(() => {
|
||||
/* 静默失败,避免错误上报自身导致循环 */
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user