- sharepoint_browse.py: replace @microsoft.graph.downloadUrl in $select
with direct /content endpoint download (MS Graph OData annotation not
returned when $select is used — use /drives/{id}/items/{id}/content
with follow_redirects=True instead)
- llm.py: fix tool_call_id=None crash — streaming delta chunks have
id=None in subsequent chunks; use `or` instead of .get(default) which
doesn't handle explicit None values; deduplicate tool calls by id to
avoid duplicate execution; skip incomplete delta entries with empty name
- api-client.ts: handle 204 No Content in handleResponse — return null
instead of calling response.json() on empty body (was crashing on
DELETE region/department endpoints)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
141 lines
4.7 KiB
TypeScript
141 lines
4.7 KiB
TypeScript
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:1222/api/v1';
|
|
|
|
class ApiClient {
|
|
private baseUrl: string;
|
|
|
|
constructor(baseUrl: string) {
|
|
this.baseUrl = baseUrl;
|
|
}
|
|
|
|
private getHeaders(): Record<string, string> {
|
|
const headers: Record<string, string> = {
|
|
'Content-Type': 'application/json',
|
|
};
|
|
if (typeof window !== 'undefined') {
|
|
const token = localStorage.getItem('access_token');
|
|
if (token) {
|
|
headers['Authorization'] = `Bearer ${token}`;
|
|
}
|
|
}
|
|
return headers;
|
|
}
|
|
|
|
private async handleResponse<T>(response: Response, retry?: () => Promise<Response>): Promise<T> {
|
|
if (response.status === 401) {
|
|
// Try refresh
|
|
const refreshed = await this.refreshToken();
|
|
if (!refreshed) {
|
|
if (typeof window !== 'undefined') {
|
|
localStorage.removeItem('access_token');
|
|
localStorage.removeItem('refresh_token');
|
|
localStorage.removeItem('user');
|
|
// Clear Zustand persisted auth state so ProtectedRoute sees logout
|
|
localStorage.removeItem('auth-storage');
|
|
window.location.href = '/login';
|
|
}
|
|
throw new Error('Session expired');
|
|
}
|
|
if (retry) {
|
|
return this.handleResponse<T>(await retry());
|
|
}
|
|
throw new Error('Token refreshed, please retry');
|
|
}
|
|
if (!response.ok) {
|
|
const body = await response.json().catch(() => ({}));
|
|
throw new Error(body.detail || `Request failed: ${response.status}`);
|
|
}
|
|
// 204 No Content — nothing to parse
|
|
if (response.status === 204) return null as unknown as T;
|
|
return response.json();
|
|
}
|
|
|
|
private async refreshToken(): Promise<boolean> {
|
|
const refreshToken = typeof window !== 'undefined' ? localStorage.getItem('refresh_token') : null;
|
|
if (!refreshToken) return false;
|
|
try {
|
|
const res = await fetch(`${this.baseUrl}/auth/refresh`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ refresh_token: refreshToken }),
|
|
});
|
|
if (!res.ok) return false;
|
|
const data = await res.json();
|
|
if (typeof window !== 'undefined') {
|
|
localStorage.setItem('access_token', data.access_token);
|
|
}
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async get<T>(path: string): Promise<T> {
|
|
const url = `${this.baseUrl}${path}`;
|
|
const res = await fetch(url, { headers: this.getHeaders() });
|
|
return this.handleResponse<T>(res, () => fetch(url, { headers: this.getHeaders() }));
|
|
}
|
|
|
|
async post<T>(path: string, body?: unknown): Promise<T> {
|
|
const url = `${this.baseUrl}${path}`;
|
|
const getOpts = () => ({
|
|
method: 'POST',
|
|
headers: this.getHeaders(),
|
|
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
});
|
|
const res = await fetch(url, getOpts());
|
|
return this.handleResponse<T>(res, () => fetch(url, getOpts()));
|
|
}
|
|
|
|
async put<T>(path: string, body?: unknown): Promise<T> {
|
|
const url = `${this.baseUrl}${path}`;
|
|
const getOpts = () => ({
|
|
method: 'PUT',
|
|
headers: this.getHeaders(),
|
|
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
});
|
|
const res = await fetch(url, getOpts());
|
|
return this.handleResponse<T>(res, () => fetch(url, getOpts()));
|
|
}
|
|
|
|
async patch<T>(path: string, body?: unknown): Promise<T> {
|
|
const url = `${this.baseUrl}${path}`;
|
|
const getOpts = () => ({
|
|
method: 'PATCH',
|
|
headers: this.getHeaders(),
|
|
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
});
|
|
const res = await fetch(url, getOpts());
|
|
return this.handleResponse<T>(res, () => fetch(url, getOpts()));
|
|
}
|
|
|
|
async delete<T>(path: string): Promise<T> {
|
|
const url = `${this.baseUrl}${path}`;
|
|
const res = await fetch(url, { method: 'DELETE', headers: this.getHeaders() });
|
|
return this.handleResponse<T>(res, () => fetch(url, { method: 'DELETE', headers: this.getHeaders() }));
|
|
}
|
|
|
|
async upload<T>(path: string, formData: FormData): Promise<T> {
|
|
const url = `${this.baseUrl}${path}`;
|
|
const getHeaders = () => {
|
|
const headers: Record<string, string> = {};
|
|
if (typeof window !== 'undefined') {
|
|
const token = localStorage.getItem('access_token');
|
|
if (token) headers['Authorization'] = `Bearer ${token}`;
|
|
}
|
|
return headers;
|
|
};
|
|
const res = await fetch(url, { method: 'POST', headers: getHeaders(), body: formData });
|
|
return this.handleResponse<T>(res, () => fetch(url, { method: 'POST', headers: getHeaders(), body: formData }));
|
|
}
|
|
|
|
getStreamUrl(path: string): string {
|
|
return `${this.baseUrl}${path}`;
|
|
}
|
|
|
|
getAuthHeaders(): Record<string, string> {
|
|
return this.getHeaders();
|
|
}
|
|
}
|
|
|
|
const apiClient = new ApiClient(API_URL);
|
|
export default apiClient;
|