const BASE_URL = import.meta.env.VITE_API_URL || '/app/api'; async function request(path: string, options: RequestInit = {}): Promise { const token = localStorage.getItem('auth_token'); const headers: Record = { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}), ...(options.headers as Record || {}), }; const res = await fetch(`${BASE_URL}${path}`, { ...options, headers }); if (res.status === 401) { localStorage.removeItem('auth_token'); window.location.href = '/login'; throw new Error('Unauthorized'); } if (!res.ok) { const err = await res.json().catch(() => ({ detail: res.statusText })); throw new Error(err.detail || `HTTP ${res.status}`); } return res.json(); } // Backend responses use a unified envelope `{code, data, message}`. // `request()` returns the raw envelope; `data()` unwraps `.data`. async function data(path: string, options: RequestInit = {}): Promise { const env = await request<{ code: number; data: T; message: string }>(path, options); return env.data; } // ─── Auth ─────────────────────────────────────────────────────────────────────── export const authApi = { login: (data: { username: string; password: string }) => request<{ access_token: string; token_type: string }>('/auth/login', { method: 'POST', body: JSON.stringify(data), }), register: (data: { username: string; email: string; password: string; code: string }) => request<{ access_token: string }>('/auth/register', { method: 'POST', body: JSON.stringify(data), }), me: () => request<{ id: string; username: string; email: string; avatar_url: string }>('/auth/me'), logout: () => request('/auth/logout', { method: 'POST' }), sendCode: (data: { email: string }) => request('/auth/send-code', { method: 'POST', body: JSON.stringify(data) }), }; // ─── Chat ──────────────────────────────────────────────────────────────────────── export interface Conversation { id: string; title: string; model: string; created_at: string; updated_at: string; message_count: number; } export interface ChatMessage { id: string; conversation_id: string; role: 'user' | 'assistant'; content: string; model: string; created_at: string; } export const chatApi = { listConversations: () => request('/chat/conversations'), createConversation: (data: { title?: string; model?: string }) => request('/chat/conversations', { method: 'POST', body: JSON.stringify(data), }), deleteConversation: (id: string) => request(`/chat/conversations/${id}`, { method: 'DELETE' }), getMessages: (convId: string) => request(`/chat/conversations/${convId}/messages`), sendMessage: (convId: string, data: { content: string; model?: string }) => request(`/chat/conversations/${convId}/messages`, { method: 'POST', body: JSON.stringify(data), }), streamMessage: async function* (convId: string, data: { content: string; model?: string }) { const token = localStorage.getItem('auth_token'); const res = await fetch(`${BASE_URL}/chat/conversations/${convId}/messages/stream`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}), }, body: JSON.stringify(data), }); const reader = res.body?.getReader(); const decoder = new TextDecoder(); if (!reader) return; while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value); const lines = chunk.split('\n').filter(l => l.startsWith('data: ')); for (const line of lines) { try { const data = JSON.parse(line.slice(6)); if (data.content) yield data.content; } catch {} } } }, getModels: () => request<{ id: string; name: string; provider: string }[]>('/chat/models'), }; // ─── Knowledge ─────────────────────────────────────────────────────────────────── export interface KnowledgeBase { id: string; name: string; description: string; document_count: number; status: 'active' | 'building' | 'error'; created_at: string; updated_at: string; } export const knowledgeApi = { listBases: () => request('/knowledge/bases'), createBase: (data: { name: string; description?: string }) => request('/knowledge/bases', { method: 'POST', body: JSON.stringify(data), }), deleteBase: (id: string) => request(`/knowledge/bases/${id}`, { method: 'DELETE' }), search: (baseId: string, query: string) => request<{ results: { content: string; score: number; source: string }[] }>( `/knowledge/bases/${baseId}/search?q=${encodeURIComponent(query)}` ), getDocuments: (baseId: string) => request<{ id: string; name: string; status: string; size: number }[]>( `/knowledge/bases/${baseId}/documents` ), }; // ─── Market / Apps ────────────────────────────────────────────────────────────── export interface MarketApp { id: string; name: string; description: string; icon: string; category: string; status: 'enabled' | 'disabled'; type: 'standalone' | 'embedded'; downloads: number; rating: number; } export const marketApi = { listApps: () => request('/market/apps'), getMyApps: () => request('/market/my-apps'), installApp: (appId: string) => request(`/market/apps/${appId}/install`, { method: 'POST' }), uninstallApp: (appId: string) => request(`/market/apps/${appId}/uninstall`, { method: 'DELETE' }), getAds: () => request<{ id: string; title: string; image_url: string; url: string }[]>('/market/ads'), }; // ─── IM Bot ────────────────────────────────────────────────────────────────────── export interface ImBotStatus { status: 'connected' | 'disconnected' | 'expired'; ilink_bot_id: string; connected_at: string; last_active: string; message_count: number; } export const imBotApi = { getStatus: () => request('/im-bot/status'), getQRCode: () => request<{ qr_url: string; expire_at: string }>('/im-bot/qrcode'), checkQRStatus: (ticket: string) => request<{ status: string }>('/im-bot/qrcode/status', { method: 'POST', body: JSON.stringify({ ticket }), }), disconnect: () => request('/im-bot/disconnect', { method: 'POST' }), getHistory: () => request<{ id: string; role: string; content: string; created_at: string }[]>('/im-bot/history'), }; // ─── Profile ───────────────────────────────────────────────────────────────────── export interface UserProfile { id: string; username: string; email: string; avatar_url: string; balance: number; membership: string; } export const profileApi = { get: () => request('/profile/'), update: (data: Partial) => request('/profile/', { method: 'PUT', body: JSON.stringify(data) }), getBalance: () => request<{ balance: number; total_used: number }>('/profile/balance'), }; // ─── Token 驾驶舱 ──────────────────────────────────────────────────────────────── export interface TokenOverview { total_used: number; balance: number; today_apps: number; server_time: string; } export interface ModelRanking { model_id: string; model_name: string; total_tokens: number; usage_count: number; } export interface TopApp { conversation_id: string; total_tokens: number; } export interface TokenRankings { model_rankings: ModelRanking[]; top_apps: TopApp[]; } export interface TokenLogItem { id: string; model_id: string; model_name: string; prompt_tokens: number; completion_tokens: number; total_tokens: number; conversation_id: string; created_at: string | null; } export interface TokenLogPage { total: number; page: number; page_size: number; items: TokenLogItem[]; } export interface TokenBalance { balance: number; updated_at: string | null; } export const tokenApi = { getOverview: () => data('/token/overview'), getRankings: () => data('/token/rankings'), getLogs: (params: { start_date?: string; end_date?: string; model_id?: string; app_type?: string; page?: number; page_size?: number; } = {}) => { const qs = new URLSearchParams(); if (params.start_date) qs.set('start_date', params.start_date); if (params.end_date) qs.set('end_date', params.end_date); if (params.model_id) qs.set('model_id', params.model_id); if (params.app_type) qs.set('app_type', params.app_type); if (params.page) qs.set('page', String(params.page)); if (params.page_size) qs.set('page_size', String(params.page_size)); const q = qs.toString(); return data(`/token/logs${q ? `?${q}` : ''}`); }, getBalance: () => data('/token/balance'), };