feat(P3): Docker健康检查 + Worker优雅重启 + 前端错误监控

This commit is contained in:
Sisyphus
2026-04-25 16:07:58 +08:00
parent 025be6dd78
commit 3613cfe5a3
5 changed files with 468 additions and 404 deletions

View File

@@ -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
}

View File

@@ -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')

View 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(() => {
/* 静默失败,避免错误上报自身导致循环 */
})
}