apiClient.ts 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. import { API_BASE_URL } from '../utils/constants';
  2. interface ApiResponse<T = any> {
  3. data: T;
  4. status: number;
  5. }
  6. class ApiClient {
  7. private baseURL: string;
  8. constructor(baseURL: string) {
  9. this.baseURL = baseURL;
  10. }
  11. private getAuthHeaders(): Record<string, string> {
  12. const apiKey = localStorage.getItem('kb_api_key');
  13. const activeTenantId = localStorage.getItem('kb_active_tenant_id');
  14. const token = localStorage.getItem('authToken') || localStorage.getItem('token');
  15. const language = localStorage.getItem('userLanguage') || 'zh';
  16. const headers: Record<string, string> = {
  17. 'Content-Type': 'application/json',
  18. 'x-user-language': language,
  19. };
  20. if (apiKey) {
  21. if (apiKey.startsWith('kb_')) {
  22. headers['x-api-key'] = apiKey;
  23. } else {
  24. headers['Authorization'] = `Bearer ${apiKey}`;
  25. }
  26. } else if (token) {
  27. headers['Authorization'] = `Bearer ${token}`;
  28. }
  29. if (activeTenantId && activeTenantId !== 'undefined' && activeTenantId !== 'null') {
  30. headers['x-tenant-id'] = activeTenantId;
  31. }
  32. return headers;
  33. }
  34. private async handleResponse<T>(response: Response): Promise<ApiResponse<T>> {
  35. const text = await response.text();
  36. let data: any;
  37. try {
  38. data = text ? JSON.parse(text) : null;
  39. } catch (e) {
  40. data = null;
  41. }
  42. if (!response.ok) {
  43. throw new Error(data?.message || text || 'Request failed');
  44. }
  45. return { data: data as T, status: response.status };
  46. }
  47. // 新しい API 呼び出し方法、{ data, status } を返す
  48. async get<T = any>(url: string): Promise<ApiResponse<T>> {
  49. const response = await fetch(`${this.baseURL}${url}`, {
  50. method: 'GET',
  51. headers: this.getAuthHeaders(),
  52. });
  53. return this.handleResponse<T>(response);
  54. }
  55. async post<T = any>(url: string, body?: any): Promise<ApiResponse<T>> {
  56. const response = await fetch(`${this.baseURL}${url}`, {
  57. method: 'POST',
  58. headers: this.getAuthHeaders(),
  59. body: body ? JSON.stringify(body) : undefined,
  60. });
  61. return this.handleResponse<T>(response);
  62. }
  63. async put<T = any>(url: string, body?: any): Promise<ApiResponse<T>> {
  64. const response = await fetch(`${this.baseURL}${url}`, {
  65. method: 'PUT',
  66. headers: this.getAuthHeaders(),
  67. body: body ? JSON.stringify(body) : undefined,
  68. });
  69. return this.handleResponse<T>(response);
  70. }
  71. async patch<T = any>(url: string, body?: any): Promise<ApiResponse<T>> {
  72. const response = await fetch(`${this.baseURL}${url}`, {
  73. method: 'PATCH',
  74. headers: this.getAuthHeaders(),
  75. body: body ? JSON.stringify(body) : undefined,
  76. });
  77. return this.handleResponse<T>(response);
  78. }
  79. async delete<T = any>(url: string): Promise<ApiResponse<T>> {
  80. const response = await fetch(`${this.baseURL}${url}`, {
  81. method: 'DELETE',
  82. headers: this.getAuthHeaders(),
  83. });
  84. return this.handleResponse<T>(response);
  85. }
  86. // New methods for special formats
  87. async getBlob(url: string): Promise<Blob> {
  88. const response = await fetch(`${this.baseURL}${url}`, {
  89. method: 'GET',
  90. headers: this.getAuthHeaders(),
  91. });
  92. if (!response.ok) {
  93. throw new Error('Request failed');
  94. }
  95. return await response.blob();
  96. }
  97. async postMultipart<T = any>(url: string, formData: FormData): Promise<ApiResponse<T>> {
  98. const headers = this.getAuthHeaders();
  99. // Remove Content-Type to let the browser set it with the correct boundary
  100. delete headers['Content-Type'];
  101. const response = await fetch(`${this.baseURL}${url}`, {
  102. method: 'POST',
  103. headers,
  104. body: formData,
  105. });
  106. return this.handleResponse<T>(response);
  107. }
  108. // Legacy compatibility method — returns raw Response for streaming and other special cases
  109. async request(path: string, options: RequestInit = {}): Promise<Response> {
  110. const authHeaders = this.getAuthHeaders();
  111. const headers = new Headers(options.headers);
  112. // Merge auth headers into request headers
  113. Object.entries(authHeaders).forEach(([key, value]) => {
  114. headers.set(key, value);
  115. });
  116. let url = path;
  117. if (!path.startsWith('http')) {
  118. const cleanPath = path.startsWith('/') ? path : `/${path}`;
  119. url = `${this.baseURL}${cleanPath}`;
  120. }
  121. const response = await fetch(url, {
  122. ...options,
  123. headers,
  124. });
  125. if (response.status === 401) {
  126. localStorage.removeItem('kb_api_key');
  127. localStorage.removeItem('authToken');
  128. window.location.href = '/login';
  129. throw new Error('Unauthorized');
  130. }
  131. return response;
  132. }
  133. private handleUnauthorized() {
  134. console.warn('[ApiClient] 401 Unauthorized detected. Cleaning up and redirecting to login...');
  135. localStorage.removeItem('kb_api_key');
  136. localStorage.removeItem('authToken');
  137. localStorage.removeItem('token');
  138. localStorage.removeItem('kb_active_tenant_id');
  139. // Only redirect if we are not already on the login page
  140. if (window.location.pathname !== '/login') {
  141. window.location.href = '/login';
  142. }
  143. }
  144. }
  145. export const apiClient = new ApiClient(API_BASE_URL);