feat: 加固运行期可靠性并补齐调度与观测接口
- 运行句柄按 run_id 归属,修复取消后重跑时闸门被旧运行删除的竞态 - 调度循环改用取消令牌退出,启动时对账遗留运行,校验 rows_affected - 三类任务统一参与调度,新增 /api/v1/stats 与 /api/v1/schedule - SQLite 启用 WAL 与 busy_timeout,日志清理跳过活跃文件,单行上限 256 KiB - 前端缓存管理令牌并在 403 时重试一次,仪表盘改用统计接口
This commit is contained in:
+56
-18
@@ -24,6 +24,8 @@ export type InterpreterKind = components['schemas']['InterpreterKind']
|
||||
export type DirectoryEntry = components['schemas']['DirectoryEntry']
|
||||
export type DirectoryListingResponse =
|
||||
components['schemas']['DirectoryListingResponse']
|
||||
export type TaskStats = components['schemas']['TaskStatsResponse']
|
||||
export type ScheduledTask = components['schemas']['ScheduledTaskResponse']
|
||||
|
||||
export interface TaskFilters {
|
||||
q?: string
|
||||
@@ -77,20 +79,27 @@ async function requestJson<T>(
|
||||
path: string,
|
||||
init: RequestInit = {},
|
||||
): Promise<T> {
|
||||
const headers = new Headers(init.headers)
|
||||
headers.set('Accept', 'application/json')
|
||||
const method = (init.method ?? 'GET').toUpperCase()
|
||||
if (init.body) {
|
||||
headers.set('Content-Type', 'application/json')
|
||||
}
|
||||
if (!['GET', 'HEAD', 'OPTIONS'].includes(method)) {
|
||||
headers.set('X-TaskPilot-Token', await fetchSessionToken())
|
||||
const mutating = !['GET', 'HEAD', 'OPTIONS'].includes(method)
|
||||
|
||||
const send = async (): Promise<Response> => {
|
||||
const headers = new Headers(init.headers)
|
||||
headers.set('Accept', 'application/json')
|
||||
if (init.body) {
|
||||
headers.set('Content-Type', 'application/json')
|
||||
}
|
||||
if (mutating) {
|
||||
headers.set('X-TaskPilot-Token', await fetchSessionToken())
|
||||
}
|
||||
return fetch(path, { ...init, headers })
|
||||
}
|
||||
|
||||
const response = await fetch(path, {
|
||||
...init,
|
||||
headers,
|
||||
})
|
||||
let response = await send()
|
||||
if (mutating && response.status === 403) {
|
||||
// 后端重启会轮换管理令牌:丢弃缓存后重试一次。
|
||||
invalidateSessionToken()
|
||||
response = await send()
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
let body: ErrorResponse | undefined
|
||||
@@ -108,15 +117,34 @@ async function requestJson<T>(
|
||||
return (await response.json()) as T
|
||||
}
|
||||
|
||||
let cachedSessionToken: string | null = null
|
||||
let pendingSessionToken: Promise<string> | null = null
|
||||
|
||||
export function invalidateSessionToken(): void {
|
||||
cachedSessionToken = null
|
||||
}
|
||||
|
||||
async function fetchSessionToken(): Promise<string> {
|
||||
const response = await fetch('/api/v1/session', {
|
||||
headers: { Accept: 'application/json' },
|
||||
})
|
||||
if (!response.ok) {
|
||||
throw new ApiRequestError(response.status)
|
||||
if (cachedSessionToken) {
|
||||
return cachedSessionToken
|
||||
}
|
||||
const session = (await response.json()) as SessionResponse
|
||||
return session.token
|
||||
if (!pendingSessionToken) {
|
||||
pendingSessionToken = fetch('/api/v1/session', {
|
||||
headers: { Accept: 'application/json' },
|
||||
})
|
||||
.then(async (response) => {
|
||||
if (!response.ok) {
|
||||
throw new ApiRequestError(response.status)
|
||||
}
|
||||
const session = (await response.json()) as SessionResponse
|
||||
cachedSessionToken = session.token
|
||||
return session.token
|
||||
})
|
||||
.finally(() => {
|
||||
pendingSessionToken = null
|
||||
})
|
||||
}
|
||||
return pendingSessionToken
|
||||
}
|
||||
|
||||
function withQuery(path: string, values: object): string {
|
||||
@@ -265,3 +293,13 @@ export function listDirectories(
|
||||
{ signal },
|
||||
)
|
||||
}
|
||||
|
||||
/** 仪表盘计数:一次请求拿到全部指标。 */
|
||||
export function fetchStats(signal?: AbortSignal): Promise<TaskStats> {
|
||||
return requestJson<TaskStats>('/api/v1/stats', { signal })
|
||||
}
|
||||
|
||||
/** 已启用任务的下次触发时间。 */
|
||||
export function fetchSchedule(signal?: AbortSignal): Promise<ScheduledTask[]> {
|
||||
return requestJson<ScheduledTask[]>('/api/v1/schedule', { signal })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user