import { ApiError } from "./errors.js"; import type { FoxConfig } from "./config.js"; export interface RequestOptions { method?: string; body?: unknown; /** 此請求需要 Bearer token(缺 token 時報錯)。 */ auth?: boolean; } /** * 對 Fox 後端發起 HTTP 請求。 * * - 成功(2xx):解析後的 JSON 回應;204 回 null。 * - 非 2xx:丟 ApiError(帶 status 與後端訊息)。 * - 網路錯誤:丟 ApiError。 */ export async function request( config: FoxConfig, path: string, options: RequestOptions = {}, ): Promise { const { method = "GET", body, auth = false } = options; const headers: Record = { Accept: "application/json" }; if (body !== undefined) headers["Content-Type"] = "application/json"; if (auth) { if (!config.token) { throw new ApiError( "缺少 access token:請設定 FOX_API_TOKEN 環境變數、--token 參數,或 ~/.config/fox/cli.json(見 fox --help)", ); } headers.Authorization = `Bearer ${config.token}`; } let res: Response; try { res = await fetch(new URL(path, `${config.apiUrl}/`), { method, headers, body: body === undefined ? undefined : JSON.stringify(body), redirect: "manual", }); } catch (err) { throw new ApiError(`無法連線到 ${config.apiUrl}:${(err as Error).message}`); } if (res.status === 204) { await res.body?.cancel(); return null; } let data: unknown = null; const text = await res.text(); if (text) { try { data = JSON.parse(text); } catch { data = text; } } if (!res.ok) { const message = extractMessage(data) ?? `後端回應 ${res.status}`; throw new ApiError(message, res.status, data); } return data; } /** 從 NestJS 錯誤回應({message: string | string[]}抽取人類可讀訊息。 */ function extractMessage(data: unknown): string | null { if (data && typeof data === "object" && !Array.isArray(data)) { const message = (data as Record).message; if (typeof message === "string") return message; if (Array.isArray(message)) { const parts = message.filter((p): p is string => typeof p === "string"); if (parts.length > 0) return parts.join(";"); } } if (typeof data === "string" && data) return data; return null; }