127 lines
3.5 KiB
TypeScript
127 lines
3.5 KiB
TypeScript
export const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || "/api/v1";
|
|
|
|
const TOKEN_KEY = "cyber_mister_token";
|
|
const CLIENT_KEY = "cyber_mister_client_id";
|
|
|
|
export type HandSide = "left" | "right" | "unknown";
|
|
export type ReadingType = "palm" | "face" | "bazi";
|
|
|
|
export type Dimension = {
|
|
name: string;
|
|
observations: string[];
|
|
interpretation: string;
|
|
confidence: number;
|
|
advice: string;
|
|
};
|
|
|
|
export type ReportData = {
|
|
quality_check: {
|
|
can_analyze: boolean;
|
|
reason: string;
|
|
confidence: number;
|
|
};
|
|
overall_summary: string;
|
|
dimensions: Dimension[];
|
|
strengths: string[];
|
|
challenges: string[];
|
|
suggestions: string[];
|
|
lucky_keywords: string[];
|
|
disclaimer: string;
|
|
};
|
|
|
|
export type Reading = {
|
|
id: string;
|
|
reading_type: ReadingType;
|
|
status: "pending" | "processing" | "completed" | "failed";
|
|
input_data: Record<string, unknown>;
|
|
error_message?: string | null;
|
|
report_data?: ReportData | null;
|
|
created_at: string;
|
|
updated_at: string;
|
|
};
|
|
|
|
export type ReadingSummary = {
|
|
id: string;
|
|
reading_type: ReadingType;
|
|
status: Reading["status"];
|
|
created_at: string;
|
|
overall_summary?: string | null;
|
|
};
|
|
|
|
export type Quota = {
|
|
limit: number;
|
|
used: number;
|
|
remaining: number;
|
|
reset_at: string;
|
|
};
|
|
|
|
export type Report = Reading & { hand_side?: HandSide };
|
|
export type ReportSummary = ReadingSummary & { hand_side?: HandSide };
|
|
|
|
export function getStoredToken() {
|
|
if (typeof window === "undefined") return "";
|
|
return localStorage.getItem(TOKEN_KEY) || "";
|
|
}
|
|
|
|
export async function ensureToken() {
|
|
const existing = getStoredToken();
|
|
if (existing) return existing;
|
|
|
|
let clientId = localStorage.getItem(CLIENT_KEY);
|
|
if (!clientId) {
|
|
clientId = crypto.randomUUID();
|
|
localStorage.setItem(CLIENT_KEY, clientId);
|
|
}
|
|
|
|
const response = await fetch(`${API_BASE_URL}/auth/anonymous-login`, {
|
|
method: "POST",
|
|
headers: { "content-type": "application/json" },
|
|
body: JSON.stringify({ client_id: clientId }),
|
|
});
|
|
if (!response.ok) throw new Error("匿名会话创建失败");
|
|
const data = await response.json();
|
|
localStorage.setItem(TOKEN_KEY, data.access_token);
|
|
return data.access_token as string;
|
|
}
|
|
|
|
export async function apiFetch<T>(path: string, options: RequestInit = {}): Promise<T> {
|
|
const token = await ensureToken();
|
|
const headers = new Headers(options.headers);
|
|
if (!(options.body instanceof FormData) && !headers.has("content-type")) {
|
|
headers.set("content-type", "application/json");
|
|
}
|
|
headers.set("authorization", `Bearer ${token}`);
|
|
|
|
const response = await fetch(`${API_BASE_URL}${path}`, { ...options, headers });
|
|
if (!response.ok) {
|
|
const data = await response.json().catch(() => ({}));
|
|
throw new Error(data.detail || "请求失败");
|
|
}
|
|
return response.json() as Promise<T>;
|
|
}
|
|
|
|
export async function uploadPalmImage(file: File) {
|
|
return uploadImage(file, "palm");
|
|
}
|
|
|
|
export async function uploadFaceImage(file: File) {
|
|
return uploadImage(file, "face");
|
|
}
|
|
|
|
async function uploadImage(file: File, type: "palm" | "face") {
|
|
const token = await ensureToken();
|
|
const formData = new FormData();
|
|
formData.append("file", file);
|
|
|
|
const response = await fetch(`${API_BASE_URL}/uploads/${type}`, {
|
|
method: "POST",
|
|
headers: { authorization: `Bearer ${token}` },
|
|
body: formData,
|
|
});
|
|
if (!response.ok) {
|
|
const data = await response.json().catch(() => ({}));
|
|
throw new Error(data.detail || "上传失败");
|
|
}
|
|
return response.json() as Promise<{ image_id: string; quality_check: Record<string, unknown>; expires_at: string }>;
|
|
}
|