移除gitignore中的lib文件夹,上传被排除掉的前端lib文件

This commit is contained in:
墨梓柒
2026-01-21 15:41:43 +08:00
parent 761e4c8940
commit 433a4551f9
26 changed files with 4559 additions and 1 deletions

View File

@@ -0,0 +1,350 @@
/**
* 重启管理 Context
*
* 提供全局的重启状态管理和触发能力
* 使用方式:
* const { triggerRestart, isRestarting } = useRestart()
* triggerRestart() // 触发重启
*/
import {
createContext,
useContext,
useState,
useCallback,
useRef,
type ReactNode,
} from 'react'
import { restartMaiBot } from './system-api'
// ============ 类型定义 ============
export type RestartStatus =
| 'idle'
| 'requesting'
| 'restarting'
| 'checking'
| 'success'
| 'failed'
export interface RestartState {
status: RestartStatus
progress: number
elapsedTime: number
checkAttempts: number
maxAttempts: number
error?: string
}
export interface RestartContextValue {
/** 当前重启状态 */
state: RestartState
/** 是否正在重启中(任何非 idle 状态) */
isRestarting: boolean
/** 触发重启 */
triggerRestart: (options?: TriggerRestartOptions) => Promise<void>
/** 重置状态(用于失败后重试) */
resetState: () => void
/** 手动开始健康检查(用于重试) */
retryHealthCheck: () => void
}
export interface TriggerRestartOptions {
/** 重启前延迟(毫秒),用于显示提示 */
delay?: number
/** 自定义重启消息 */
message?: string
/** 跳过 API 调用(用于后端已触发重启的情况) */
skipApiCall?: boolean
}
// ============ 配置常量 ============
const CONFIG = {
/** 初始等待时间(毫秒),给后端重启时间 */
INITIAL_DELAY: 3000,
/** 健康检查间隔(毫秒) */
CHECK_INTERVAL: 2000,
/** 健康检查超时(毫秒) */
CHECK_TIMEOUT: 3000,
/** 最大检查次数 */
MAX_ATTEMPTS: 60,
/** 进度条更新间隔(毫秒) */
PROGRESS_INTERVAL: 200,
/** 成功后跳转延迟(毫秒) */
SUCCESS_REDIRECT_DELAY: 1500,
} as const
// ============ Context ============
const RestartContext = createContext<RestartContextValue | null>(null)
// ============ Provider ============
interface RestartProviderProps {
children: ReactNode
/** 重启成功后的回调 */
onRestartComplete?: () => void
/** 重启失败后的回调 */
onRestartFailed?: (error: string) => void
/** 自定义健康检查 URL */
healthCheckUrl?: string
/** 自定义最大尝试次数 */
maxAttempts?: number
}
export function RestartProvider({
children,
onRestartComplete,
onRestartFailed,
healthCheckUrl = '/api/webui/system/status',
maxAttempts = CONFIG.MAX_ATTEMPTS,
}: RestartProviderProps) {
const [state, setState] = useState<RestartState>({
status: 'idle',
progress: 0,
elapsedTime: 0,
checkAttempts: 0,
maxAttempts,
})
// 使用 useRef 存储定时器引用,避免闭包陷阱
const timersRef = useRef<{
progress?: ReturnType<typeof setInterval>
elapsed?: ReturnType<typeof setInterval>
check?: ReturnType<typeof setTimeout>
}>({})
// 清理所有定时器
const clearAllTimers = useCallback(() => {
const timers = timersRef.current
if (timers.progress) {
clearInterval(timers.progress)
timers.progress = undefined
}
if (timers.elapsed) {
clearInterval(timers.elapsed)
timers.elapsed = undefined
}
if (timers.check) {
clearTimeout(timers.check)
timers.check = undefined
}
}, [])
// 重置状态
const resetState = useCallback(() => {
clearAllTimers()
setState({
status: 'idle',
progress: 0,
elapsedTime: 0,
checkAttempts: 0,
maxAttempts,
})
}, [clearAllTimers, maxAttempts])
// 健康检查
const checkHealth = useCallback(
async (): Promise<boolean> => {
try {
const controller = new AbortController()
const timeoutId = setTimeout(
() => controller.abort(),
CONFIG.CHECK_TIMEOUT
)
const response = await fetch(healthCheckUrl, {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
signal: controller.signal,
})
clearTimeout(timeoutId)
return response.ok
} catch {
// 网络错误、超时等都视为服务不可用,这是正常的
return false
}
},
[healthCheckUrl]
)
// 开始健康检查循环
const startHealthCheck = useCallback(() => {
let currentAttempt = 0
const doCheck = async () => {
currentAttempt++
setState((prev) => ({
...prev,
status: 'checking',
checkAttempts: currentAttempt,
}))
const isHealthy = await checkHealth()
if (isHealthy) {
// 成功
clearAllTimers()
setState((prev) => ({
...prev,
status: 'success',
progress: 100,
}))
// 延迟后跳转
setTimeout(() => {
onRestartComplete?.()
// 默认跳转到 auth 页面
window.location.href = '/auth'
}, CONFIG.SUCCESS_REDIRECT_DELAY)
} else if (currentAttempt >= maxAttempts) {
// 失败
clearAllTimers()
const error = `健康检查超时 (${currentAttempt}/${maxAttempts})`
setState((prev) => ({
...prev,
status: 'failed',
error,
}))
onRestartFailed?.(error)
} else {
// 继续检查
const checkTimer = setTimeout(doCheck, CONFIG.CHECK_INTERVAL)
timersRef.current.check = checkTimer
}
}
doCheck()
}, [checkHealth, clearAllTimers, maxAttempts, onRestartComplete, onRestartFailed])
// 重试健康检查
const retryHealthCheck = useCallback(() => {
setState((prev) => ({
...prev,
status: 'checking',
checkAttempts: 0,
error: undefined,
}))
startHealthCheck()
}, [startHealthCheck])
// 触发重启
const triggerRestart = useCallback(
async (options?: TriggerRestartOptions) => {
const { delay = 0, skipApiCall = false } = options ?? {}
// 已经在重启中,忽略
if (state.status !== 'idle' && state.status !== 'failed') {
return
}
// 重置状态
clearAllTimers()
setState({
status: 'requesting',
progress: 0,
elapsedTime: 0,
checkAttempts: 0,
maxAttempts,
})
// 可选延迟
if (delay > 0) {
await new Promise((resolve) => setTimeout(resolve, delay))
}
// 调用重启 API
if (!skipApiCall) {
try {
setState((prev) => ({ ...prev, status: 'restarting' }))
// 重启 API 可能不返回响应(服务立即关闭)
await Promise.race([
restartMaiBot(),
// 5秒超时超时也视为成功服务已关闭
new Promise((resolve) => setTimeout(resolve, 5000)),
])
} catch {
// API 调用失败也是正常的(服务已关闭)
// 继续进行健康检查
}
} else {
setState((prev) => ({ ...prev, status: 'restarting' }))
}
// 启动进度条动画
const progressTimer = setInterval(() => {
setState((prev) => ({
...prev,
progress: prev.progress >= 90 ? prev.progress : prev.progress + 1,
}))
}, CONFIG.PROGRESS_INTERVAL)
// 启动计时器
const elapsedTimer = setInterval(() => {
setState((prev) => ({
...prev,
elapsedTime: prev.elapsedTime + 1,
}))
}, 1000)
timersRef.current.progress = progressTimer
timersRef.current.elapsed = elapsedTimer
// 延迟后开始健康检查
setTimeout(() => {
startHealthCheck()
}, CONFIG.INITIAL_DELAY)
},
[state.status, clearAllTimers, maxAttempts, startHealthCheck]
)
const contextValue: RestartContextValue = {
state,
isRestarting: state.status !== 'idle',
triggerRestart,
resetState,
retryHealthCheck,
}
return (
<RestartContext.Provider value={contextValue}>
{children}
</RestartContext.Provider>
)
}
// ============ Hook ============
export function useRestart(): RestartContextValue {
const context = useContext(RestartContext)
if (!context) {
throw new Error('useRestart must be used within a RestartProvider')
}
return context
}
// ============ 便捷 Hook无需 Provider ============
/**
* 独立的重启 Hook不依赖 Provider
* 适用于只需要触发重启,不需要全局状态的场景
*/
export function useRestartAction() {
const [isRestarting, setIsRestarting] = useState(false)
const triggerRestart = useCallback(async () => {
if (isRestarting) return
setIsRestarting(true)
try {
await restartMaiBot()
} catch {
// 忽略错误,服务可能已关闭
}
}, [isRestarting])
return { isRestarting, triggerRestart }
}