api.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732
  1. /**
  2. * API 工具类
  3. * 统一管理后端 API 调用
  4. */
  5. /**
  6. * 获取 API Key(从环境变量读取)
  7. */
  8. const getApiKey = (): string => {
  9. return import.meta.env.VITE_API_CLIENT_KEY || '';
  10. };
  11. /**
  12. * 获取统一的请求头(包含 API Key)
  13. */
  14. const getApiHeaders = (): Record<string, string> => {
  15. const headers: Record<string, string> = {
  16. 'Content-Type': 'application/json',
  17. };
  18. const apiKey = getApiKey();
  19. if (apiKey) {
  20. headers['X-Client-Key'] = apiKey;
  21. }
  22. return headers;
  23. };
  24. /**
  25. * 获取后端 API 基础 URL
  26. */
  27. const getApiBaseUrl = (): string => {
  28. // 开发环境:使用相对路径,通过 Vite 代理访问后端
  29. // 这样可以避免 Safari 对自签名证书的跨域限制
  30. if (import.meta.env.DEV) {
  31. return ''; // 使用相对路径,Vite 会代理到后端
  32. }
  33. // 生产环境:使用完整 URL
  34. // 优先使用环境变量指定的 API 地址
  35. if (import.meta.env.VITE_API_BASE_URL) {
  36. return import.meta.env.VITE_API_BASE_URL;
  37. }
  38. // 从 WebSocket URL 推导 API 基础 URL
  39. const wsUrl = import.meta.env.VITE_WS_URL || 'ws://localhost:8000/ws';
  40. // 将 ws:// 或 wss:// 转换为 http:// 或 https://
  41. const apiUrl = wsUrl
  42. .replace(/^wss:\/\//, 'https://')
  43. .replace(/^ws:\/\//, 'http://')
  44. .replace(/\/ws$/, '');
  45. return apiUrl;
  46. };
  47. /**
  48. * 创建用户
  49. */
  50. export interface CreateUserResponse {
  51. success: boolean;
  52. data: {
  53. user_id: string;
  54. username: string;
  55. user_type: string;
  56. user_level: number;
  57. current_background_image_id?: string;
  58. created_at: string;
  59. };
  60. }
  61. export const createUser = async (username: string = ''): Promise<CreateUserResponse> => {
  62. const apiBaseUrl = getApiBaseUrl();
  63. const url = `${apiBaseUrl}/api/users/create`;
  64. console.log('[API] 创建用户请求:', {
  65. url,
  66. apiBaseUrl,
  67. isDev: import.meta.env.DEV,
  68. });
  69. try {
  70. const response = await fetch(url, {
  71. method: 'POST',
  72. headers: getApiHeaders(),
  73. body: JSON.stringify({ username }),
  74. });
  75. console.log('[API] 创建用户响应:', {
  76. status: response.status,
  77. statusText: response.statusText,
  78. ok: response.ok,
  79. });
  80. if (!response.ok) {
  81. const errorText = await response.text().catch(() => '无法读取错误信息');
  82. console.error('[API] 创建用户失败:', {
  83. status: response.status,
  84. statusText: response.statusText,
  85. errorText,
  86. });
  87. throw new Error(`创建用户失败: ${response.status} ${response.statusText} - ${errorText}`);
  88. }
  89. const data = await response.json();
  90. console.log('[API] 创建用户成功:', data);
  91. // 同步背景图片到 localStorage(包括metadata中的透明度配置)
  92. if (data.success && data.data?.current_background_image_id) {
  93. const { syncBackgroundImageToStorage } = await import('./storage');
  94. syncBackgroundImageToStorage(
  95. data.data.current_background_image_id,
  96. data.data.current_background_image_metadata
  97. );
  98. }
  99. return data;
  100. } catch (error) {
  101. console.error('[API] 创建用户异常:', error);
  102. // 如果是网络错误,提供更友好的提示
  103. if (error instanceof TypeError && error.message.includes('fetch')) {
  104. throw new Error('网络请求失败,请检查后端服务是否运行');
  105. }
  106. throw error;
  107. }
  108. };
  109. /**
  110. * 获取用户信息
  111. */
  112. export interface GetUserResponse {
  113. success: boolean;
  114. data: {
  115. user_id: string;
  116. username: string;
  117. user_type: string;
  118. bot_name?: string;
  119. total_speech_duration_ms?: number;
  120. user_level?: number;
  121. current_background_image_id?: string;
  122. current_background_image_metadata?: Record<string, any>;
  123. created_at: string;
  124. };
  125. }
  126. export const getUserInfo = async (user_id: string): Promise<GetUserResponse> => {
  127. const apiBaseUrl = getApiBaseUrl();
  128. const response = await fetch(`${apiBaseUrl}/api/users/${user_id}`, {
  129. method: 'GET',
  130. headers: getApiHeaders(),
  131. });
  132. if (!response.ok) {
  133. throw new Error(`获取用户信息失败: ${response.statusText}`);
  134. }
  135. const data = await response.json();
  136. // 同步背景图片到 localStorage(包括metadata中的透明度配置)
  137. if (data.success && data.data?.current_background_image_id) {
  138. const { syncBackgroundImageToStorage } = await import('./storage');
  139. syncBackgroundImageToStorage(
  140. data.data.current_background_image_id,
  141. data.data.current_background_image_metadata
  142. );
  143. }
  144. return data;
  145. };
  146. /**
  147. * 更新用户名
  148. */
  149. export interface UpdateUsernameResponse {
  150. success: boolean;
  151. message: string;
  152. }
  153. export const updateUsername = async (user_id: string, username: string): Promise<UpdateUsernameResponse> => {
  154. const apiBaseUrl = getApiBaseUrl();
  155. const response = await fetch(`${apiBaseUrl}/api/users/${user_id}/username`, {
  156. method: 'PUT',
  157. headers: getApiHeaders(),
  158. body: JSON.stringify({ username }),
  159. });
  160. if (!response.ok) {
  161. throw new Error(`更新用户名失败: ${response.statusText}`);
  162. }
  163. return response.json();
  164. };
  165. /**
  166. * 更新背景图片ID
  167. */
  168. export interface UpdateBackgroundImageIdResponse {
  169. success: boolean;
  170. message: string;
  171. }
  172. export const updateBackgroundImageId = async (
  173. user_id: string,
  174. background_image_id: string,
  175. ): Promise<UpdateBackgroundImageIdResponse> => {
  176. const apiBaseUrl = getApiBaseUrl();
  177. const response = await fetch(`${apiBaseUrl}/api/users/${user_id}/background-image-id`, {
  178. method: 'PUT',
  179. headers: getApiHeaders(),
  180. body: JSON.stringify({ background_image_id }),
  181. });
  182. if (!response.ok) {
  183. throw new Error(`更新背景图片ID失败: ${response.statusText}`);
  184. }
  185. return response.json();
  186. };
  187. /**
  188. * 更新机器人名字
  189. */
  190. export interface UpdateBotNameResponse {
  191. success: boolean;
  192. message: string;
  193. }
  194. export const updateBotName = async (user_id: string, bot_name: string): Promise<UpdateBotNameResponse> => {
  195. const apiBaseUrl = getApiBaseUrl();
  196. const response = await fetch(`${apiBaseUrl}/api/users/${user_id}/bot-name`, {
  197. method: 'PUT',
  198. headers: getApiHeaders(),
  199. body: JSON.stringify({ bot_name }),
  200. });
  201. if (!response.ok) {
  202. throw new Error(`更新机器人名字失败: ${response.statusText}`);
  203. }
  204. return response.json();
  205. };
  206. /**
  207. * 用户 UI 状态相关接口
  208. */
  209. export interface UserUIState {
  210. current_page: string | null;
  211. visited_routes: string[];
  212. has_shown_tap_to_chat: boolean;
  213. has_shown_cancel_tooltip: boolean;
  214. has_shown_send_tooltip: boolean;
  215. has_shown_progress_bar_tooltip: boolean;
  216. has_shown_mission_stone_modal: boolean;
  217. has_shown_conversation_stone_modal: boolean;
  218. has_clicked_mission_stone: boolean;
  219. has_clicked_conversation_stone: boolean;
  220. avatar_c_unlocked: boolean;
  221. has_shared_avatar: boolean;
  222. current_avatar_id?: string | null;
  223. }
  224. export interface GetUserUIStateResponse {
  225. success: boolean;
  226. data: {
  227. user_id: string;
  228. } & UserUIState;
  229. }
  230. export const getUserUIState = async (user_id: string): Promise<GetUserUIStateResponse> => {
  231. const apiBaseUrl = getApiBaseUrl();
  232. const response = await fetch(`${apiBaseUrl}/api/users/${user_id}/ui-state`, {
  233. method: 'GET',
  234. headers: getApiHeaders(),
  235. });
  236. if (!response.ok) {
  237. throw new Error(`获取用户UI状态失败: ${response.statusText}`);
  238. }
  239. return response.json();
  240. };
  241. export interface UpdateUserUIStateResponse {
  242. success: boolean;
  243. data: {
  244. user_id: string;
  245. } & UserUIState;
  246. }
  247. export type UpdateUserUIStatePayload = Partial<UserUIState>;
  248. export const updateUserUIState = async (
  249. user_id: string,
  250. payload: UpdateUserUIStatePayload,
  251. ): Promise<UpdateUserUIStateResponse> => {
  252. const apiBaseUrl = getApiBaseUrl();
  253. const response = await fetch(`${apiBaseUrl}/api/users/${user_id}/ui-state`, {
  254. method: 'PUT',
  255. headers: getApiHeaders(),
  256. body: JSON.stringify(payload),
  257. });
  258. if (!response.ok) {
  259. throw new Error(`更新用户UI状态失败: ${response.statusText}`);
  260. }
  261. return response.json();
  262. };
  263. /**
  264. * 注册/登录相关接口
  265. */
  266. // 手机号注册
  267. export interface RegisterPhoneResponse {
  268. success: boolean;
  269. data: {
  270. user_id: string;
  271. username: string;
  272. area_code?: string;
  273. phone: string;
  274. user_type: string;
  275. user_level?: number;
  276. created_at: string;
  277. };
  278. }
  279. export const registerWithPhone = async (
  280. phone: string,
  281. username: string = '',
  282. user_id?: string | null,
  283. area_code?: string,
  284. ): Promise<RegisterPhoneResponse> => {
  285. const apiBaseUrl = getApiBaseUrl();
  286. const body: { phone: string; username: string; user_id?: string; area_code?: string } = { phone, username };
  287. if (user_id) {
  288. body.user_id = user_id;
  289. }
  290. if (area_code) {
  291. body.area_code = area_code;
  292. }
  293. const response = await fetch(`${apiBaseUrl}/api/auth/register/phone`, {
  294. method: 'POST',
  295. headers: getApiHeaders(),
  296. body: JSON.stringify(body),
  297. });
  298. if (!response.ok) {
  299. const errorText = await response.text().catch(() => '注册失败');
  300. throw new Error(errorText);
  301. }
  302. return response.json();
  303. };
  304. // 邮箱注册
  305. export interface RegisterEmailResponse {
  306. success: boolean;
  307. data: {
  308. user_id: string;
  309. username: string;
  310. email: string;
  311. user_type: string;
  312. user_level?: number;
  313. created_at: string;
  314. };
  315. }
  316. export const registerWithEmail = async (email: string, username: string = '', user_id?: string | null): Promise<RegisterEmailResponse> => {
  317. const apiBaseUrl = getApiBaseUrl();
  318. const body: { email: string; username: string; user_id?: string } = { email, username };
  319. if (user_id) {
  320. body.user_id = user_id;
  321. }
  322. const response = await fetch(`${apiBaseUrl}/api/auth/register/email`, {
  323. method: 'POST',
  324. headers: getApiHeaders(),
  325. body: JSON.stringify(body),
  326. });
  327. if (!response.ok) {
  328. const errorText = await response.text().catch(() => '注册失败');
  329. throw new Error(errorText);
  330. }
  331. return response.json();
  332. };
  333. // 微信登录
  334. export interface LoginWechatResponse {
  335. success: boolean;
  336. data: {
  337. user_id: string;
  338. username: string;
  339. wechat_openid?: string;
  340. user_type: string;
  341. user_level?: number;
  342. created_at: string;
  343. };
  344. }
  345. export const loginWithWechat = async (openid: string, username: string = ''): Promise<LoginWechatResponse> => {
  346. const apiBaseUrl = getApiBaseUrl();
  347. const response = await fetch(`${apiBaseUrl}/api/auth/login/wechat`, {
  348. method: 'POST',
  349. headers: getApiHeaders(),
  350. body: JSON.stringify({ openid, username }),
  351. });
  352. if (!response.ok) {
  353. const errorText = await response.text().catch(() => '登录失败');
  354. throw new Error(errorText);
  355. }
  356. return response.json();
  357. };
  358. // QQ登录
  359. export interface LoginQQResponse {
  360. success: boolean;
  361. data: {
  362. user_id: string;
  363. username: string;
  364. qq_openid?: string;
  365. user_type: string;
  366. user_level?: number;
  367. created_at: string;
  368. };
  369. }
  370. export const loginWithQQ = async (openid: string, username: string = ''): Promise<LoginQQResponse> => {
  371. const apiBaseUrl = getApiBaseUrl();
  372. const response = await fetch(`${apiBaseUrl}/api/auth/login/qq`, {
  373. method: 'POST',
  374. headers: getApiHeaders(),
  375. body: JSON.stringify({ openid, username }),
  376. });
  377. if (!response.ok) {
  378. const errorText = await response.text().catch(() => '登录失败');
  379. throw new Error(errorText);
  380. }
  381. return response.json();
  382. };
  383. // 手机号登录
  384. export interface LoginPhoneResponse {
  385. success: boolean;
  386. data: {
  387. user_id: string;
  388. username: string;
  389. area_code?: string;
  390. phone: string;
  391. user_type: string;
  392. user_level?: number;
  393. created_at: string;
  394. };
  395. }
  396. export const loginWithPhone = async (phone: string, area_code?: string): Promise<LoginPhoneResponse> => {
  397. const apiBaseUrl = getApiBaseUrl();
  398. const response = await fetch(`${apiBaseUrl}/api/auth/login/phone`, {
  399. method: 'POST',
  400. headers: getApiHeaders(),
  401. body: JSON.stringify({ phone, area_code }),
  402. });
  403. if (!response.ok) {
  404. const errorText = await response.text().catch(() => '登录失败');
  405. throw new Error(errorText);
  406. }
  407. return response.json();
  408. };
  409. // 邮箱登录
  410. export interface LoginEmailResponse {
  411. success: boolean;
  412. data: {
  413. user_id: string;
  414. username: string;
  415. email: string;
  416. user_type: string;
  417. user_level?: number;
  418. created_at: string;
  419. };
  420. }
  421. export const loginWithEmail = async (email: string): Promise<LoginEmailResponse> => {
  422. const apiBaseUrl = getApiBaseUrl();
  423. const response = await fetch(`${apiBaseUrl}/api/auth/login/email`, {
  424. method: 'POST',
  425. headers: getApiHeaders(),
  426. body: JSON.stringify({ email }),
  427. });
  428. if (!response.ok) {
  429. const errorText = await response.text().catch(() => '登录失败');
  430. throw new Error(errorText);
  431. }
  432. return response.json();
  433. };
  434. // 发送验证码
  435. export interface SendCodeRequest {
  436. area_code?: string; // 手机区号(如 +86)
  437. phone?: string;
  438. email?: string;
  439. send_type: 'domestic_sms' | 'international_sms' | 'email';
  440. }
  441. export interface SendCodeResponse {
  442. success: boolean;
  443. data: {
  444. code_id: string;
  445. message: string;
  446. };
  447. }
  448. export const sendCode = async (request: SendCodeRequest): Promise<SendCodeResponse> => {
  449. const apiBaseUrl = getApiBaseUrl();
  450. const response = await fetch(`${apiBaseUrl}/api/auth/send-code`, {
  451. method: 'POST',
  452. headers: getApiHeaders(),
  453. body: JSON.stringify(request),
  454. });
  455. if (!response.ok) {
  456. const errorText = await response.text().catch(() => '发送验证码失败');
  457. throw new Error(errorText);
  458. }
  459. return response.json();
  460. };
  461. // 验证验证码
  462. export interface VerifyCodeRequest {
  463. phone?: string;
  464. email?: string;
  465. code: string;
  466. send_type: 'domestic_sms' | 'international_sms' | 'email';
  467. }
  468. export interface VerifyCodeResponse {
  469. success: boolean;
  470. data: {
  471. valid: boolean;
  472. message: string;
  473. };
  474. }
  475. export const verifyCode = async (request: VerifyCodeRequest): Promise<VerifyCodeResponse> => {
  476. const apiBaseUrl = getApiBaseUrl();
  477. const response = await fetch(`${apiBaseUrl}/api/auth/verify-code`, {
  478. method: 'POST',
  479. headers: getApiHeaders(),
  480. body: JSON.stringify(request),
  481. });
  482. if (!response.ok) {
  483. const errorText = await response.text().catch(() => '验证验证码失败');
  484. throw new Error(errorText);
  485. }
  486. return response.json();
  487. };
  488. /**
  489. * OAuth 授权相关接口
  490. */
  491. // 获取微信授权URL
  492. export const getWechatAuthUrl = (): string => {
  493. const apiBaseUrl = getApiBaseUrl();
  494. return `${apiBaseUrl}/api/auth/wechat/authorize`;
  495. };
  496. // 获取QQ授权URL
  497. export const getQQAuthUrl = (from?: string): string => {
  498. const apiBaseUrl = getApiBaseUrl();
  499. const url = `${apiBaseUrl}/api/auth/qq/authorize`;
  500. if (from) {
  501. return `${url}?from=${encodeURIComponent(from)}`;
  502. }
  503. return url;
  504. };
  505. /**
  506. * 头像 GIF 配置
  507. */
  508. export interface AvatarGifSet {
  509. default: string;
  510. positive: string;
  511. negative: string;
  512. talking: string;
  513. }
  514. export interface AvatarGifConfigResponse {
  515. success: boolean;
  516. data: {
  517. avatar_default?: Partial<AvatarGifSet>;
  518. avatar_registered_cap?: Partial<AvatarGifSet>;
  519. };
  520. }
  521. export const getAvatarGifs = async (): Promise<AvatarGifConfigResponse> => {
  522. const apiBaseUrl = getApiBaseUrl();
  523. const response = await fetch(`${apiBaseUrl}/api/avatar-gifs`, {
  524. method: 'GET',
  525. headers: getApiHeaders(),
  526. });
  527. if (!response.ok) {
  528. throw new Error(`获取头像 GIF 配置失败: ${response.statusText}`);
  529. }
  530. return response.json();
  531. };
  532. /**
  533. * 分享图片
  534. */
  535. export interface ShareImageItem {
  536. id: string;
  537. url: string;
  538. title?: string;
  539. sort_order?: number;
  540. is_active?: boolean;
  541. }
  542. export interface GetShareImagesResponse {
  543. success: boolean;
  544. data: ShareImageItem[];
  545. }
  546. export const getShareImages = async (): Promise<GetShareImagesResponse> => {
  547. const apiBaseUrl = getApiBaseUrl();
  548. const response = await fetch(`${apiBaseUrl}/api/share-images`, {
  549. method: 'GET',
  550. headers: getApiHeaders(),
  551. });
  552. if (!response.ok) {
  553. throw new Error(`获取分享图片失败: ${response.statusText}`);
  554. }
  555. return response.json();
  556. };
  557. /**
  558. * 领取分享奖励
  559. */
  560. export interface ClaimShareRewardResponse {
  561. success: boolean;
  562. data?: {
  563. user_id: string;
  564. background_image_id: string;
  565. background_image_metadata?: Record<string, any>;
  566. message: string;
  567. };
  568. message?: string;
  569. }
  570. export const claimShareReward = async (user_id: string): Promise<ClaimShareRewardResponse> => {
  571. const apiBaseUrl = getApiBaseUrl();
  572. const response = await fetch(`${apiBaseUrl}/api/users/${user_id}/claim-share-reward`, {
  573. method: 'POST',
  574. headers: getApiHeaders(),
  575. });
  576. if (!response.ok) {
  577. const errorText = await response.text().catch(() => '领取分享奖励失败');
  578. throw new Error(errorText);
  579. }
  580. const data = await response.json();
  581. // 如果成功,同步背景图片到 localStorage
  582. if (data.success && data.data?.background_image_id) {
  583. const { syncBackgroundImageToStorage } = await import('./storage');
  584. syncBackgroundImageToStorage(
  585. data.data.background_image_id,
  586. data.data.background_image_metadata || { opacity: 0.5 } // 使用后端返回的metadata,或默认透明度
  587. );
  588. }
  589. return data;
  590. };
  591. /**
  592. * 获取所有背景图片配置
  593. */
  594. export interface BackgroundImageConfig {
  595. image_id: string;
  596. image_name: string;
  597. image_url: string;
  598. thumbnail_url?: string;
  599. unlock_type: 'default' | 'level' | 'purchase' | 'mission';
  600. unlock_level?: number;
  601. unlock_mission_type?: string;
  602. display_order: number;
  603. is_active: boolean;
  604. description?: string;
  605. metadata?: Record<string, any>;
  606. created_at?: string;
  607. updated_at?: string;
  608. }
  609. export interface GetBackgroundImagesResponse {
  610. success: boolean;
  611. data: BackgroundImageConfig[];
  612. }
  613. export const getBackgroundImages = async (): Promise<GetBackgroundImagesResponse> => {
  614. const apiBaseUrl = getApiBaseUrl();
  615. const response = await fetch(`${apiBaseUrl}/api/background-images`, {
  616. method: 'GET',
  617. headers: getApiHeaders(),
  618. });
  619. if (!response.ok) {
  620. throw new Error(`获取背景图片配置失败: ${response.statusText}`);
  621. }
  622. return response.json();
  623. };