fix(workspace): background vector indexing and correct diagram previews

This commit is contained in:
2026-09-06 02:57:56 +08:00
parent 9e0715f9db
commit a5c44c4ac0
21 changed files with 561 additions and 75 deletions
+26
View File
@@ -0,0 +1,26 @@
// @vitest-environment happy-dom
import { afterEach, expect, it, vi } from 'vitest'
import apiClient from './apiClient'
afterEach(() => { vi.useRealTimers(); vi.unstubAllGlobals() })
it('aborts a stuck request with an actionable timeout', async () => {
vi.useFakeTimers()
let signal: AbortSignal | undefined
vi.stubGlobal('fetch', vi.fn((_url, options) => new Promise((_resolve, reject) => {
signal = options.signal
signal?.addEventListener('abort', () => reject(new Error('aborted')))
})))
const assertion = expect(apiClient.get('/api/workspace', { timeoutMs: 15000 })).rejects.toMatchObject({ code: 'REQUEST_TIMEOUT' })
await vi.advanceTimersByTimeAsync(15000)
await assertion
expect(signal?.aborted).toBe(true)
expect(vi.getTimerCount()).toBe(0)
})
it('clears the deadline after success', async () => {
vi.useFakeTimers()
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response('{"ok":true}', { headers: { 'Content-Type': 'application/json' } })))
expect(await apiClient.get('/health', { timeoutMs: 10000 })).toEqual({ ok: true })
expect(vi.getTimerCount()).toBe(0)
})
+13 -1
View File
@@ -9,6 +9,7 @@ export function resolveApiUrl(path: string): string {
}
interface RequestOptions extends RequestInit {
timeoutMs?: number
params?: Record<string, string | number | boolean | undefined>
token?: string
}
@@ -26,7 +27,13 @@ export class ApiErrorClass extends Error {
}
async function request<T>(path: string, options: RequestOptions = {}): Promise<T> {
const { params, token, headers, ...rest } = options
const { params, token, headers, timeoutMs, ...rest } = options
const controller = timeoutMs ? new AbortController() : null
let timedOut = false
const abort = () => controller?.abort()
if (rest.signal?.aborted) abort()
rest.signal?.addEventListener('abort', abort, { once: true })
const timer = timeoutMs ? setTimeout(() => { timedOut = true; controller?.abort() }, timeoutMs) : undefined
let url = resolveApiUrl(path)
@@ -54,6 +61,7 @@ async function request<T>(path: string, options: RequestOptions = {}): Promise<T
try {
const resp = await fetch(url, {
...rest,
signal: controller?.signal ?? rest.signal,
headers: reqHeaders,
})
@@ -78,8 +86,12 @@ async function request<T>(path: string, options: RequestOptions = {}): Promise<T
throw new ApiErrorClass(code, message, details)
} catch (e) {
if (timedOut) throw new ApiErrorClass('REQUEST_TIMEOUT', '请求超时,请检查后端状态后重试。')
if (e instanceof ApiErrorClass) throw e
throw new ApiErrorClass('NETWORK_ERROR', (e as Error).message || 'Network error')
} finally {
if (timer) clearTimeout(timer)
rest.signal?.removeEventListener('abort', abort)
}
}
+2 -1
View File
@@ -3,6 +3,7 @@ import type { ApiIndexJob, ApiIndexStatus, IndexStatus } from '@/contracts'
function toIndexStatus(status: ApiIndexStatus): IndexStatus {
return {
vector_refresh_required: status.vector_refresh_required ?? false,
status: status.status === 'idle' ? 'idle' : status.status === 'failed' ? 'error' : 'indexing',
pending_jobs: status.pending_jobs,
total_notes: status.total_notes ?? null,
@@ -13,7 +14,7 @@ function toIndexStatus(status: ApiIndexStatus): IndexStatus {
}
export async function getIndexStatus(): Promise<IndexStatus> {
return toIndexStatus(await apiClient.get<ApiIndexStatus>('/api/index/status'))
return toIndexStatus(await apiClient.get<ApiIndexStatus>('/api/index/status', { timeoutMs: 10000 }))
}
export async function rebuildIndex(scope: 'full' | 'fts' | 'vector' = 'full'): Promise<ApiIndexJob> {
+3 -3
View File
@@ -2,13 +2,13 @@ import apiClient from './apiClient'
import type { SystemStatus } from '@/contracts'
export function healthCheck(): Promise<{ status: string }> {
return apiClient.get('/health')
return apiClient.get('/health', { timeoutMs: 10000 })
}
export function getStatus(): Promise<SystemStatus> {
return apiClient.get('/api/status')
return apiClient.get('/api/status', { timeoutMs: 10000 })
}
export function getPermissionPolicy(): Promise<Record<string, 'allow' | 'confirm' | 'deny'>> {
return apiClient.get('/api/permissions/policy')
return apiClient.get('/api/permissions/policy', { timeoutMs: 10000 })
}
+2 -2
View File
@@ -79,7 +79,7 @@ async function requireNoteId(filePath: string): Promise<string> {
}
export async function getWorkspaceInfo(): Promise<ApiWorkspaceInfo> {
return apiClient.get('/api/workspace')
return apiClient.get('/api/workspace', { timeoutMs: 15000 })
}
export async function getRecentVaults(): Promise<VaultInfo[]> {
@@ -88,7 +88,7 @@ export async function getRecentVaults(): Promise<VaultInfo[]> {
}
export async function openVault(path: string): Promise<VaultInfo> {
const snapshot = await apiClient.post<ApiWorkspaceSnapshot>('/api/workspace/open', { path })
const snapshot = await apiClient.post<ApiWorkspaceSnapshot>('/api/workspace/open', { path }, { timeoutMs: 15000 })
cacheEntries(snapshot.items)
return {
vault_id: snapshot.workspace.vault_id,