Files
fox-cli/src/http.ts
T
chenyunda218 80b784a9f3 feat(cli): 實作 CLI 各指令與 help(M1+M2,issue #13)
- 加入 pnpm workspace;Node.js ≥22 + TypeScript 實作
- 指令:auth status、tokens verify/create/list/revoke、novels get
- 非互動、stdout 僅輸出單一 JSON;exit code 0/1/2
- 設定優先順序:--token/--api-url > FOX_API_TOKEN/FOX_API_URL > ~/.config/fox/cli.json
- novels list/create/update 待後端接受 Bearer token 後補齊(README 授權缺口)
- 已對模擬後端完成端到端驗證(含 401/404/網路錯誤、冪等 revoke)
2026-09-04 20:26:11 +08:00

82 lines
2.3 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 { 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<unknown> {
const { method = "GET", body, auth = false } = options;
const headers: Record<string, string> = { 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<string, unknown>).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;
}