fix: 串行化凭据所有权并响应桌面请求取消

This commit is contained in:
2026-09-08 13:16:11 +08:00
parent 8d9333d8b0
commit 0d225f308f
21 changed files with 1034 additions and 118 deletions
+21 -9
View File
@@ -1,5 +1,6 @@
import type { ApiError, ErrorResponse } from '@/contracts'
import { hostInvoke, isDesktop } from './platform/desktop'
import { isDesktop } from './platform/desktop'
import { coreRequest, type RequestProgress } from './platform/coreRequest'
// 所有 HTTP 请求都经过此边界,以统一地址、请求追踪和错误契约。
const BASE_URL = import.meta.env.VITE_API_BASE_URL ?? import.meta.env.VITE_API_BASE ?? ''
@@ -50,12 +51,18 @@ export class ApiErrorClass extends Error {
async function request<T>(path: string, options: RequestOptions = {}): Promise<T> {
const { params, token, headers, timeoutMs, ...rest } = options
const controller = timeoutMs ? new AbortController() : null
const desktop = isDesktop()
const deadlineMs = timeoutMs ?? (desktop ? 30_000 : undefined)
if (deadlineMs !== undefined && (!Number.isFinite(deadlineMs) || deadlineMs <= 0 || (desktop && deadlineMs > 600_000))) {
throw new ApiErrorClass('CORE_TIMEOUT_INVALID', '请求超时设置无效')
}
const controller = new AbortController()
const progress: RequestProgress = { issued: false }
let timedOut = false
const abort = () => controller?.abort()
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
const timer = deadlineMs ? setTimeout(() => { timedOut = true; controller.abort() }, deadlineMs) : undefined
let url = resolveApiUrl(path)
@@ -81,7 +88,8 @@ async function request<T>(path: string, options: RequestOptions = {}): Promise<T
reqHeaders['X-Request-Id'] = reqId
try {
if (isDesktop()) {
controller.signal.throwIfAborted()
if (desktop) {
const parsed = new URL(url, 'http://localhost')
let bodyBase64: string | undefined
if (rest.body instanceof Blob) {
@@ -91,14 +99,14 @@ async function request<T>(path: string, options: RequestOptions = {}): Promise<T
for (let offset = 0; offset < bytes.length; offset += 16384) parts.push(String.fromCharCode(...bytes.subarray(offset, offset + 16384)))
bodyBase64 = btoa(parts.join(''))
}
const response = await hostInvoke<DesktopCoreResponse>('core_request', {
const response = await coreRequest<DesktopCoreResponse>({
method: rest.method ?? 'GET',
path: `${parsed.pathname}${parsed.search}`,
body: typeof rest.body === 'string' ? JSON.parse(rest.body) : undefined,
bodyBase64,
contentType: new Headers(reqHeaders).get('Content-Type'),
idempotencyKey: new Headers(reqHeaders).get('Idempotency-Key') ?? undefined,
})
}, controller.signal, Math.ceil(deadlineMs!), progress)
if (response.status >= 200 && response.status < 300) {
if (response.status === 204) return undefined as T
return (response.content_type.includes('application/json')
@@ -111,7 +119,7 @@ async function request<T>(path: string, options: RequestOptions = {}): Promise<T
}
const resp = await fetch(url, {
...rest,
signal: controller?.signal ?? rest.signal,
signal: controller.signal,
headers: reqHeaders,
})
@@ -136,7 +144,11 @@ 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', '请求超时,请检查后端状态后重试。')
const nativeCode = (e as { code?: string })?.code
const uncertain = desktop && progress.issued && !['GET', 'HEAD'].includes(rest.method ?? 'GET')
const details = desktop ? { request_id: progress.requestId, outcome: progress.issued ? 'unknown' : 'not_sent' } : undefined
if (timedOut || nativeCode === 'REQUEST_TIMEOUT') throw new ApiErrorClass('REQUEST_TIMEOUT', uncertain ? '请求超时,变更可能已提交,请先检查结果。' : '请求超时,请检查连接。', details)
if (controller.signal.aborted || nativeCode === 'REQUEST_CANCELLED') throw new ApiErrorClass('REQUEST_CANCELLED', uncertain ? '请求已取消,变更可能已提交,请先检查结果。' : '请求已取消。', details)
if (e instanceof ApiErrorClass) throw e
throw new ApiErrorClass('NETWORK_ERROR', (e as Error).message || 'Network error')
} finally {