Files
mactfs/mactfsui/app/lib/api/client.ts
T
fenghpandCursor 5c633fd024 frontend: add electron service bootstrap, preload and api client
FE-001: detect and launch the local mactfs-server, expose a narrow preload
bridge (token, service status, directory picker), and add a unified API
client that auto-attaches the Bearer token. Home becomes a bootstrap gate
with a retry entry. Update task board, FE-001 record and FRONTEND_SPEC.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-09 19:46:09 +08:00

118 lines
3.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { getApiBaseUrl, getToken } from "~/lib/electron"
import type { ApiRequestOptions, ApiResponse, ApiResult } from "./types"
/**
* 拼接查询参数到请求路径,自动跳过空值。
*/
function buildUrl(
baseUrl: string,
path: string,
query?: ApiRequestOptions["query"],
): string {
const url = new URL(path, baseUrl)
if (query) {
for (const [key, value] of Object.entries(query)) {
if (value !== undefined && value !== null) {
url.searchParams.set(key, String(value))
}
}
}
return url.toString()
}
/**
* 统一提取错误信息,遵循 errorMessage || message 的展示约定。
*/
function resolveErrorMessage(
result: ApiResult<unknown> | null,
fallback: string,
): string {
if (result) {
return result.errorMessage || result.message || fallback
}
return fallback
}
/**
* 发起一次本地 API 请求,自动携带 Bearer Token 并统一封装结果。
*/
async function request<T = Record<string, unknown>>(
method: string,
path: string,
options: ApiRequestOptions = {},
): Promise<ApiResponse<T>> {
const baseUrl = await getApiBaseUrl()
const token = await getToken()
const headers: Record<string, string> = {
Accept: "application/json",
}
if (token) {
headers.Authorization = `Bearer ${token}`
}
const init: RequestInit = { method, headers, signal: options.signal }
if (options.body !== undefined) {
headers["Content-Type"] = "application/json;charset=utf-8"
init.body = JSON.stringify(options.body)
}
let response: Response
try {
response = await fetch(buildUrl(baseUrl, path, options.query), init)
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
return {
ok: false,
status: 0,
result: null,
data: null,
errorMessage: `无法连接本地服务:${message}`,
}
}
let result: ApiResult<T> | null = null
try {
result = (await response.json()) as ApiResult<T>
} catch {
result = null
}
const ok = response.ok && result?.success === true
return {
ok,
status: response.status,
result,
data: result?.data ?? null,
errorMessage: ok
? null
: resolveErrorMessage(result, `请求失败(HTTP ${response.status})`),
}
}
// 统一 API 客户端,组件只通过本对象访问后端,不直接 fetch、不直接读 token。
export const apiClient = {
/**
* 发起 GET 请求。
*/
get<T = Record<string, unknown>>(path: string, options?: ApiRequestOptions) {
return request<T>("GET", path, options)
},
/**
* 发起 POST 请求。
*/
post<T = Record<string, unknown>>(path: string, options?: ApiRequestOptions) {
return request<T>("POST", path, options)
},
/**
* 发起 PUT 请求。
*/
put<T = Record<string, unknown>>(path: string, options?: ApiRequestOptions) {
return request<T>("PUT", path, options)
},
/**
* 发起 DELETE 请求。
*/
delete<T = Record<string, unknown>>(path: string, options?: ApiRequestOptions) {
return request<T>("DELETE", path, options)
},
}