| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126 |
- /**
- * API 工具类
- * 统一管理后端 API 调用
- */
- import { getUserId, getUserToken } from './storage';
- /** 后端返回 detail 为 { code: string } 或旧版字符串 */
- function getDetailCode(detail: unknown): string | undefined {
- if (detail && typeof detail === 'object' && 'code' in detail && typeof (detail as { code: unknown }).code === 'string') {
- return (detail as { code: string }).code;
- }
- if (typeof detail === 'string') return detail;
- return undefined;
- }
- /**
- * 解析 4xx/5xx 响应为带 code 的 ApiError,供前端用 t('error.' + code) 展示
- */
- export class ApiError extends Error {
- code: string;
- status: number;
- constructor(code: string, status: number, message?: string) {
- super(message ?? code);
- this.name = 'ApiError';
- this.code = code;
- this.status = status;
- }
- }
- export async function parseApiError(response: Response, fallbackCode = 'UNKNOWN'): Promise<ApiError> {
- let code = fallbackCode;
- const contentType = response.headers.get('content-type') || '';
- if (contentType.includes('application/json')) {
- try {
- const body = await response.json();
- const detail = body?.detail;
- const parsed = getDetailCode(detail);
- if (parsed) code = parsed;
- } catch {
- // ignore
- }
- }
- return new ApiError(code, response.status, response.statusText);
- }
- /**
- * 获取 API Key(从环境变量读取)
- */
- const getApiKey = (): string => {
- return import.meta.env.VITE_API_CLIENT_KEY || '';
- };
- /**
- * 获取统一的请求头(包含 API Key 与用户会话信息)
- */
- export const getApiHeaders = (): Record<string, string> => {
- const headers: Record<string, string> = {
- 'Content-Type': 'application/json',
- };
-
- const apiKey = getApiKey();
- if (apiKey) {
- headers['X-Client-Key'] = apiKey;
- }
- // 附加用户会话信息(用于后端基于 token 的 HTTP 鉴权)
- const userId = getUserId();
- const token = getUserToken();
- if (userId && token) {
- headers['X-User-Id'] = userId;
- headers['X-User-Token'] = token;
- }
-
- return headers;
- };
- /**
- * 会话确保逻辑:
- * - 若本地已有 userId + token,直接复用(依赖服务端滑动续期,不主动换 token)
- * - 若没有,则通过 /api/session/init 创建一次会话
- * - 使用单例 Promise 防抖,避免并发多次刷新导致 token 频繁变更
- */
- let ensureSessionPromise: Promise<{ userId: string; token: string }> | null = null;
- export const ensureSession = async (): Promise<{ userId: string; token: string }> => {
- // 已有未完成的会话初始化,直接复用
- if (ensureSessionPromise) {
- return ensureSessionPromise;
- }
- // 动态导入,避免循环依赖
- const { getUserId: loadUserId, getUserToken: loadUserToken, setUserId, setUserToken } = await import('./storage');
- const existingUserId = loadUserId();
- const existingToken = loadUserToken();
- // 本地已经有 userId + token,则认为会话有效,直接返回
- if (existingUserId && existingToken) {
- return { userId: existingUserId, token: existingToken };
- }
- // 没有有效会话时,串行化一次 /api/session/init 调用
- ensureSessionPromise = (async () => {
- const resp = await initSession(existingUserId || undefined);
- if (!resp.success || !resp.data?.token || !resp.data?.user_id) {
- throw new Error('Session init failed');
- }
- setUserId(resp.data.user_id);
- setUserToken(resp.data.token);
- return { userId: resp.data.user_id, token: resp.data.token };
- })();
- try {
- return await ensureSessionPromise;
- } finally {
- ensureSessionPromise = null;
- }
- };
- /**
- * 获取后端 API 基础 URL
- */
- export const getApiBaseUrl = (): string => {
- // 开发环境:使用相对路径,通过 Vite 代理访问后端
- // 这样可以避免 Safari 对自签名证书的跨域限制
- if (import.meta.env.DEV) {
- return ''; // 使用相对路径,Vite 会代理到后端
- }
-
- // 生产环境:使用完整 URL
- // 优先使用环境变量指定的 API 地址
- if (import.meta.env.VITE_API_BASE_URL) {
- return import.meta.env.VITE_API_BASE_URL;
- }
-
- // 从 WebSocket URL 推导 API 基础 URL
- const wsUrl = import.meta.env.VITE_WS_URL || 'ws://localhost:8000/ws';
-
- // 将 ws:// 或 wss:// 转换为 http:// 或 https://
- const apiUrl = wsUrl
- .replace(/^wss:\/\//, 'https://')
- .replace(/^ws:\/\//, 'http://')
- .replace(/\/ws$/, '');
-
- return apiUrl;
- };
- /**
- * 上报页面访问(每次打开任意界面时调用,保存到后台数据库)
- * 不阻塞 UI,失败静默忽略。
- */
- export const reportPageVisit = (path: string): void => {
- const apiBaseUrl = getApiBaseUrl();
- const body = JSON.stringify({ path: path || '/' });
- fetch(`${apiBaseUrl}/api/page-visit`, {
- method: 'POST',
- headers: getApiHeaders(),
- body,
- }).catch(() => {});
- };
- export interface UserEventPayload {
- event_id: string;
- user_id: string;
- event_name: string;
- event_label?: string;
- event_time: string;
- session_id?: string;
- platform: 'ios' | 'android' | 'web';
- app_version?: string;
- timezone_offset_minutes?: number;
- properties?: Record<string, unknown>;
- }
- /**
- * 上报单条用户事件(user_events)
- * - 不抛异常,失败静默
- * - 开发环境会打印请求与响应状态,便于排查未落库问题
- */
- export const reportUserEvent = (payload: UserEventPayload): void => {
- const apiBaseUrl = getApiBaseUrl();
- const url = `${apiBaseUrl}/api/user-events`;
- const headers = getApiHeaders();
- if (import.meta.env.DEV) {
- const hasAuth = !!(headers['X-User-Id'] && headers['X-User-Token']);
- const keys = Object.keys(payload);
- const required = ['event_id', 'user_id', 'event_name', 'event_time', 'platform'];
- const missing = required.filter((k) => !(k in payload) || payload[k as keyof UserEventPayload] == null);
- if (missing.length) {
- console.warn('[user_events] 请求体缺少必填字段:', missing, '当前 keys:', keys);
- }
- console.log('[user_events] 上报:', payload.event_name, { hasAuth, user_id: payload.user_id, keys });
- }
- fetch(url, {
- method: 'POST',
- headers,
- body: JSON.stringify(payload),
- })
- .then(async (res) => {
- if (import.meta.env.DEV && !res.ok) {
- const body = await res.text();
- try {
- const parsed = JSON.parse(body);
- const details = Array.isArray(parsed?.detail) ? parsed.detail : parsed?.detail ? [parsed.detail] : [];
- const first = details[0];
- const loc = first?.loc;
- console.warn('[user_events] 上报失败:', res.status, payload.event_name, '缺失/错误字段 loc:', loc, 'msg:', first?.msg, '详情:', details);
- } catch {
- console.warn('[user_events] 上报失败:', res.status, res.statusText, payload.event_name, body?.slice(0, 200));
- }
- }
- })
- .catch((err) => {
- if (import.meta.env.DEV) {
- console.warn('[user_events] 请求异常:', err?.message ?? err, payload.event_name);
- }
- });
- };
- /**
- * 创建用户
- */
- export interface CreateUserResponse {
- success: boolean;
- data: {
- user_id: string;
- username: string;
- user_type: string;
- user_level: number;
- current_background_image_id?: string;
- created_at: string;
- };
- }
- export const createUser = async (username: string = ''): Promise<CreateUserResponse> => {
- const apiBaseUrl = getApiBaseUrl();
- const url = `${apiBaseUrl}/api/users/create`;
-
- try {
- const response = await fetch(url, {
- method: 'POST',
- headers: getApiHeaders(),
- body: JSON.stringify({ username }),
- });
- if (!response.ok) {
- throw await parseApiError(response, 'CREATE_USER_FAILED');
- }
- const data = await response.json();
-
- // 同步背景图片到 localStorage(包括metadata中的透明度配置)
- if (data.success && data.data?.current_background_image_id) {
- const { syncBackgroundImageToStorage } = await import('./storage');
- syncBackgroundImageToStorage(
- data.data.current_background_image_id,
- data.data.current_background_image_metadata
- );
- }
-
- return data;
- } catch (error) {
- console.error('[API] 创建用户异常:', error);
-
- // 如果是网络错误,提供更友好的提示
- if (error instanceof TypeError && error.message.includes('fetch')) {
- throw new Error('网络请求失败,请检查后端服务是否运行');
- }
-
- throw error;
- }
- };
- /**
- * 初始化/刷新会话,返回 user_id + token
- */
- export interface InitSessionResponse {
- success: boolean;
- data: {
- user_id: string;
- token: string;
- expires_at: string;
- username?: string;
- user_level?: number;
- current_background_image_id?: string;
- current_background_image_metadata?: Record<string, any>;
- };
- }
- export const initSession = async (user_id?: string, username?: string): Promise<InitSessionResponse> => {
- const apiBaseUrl = getApiBaseUrl();
- const url = `${apiBaseUrl}/api/session/init`;
- const body: Record<string, string> = {};
- if (user_id) {
- body.user_id = user_id;
- }
- if (username) {
- body.username = username;
- }
- const response = await fetch(url, {
- method: 'POST',
- headers: getApiHeaders(),
- body: JSON.stringify(body),
- });
- if (!response.ok) {
- throw await parseApiError(response, 'INIT_SESSION_FAILED');
- }
- return response.json();
- };
- /**
- * 获取用户信息
- */
- export interface GetUserResponse {
- success: boolean;
- data: {
- user_id: string;
- username: string;
- user_type: string;
- user_segment?: string;
- org_id?: string | null;
- is_test_user?: boolean;
- region?: string | null;
- bot_name?: string;
- total_speech_duration_ms?: number;
- user_level?: number;
- current_background_image_id?: string;
- current_background_image_metadata?: Record<string, any>;
- preferred_language?: 'zh' | 'en';
- created_at: string;
- };
- }
- export const getUserInfo = async (user_id: string): Promise<GetUserResponse> => {
- const apiBaseUrl = getApiBaseUrl();
- const response = await fetch(`${apiBaseUrl}/api/users/${user_id}`, {
- method: 'GET',
- headers: getApiHeaders(),
- });
- if (!response.ok) {
- throw await parseApiError(response, 'GET_USER_INFO_FAILED');
- }
- const data = await response.json();
-
- // 同步背景图片到 localStorage(包括metadata中的透明度配置)
- if (data.success && data.data?.current_background_image_id) {
- const { syncBackgroundImageToStorage } = await import('./storage');
- syncBackgroundImageToStorage(
- data.data.current_background_image_id,
- data.data.current_background_image_metadata
- );
- }
-
- return data;
- };
- export interface JournalLatestTopicData {
- scenario_key: string;
- topic_key: string;
- /** ISO 8601,个人页日记本场景图上的日期由此格式化 */
- conversed_at: string;
- }
- export interface JournalLatestTopicResponse {
- success: boolean;
- data: JournalLatestTopicData | null;
- }
- export const getJournalLatestTopic = async (user_id: string): Promise<JournalLatestTopicResponse> => {
- const apiBaseUrl = getApiBaseUrl();
- const response = await fetch(`${apiBaseUrl}/api/users/${user_id}/journal-latest-topic`, {
- method: 'GET',
- headers: getApiHeaders(),
- });
- if (!response.ok) {
- throw await parseApiError(response, 'SERVER_ERROR');
- }
- return response.json();
- };
- /**
- * 更新用户名
- */
- export interface UpdateUsernameResponse {
- success: boolean;
- message: string;
- }
- export const updateUsername = async (user_id: string, username: string): Promise<UpdateUsernameResponse> => {
- const apiBaseUrl = getApiBaseUrl();
- const response = await fetch(`${apiBaseUrl}/api/users/${user_id}/username`, {
- method: 'PUT',
- headers: getApiHeaders(),
- body: JSON.stringify({ username }),
- });
- if (!response.ok) {
- throw await parseApiError(response, 'UPDATE_USERNAME_FAILED');
- }
- return response.json();
- };
- /**
- * 更新背景图片ID
- */
- export interface UpdateBackgroundImageIdResponse {
- success: boolean;
- message: string;
- }
- export const updateBackgroundImageId = async (
- user_id: string,
- background_image_id: string,
- ): Promise<UpdateBackgroundImageIdResponse> => {
- const apiBaseUrl = getApiBaseUrl();
- const response = await fetch(`${apiBaseUrl}/api/users/${user_id}/background-image-id`, {
- method: 'PUT',
- headers: getApiHeaders(),
- body: JSON.stringify({ background_image_id }),
- });
- if (!response.ok) {
- throw await parseApiError(response, 'UPDATE_BG_IMAGE_FAILED');
- }
- return response.json();
- };
- /**
- * 更新机器人名字
- */
- export interface UpdateBotNameResponse {
- success: boolean;
- message: string;
- }
- export const updateBotName = async (user_id: string, bot_name: string): Promise<UpdateBotNameResponse> => {
- const apiBaseUrl = getApiBaseUrl();
- const response = await fetch(`${apiBaseUrl}/api/users/${user_id}/bot-name`, {
- method: 'PUT',
- headers: getApiHeaders(),
- body: JSON.stringify({ bot_name }),
- });
- if (!response.ok) {
- throw await parseApiError(response, 'UPDATE_BOT_NAME_FAILED');
- }
- return response.json();
- };
- /**
- * 更新用户首选界面语言(zh/en)
- */
- export interface UpdatePreferredLanguageResponse {
- success: boolean;
- message?: string;
- }
- export const updatePreferredLanguage = async (
- user_id: string,
- preferred_language: 'zh' | 'en',
- ): Promise<UpdatePreferredLanguageResponse> => {
- const apiBaseUrl = getApiBaseUrl();
- const response = await fetch(`${apiBaseUrl}/api/users/${user_id}/preferred-language`, {
- method: 'PUT',
- headers: getApiHeaders(),
- body: JSON.stringify({ preferred_language }),
- });
- if (!response.ok) {
- throw await parseApiError(response, 'UPDATE_PREFERRED_LANGUAGE_FAILED');
- }
- return response.json();
- };
- /**
- * 用户 UI 状态相关接口
- */
- export interface UserUIState {
- current_page: string | null;
- visited_routes: string[];
- has_shown_tap_to_chat: boolean;
- has_shown_cancel_tooltip: boolean;
- has_shown_send_tooltip: boolean;
- has_shown_progress_bar_tooltip: boolean;
- has_shown_mission_stone_modal: boolean;
- has_shown_conversation_stone_modal: boolean;
- has_clicked_mission_stone: boolean;
- has_clicked_conversation_stone: boolean;
- subtitle_on: boolean;
- avatar_c_unlocked: boolean;
- has_shared_avatar: boolean;
- current_avatar_id?: string | null;
- }
- export interface GetUserUIStateResponse {
- success: boolean;
- data: {
- user_id: string;
- } & UserUIState;
- }
- export const getUserUIState = async (user_id: string): Promise<GetUserUIStateResponse> => {
- const apiBaseUrl = getApiBaseUrl();
- const response = await fetch(`${apiBaseUrl}/api/users/${user_id}/ui-state`, {
- method: 'GET',
- headers: getApiHeaders(),
- });
- if (!response.ok) {
- throw await parseApiError(response, 'GET_UI_STATE_FAILED');
- }
- return response.json();
- };
- export interface UpdateUserUIStateResponse {
- success: boolean;
- data: {
- user_id: string;
- } & UserUIState;
- }
- export type UpdateUserUIStatePayload = Partial<UserUIState>;
- export const updateUserUIState = async (
- user_id: string,
- payload: UpdateUserUIStatePayload,
- ): Promise<UpdateUserUIStateResponse> => {
- const apiBaseUrl = getApiBaseUrl();
- const response = await fetch(`${apiBaseUrl}/api/users/${user_id}/ui-state`, {
- method: 'PUT',
- headers: getApiHeaders(),
- body: JSON.stringify(payload),
- });
- if (!response.ok) {
- throw await parseApiError(response, 'UPDATE_UI_STATE_FAILED');
- }
- return response.json();
- };
- /** 亲密度:heart_count 0-3,days_since_last_visit 距上次访问天数 */
- export interface GetIntimacyResponse {
- success: boolean;
- data: { heart_count: number; days_since_last_visit: number };
- }
- export const getIntimacy = async (user_id: string): Promise<GetIntimacyResponse> => {
- const apiBaseUrl = getApiBaseUrl();
- const response = await fetch(`${apiBaseUrl}/api/users/${user_id}/intimacy`, {
- method: 'GET',
- headers: getApiHeaders(),
- });
- if (!response.ok) {
- throw await parseApiError(response, 'GET_INTIMACY_FAILED');
- }
- return response.json();
- };
- export interface LightUpIntimacyResponse {
- success: boolean;
- data: { heart_count: number };
- }
- export const lightUpIntimacy = async (user_id: string): Promise<LightUpIntimacyResponse> => {
- const apiBaseUrl = getApiBaseUrl();
- const response = await fetch(`${apiBaseUrl}/api/users/${user_id}/intimacy/light-up`, {
- method: 'POST',
- headers: getApiHeaders(),
- });
- if (!response.ok) {
- throw await parseApiError(response, 'LIGHT_UP_INTIMACY_FAILED');
- }
- return response.json();
- };
- export interface GetIntimacyStatusResponse {
- success: boolean;
- data: {
- heart_count: number;
- can_light_up_today: boolean;
- };
- }
- export const getIntimacyStatus = async (user_id: string): Promise<GetIntimacyStatusResponse> => {
- const apiBaseUrl = getApiBaseUrl();
- const response = await fetch(`${apiBaseUrl}/api/users/${user_id}/intimacy/status`, {
- method: 'GET',
- headers: getApiHeaders(),
- });
- if (!response.ok) {
- throw await parseApiError(response, 'GET_INTIMACY_STATUS_FAILED');
- }
- return response.json();
- };
- /**
- * 注册/登录相关接口
- */
- // 手机号注册
- export interface RegisterPhoneResponse {
- success: boolean;
- data: {
- user_id: string;
- username: string;
- area_code?: string;
- phone: string;
- user_type: string;
- user_level?: number;
- token?: string;
- expires_at?: string;
- created_at: string;
- };
- }
- export const registerWithPhone = async (
- phone: string,
- username: string = '',
- user_id?: string | null,
- area_code?: string,
- ): Promise<RegisterPhoneResponse> => {
- const apiBaseUrl = getApiBaseUrl();
- const body: { phone: string; username: string; user_id?: string; area_code?: string } = { phone, username };
- if (user_id) {
- body.user_id = user_id;
- }
- if (area_code) {
- body.area_code = area_code;
- }
- const response = await fetch(`${apiBaseUrl}/api/auth/register/phone`, {
- method: 'POST',
- headers: getApiHeaders(),
- body: JSON.stringify(body),
- });
- if (!response.ok) {
- throw await parseApiError(response, 'REGISTER_FAILED');
- }
- return response.json();
- };
- // 邮箱注册
- export interface RegisterEmailResponse {
- success: boolean;
- data: {
- user_id: string;
- username: string;
- email: string;
- user_type: string;
- user_level?: number;
- token?: string;
- expires_at?: string;
- created_at: string;
- };
- }
- export const registerWithEmail = async (email: string, username: string = '', user_id?: string | null): Promise<RegisterEmailResponse> => {
- const apiBaseUrl = getApiBaseUrl();
- const body: { email: string; username: string; user_id?: string } = { email, username };
- if (user_id) {
- body.user_id = user_id;
- }
- const response = await fetch(`${apiBaseUrl}/api/auth/register/email`, {
- method: 'POST',
- headers: getApiHeaders(),
- body: JSON.stringify(body),
- });
- if (!response.ok) {
- throw await parseApiError(response, 'REGISTER_FAILED');
- }
- return response.json();
- };
- // 微信登录
- export interface LoginWechatResponse {
- success: boolean;
- data: {
- user_id: string;
- username: string;
- wechat_openid?: string;
- user_type: string;
- user_level?: number;
- token?: string;
- expires_at?: string;
- created_at: string;
- };
- }
- export const loginWithWechat = async (openid: string, username: string = ''): Promise<LoginWechatResponse> => {
- const apiBaseUrl = getApiBaseUrl();
- const response = await fetch(`${apiBaseUrl}/api/auth/login/wechat`, {
- method: 'POST',
- headers: getApiHeaders(),
- body: JSON.stringify({ openid, username }),
- });
- if (!response.ok) {
- throw await parseApiError(response, 'LOGIN_FAILED');
- }
- return response.json();
- };
- // QQ登录
- export interface LoginQQResponse {
- success: boolean;
- data: {
- user_id: string;
- username: string;
- qq_openid?: string;
- user_type: string;
- user_level?: number;
- token?: string;
- expires_at?: string;
- created_at: string;
- };
- }
- export const loginWithQQ = async (openid: string, username: string = ''): Promise<LoginQQResponse> => {
- const apiBaseUrl = getApiBaseUrl();
- const response = await fetch(`${apiBaseUrl}/api/auth/login/qq`, {
- method: 'POST',
- headers: getApiHeaders(),
- body: JSON.stringify({ openid, username }),
- });
- if (!response.ok) {
- throw await parseApiError(response, 'LOGIN_FAILED');
- }
- return response.json();
- };
- // 手机号登录
- export interface LoginPhoneResponse {
- success: boolean;
- data: {
- user_id: string;
- username: string;
- area_code?: string;
- phone: string;
- user_type: string;
- user_level?: number;
- token?: string;
- expires_at?: string;
- created_at: string;
- };
- }
- export const loginWithPhone = async (phone: string, area_code?: string): Promise<LoginPhoneResponse> => {
- const apiBaseUrl = getApiBaseUrl();
- const response = await fetch(`${apiBaseUrl}/api/auth/login/phone`, {
- method: 'POST',
- headers: getApiHeaders(),
- body: JSON.stringify({ phone, area_code }),
- });
- if (!response.ok) {
- throw await parseApiError(response, 'LOGIN_FAILED');
- }
- return response.json();
- };
- // 邮箱登录
- export interface LoginEmailResponse {
- success: boolean;
- data: {
- user_id: string;
- username: string;
- email: string;
- user_type: string;
- user_level?: number;
- token?: string;
- expires_at?: string;
- created_at: string;
- };
- }
- export const loginWithEmail = async (email: string): Promise<LoginEmailResponse> => {
- const apiBaseUrl = getApiBaseUrl();
- const response = await fetch(`${apiBaseUrl}/api/auth/login/email`, {
- method: 'POST',
- headers: getApiHeaders(),
- body: JSON.stringify({ email }),
- });
- if (!response.ok) {
- throw await parseApiError(response, 'LOGIN_FAILED');
- }
- return response.json();
- };
- // 发送验证码
- export interface SendCodeRequest {
- area_code?: string; // 手机区号(如 +86)
- phone?: string;
- email?: string;
- send_type: 'domestic_sms' | 'international_sms' | 'email';
- }
- export interface SendCodeResponse {
- success: boolean;
- data: {
- code_id: string;
- message: string;
- };
- }
- export const sendCode = async (request: SendCodeRequest): Promise<SendCodeResponse> => {
- const apiBaseUrl = getApiBaseUrl();
- const response = await fetch(`${apiBaseUrl}/api/auth/send-code`, {
- method: 'POST',
- headers: getApiHeaders(),
- body: JSON.stringify(request),
- });
- if (!response.ok) {
- throw await parseApiError(response, 'SEND_CODE_FAILED');
- }
- return response.json();
- };
- // 验证验证码
- export interface VerifyCodeRequest {
- phone?: string;
- email?: string;
- code: string;
- send_type: 'domestic_sms' | 'international_sms' | 'email';
- }
- export interface VerifyCodeResponse {
- success: boolean;
- data: {
- valid: boolean;
- message: string;
- };
- }
- export const verifyCode = async (request: VerifyCodeRequest): Promise<VerifyCodeResponse> => {
- const apiBaseUrl = getApiBaseUrl();
- const response = await fetch(`${apiBaseUrl}/api/auth/verify-code`, {
- method: 'POST',
- headers: getApiHeaders(),
- body: JSON.stringify(request),
- });
- if (!response.ok) {
- throw await parseApiError(response, 'VERIFY_CODE_FAILED');
- }
- return response.json();
- };
- // V2.0:验证码通过后统一登录/注册
- export interface VerifyAndAuthRequest {
- type: 'email' | 'phone';
- code: string;
- email?: string;
- phone?: string;
- area_code?: string;
- preferred_language?: 'zh' | 'en';
- }
- export interface VerifyAndAuthResponse {
- success: boolean;
- data: {
- user_id: string;
- username?: string;
- email?: string;
- phone?: string;
- user_type: string;
- user_level?: number;
- token?: string;
- expires_at?: string;
- is_new_user: boolean;
- [key: string]: unknown;
- };
- }
- export const verifyAndAuth = async (request: VerifyAndAuthRequest): Promise<VerifyAndAuthResponse> => {
- const apiBaseUrl = getApiBaseUrl();
- const response = await fetch(`${apiBaseUrl}/api/auth/verify-and-auth`, {
- method: 'POST',
- headers: getApiHeaders(),
- body: JSON.stringify(request),
- });
- if (!response.ok) {
- throw await parseApiError(response, 'CODE_INVALID');
- }
- return response.json();
- };
- // V2.0:获取「退出早期共创社区」选项(登录页回显)
- export const getCommunityChoice = async (userId: string): Promise<{ success: boolean; data: { opted_out?: boolean } }> => {
- const apiBaseUrl = getApiBaseUrl();
- const response = await fetch(`${apiBaseUrl}/api/users/${userId}/community-choice`, {
- method: 'GET',
- headers: getApiHeaders(),
- });
- if (!response.ok) throw await parseApiError(response, 'SERVER_ERROR');
- return response.json();
- };
- // V2.0:更新「退出早期共创社区」选项
- export const updateCommunityChoice = async (userId: string, optedOut: boolean): Promise<{ success: boolean }> => {
- const apiBaseUrl = getApiBaseUrl();
- const response = await fetch(`${apiBaseUrl}/api/users/${userId}/community-choice`, {
- method: 'PUT',
- headers: getApiHeaders(),
- body: JSON.stringify({ opted_out: optedOut }),
- });
- if (!response.ok) throw await parseApiError(response, 'SERVER_ERROR');
- return response.json();
- };
- // V2.0:获取早期共创状态
- export const getEarlyCommunity = async (userId: string): Promise<{ success: boolean; data: { popup_shown?: boolean; email?: string | null } }> => {
- const apiBaseUrl = getApiBaseUrl();
- const response = await fetch(`${apiBaseUrl}/api/users/${userId}/early-community`, {
- method: 'GET',
- headers: getApiHeaders(),
- });
- if (!response.ok) throw await parseApiError(response, 'SERVER_ERROR');
- return response.json();
- };
- // V2.0:提交早期共创(标记弹窗已展示,可选留邮箱)
- export const submitEarlyCommunity = async (userId: string, email?: string | null): Promise<{ success: boolean; data: Record<string, unknown> }> => {
- const apiBaseUrl = getApiBaseUrl();
- const response = await fetch(`${apiBaseUrl}/api/users/${userId}/early-community`, {
- method: 'POST',
- headers: getApiHeaders(),
- body: JSON.stringify({ email: email || undefined }),
- });
- if (!response.ok) throw await parseApiError(response, 'SERVER_ERROR');
- return response.json();
- };
- /**
- * OAuth 授权相关接口
- */
- // 获取微信授权URL
- export const getWechatAuthUrl = (): string => {
- const apiBaseUrl = getApiBaseUrl();
- return `${apiBaseUrl}/api/auth/wechat/authorize`;
- };
- // 获取QQ授权URL
- export const getQQAuthUrl = (from?: string, user_id?: string | null): string => {
- const apiBaseUrl = getApiBaseUrl();
- const url = `${apiBaseUrl}/api/auth/qq/authorize`;
- const params = new URLSearchParams();
- if (from) {
- params.append('from', from);
- }
- if (user_id) {
- params.append('user_id', user_id);
- }
- const queryString = params.toString();
- return queryString ? `${url}?${queryString}` : url;
- };
- /**
- * 头像 GIF 配置
- */
- export interface AvatarGifSet {
- default: string;
- positive: string;
- negative: string;
- talking: string;
- }
- export interface AvatarGifConfigResponse {
- success: boolean;
- data: {
- avatar_default?: Partial<AvatarGifSet>;
- avatar_registered_cap?: Partial<AvatarGifSet>;
- };
- }
- export const getAvatarGifs = async (): Promise<AvatarGifConfigResponse> => {
- const apiBaseUrl = getApiBaseUrl();
- const response = await fetch(`${apiBaseUrl}/api/avatar-gifs`, {
- method: 'GET',
- headers: getApiHeaders(),
- });
- if (!response.ok) {
- throw await parseApiError(response, 'GET_AVATARS_FAILED');
- }
- return response.json();
- };
- /**
- * 分享图片
- */
- export interface ShareImageItem {
- id: string;
- url: string;
- title?: string;
- sort_order?: number;
- is_active?: boolean;
- }
- export interface GetShareImagesResponse {
- success: boolean;
- data: ShareImageItem[];
- }
- export const getShareImages = async (): Promise<GetShareImagesResponse> => {
- const apiBaseUrl = getApiBaseUrl();
- const response = await fetch(`${apiBaseUrl}/api/share-images`, {
- method: 'GET',
- headers: getApiHeaders(),
- });
- if (!response.ok) {
- throw await parseApiError(response, 'GET_CONFIG_FAILED');
- }
- return response.json();
- };
- /**
- * 领取分享奖励
- */
- export interface ClaimShareRewardResponse {
- success: boolean;
- data?: {
- user_id: string;
- background_image_id: string;
- background_image_metadata?: Record<string, any>;
- message: string;
- };
- message?: string;
- }
- export const claimShareReward = async (user_id: string): Promise<ClaimShareRewardResponse> => {
- const apiBaseUrl = getApiBaseUrl();
- const response = await fetch(`${apiBaseUrl}/api/users/${user_id}/claim-share-reward`, {
- method: 'POST',
- headers: getApiHeaders(),
- });
- if (!response.ok) {
- throw await parseApiError(response, 'CLAIM_SHARE_REWARD_FAILED');
- }
- const data = await response.json();
-
- // 如果成功,同步背景图片到 localStorage
- if (data.success && data.data?.background_image_id) {
- const { syncBackgroundImageToStorage } = await import('./storage');
- syncBackgroundImageToStorage(
- data.data.background_image_id,
- data.data.background_image_metadata || { opacity: 0.5 } // 使用后端返回的metadata,或默认透明度
- );
- }
-
- return data;
- };
- /**
- * 获取所有背景图片配置
- */
- export interface BackgroundImageConfig {
- image_id: string;
- image_name: string;
- image_url: string;
- thumbnail_url?: string;
- unlock_type: 'default' | 'level' | 'purchase' | 'mission';
- unlock_level?: number;
- unlock_mission_type?: string;
- display_order: number;
- is_active: boolean;
- description?: string;
- metadata?: Record<string, any>;
- created_at?: string;
- updated_at?: string;
- }
- export interface GetBackgroundImagesResponse {
- success: boolean;
- data: BackgroundImageConfig[];
- }
- export const getBackgroundImages = async (): Promise<GetBackgroundImagesResponse> => {
- const apiBaseUrl = getApiBaseUrl();
- const response = await fetch(`${apiBaseUrl}/api/background-images`, {
- method: 'GET',
- headers: getApiHeaders(),
- });
- if (!response.ok) {
- throw await parseApiError(response, 'GET_BG_CONFIG_FAILED');
- }
- return response.json();
- };
|