314 lines
9.8 KiB
TypeScript
314 lines
9.8 KiB
TypeScript
const BASE_URL = import.meta.env.VITE_API_URL || '/app/api';
|
|
|
|
async function request<T>(path: string, options: RequestInit = {}): Promise<T> {
|
|
const token = localStorage.getItem('auth_token');
|
|
const headers: Record<string, string> = {
|
|
'Content-Type': 'application/json',
|
|
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
...(options.headers as Record<string, string> || {}),
|
|
};
|
|
|
|
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<T>()` returns the raw envelope; `data<T>()` unwraps `.data`.
|
|
async function data<T>(path: string, options: RequestInit = {}): Promise<T> {
|
|
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<Conversation[]>('/chat/conversations'),
|
|
|
|
createConversation: (data: { title?: string; model?: string }) =>
|
|
request<Conversation>('/chat/conversations', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
}),
|
|
|
|
deleteConversation: (id: string) =>
|
|
request(`/chat/conversations/${id}`, { method: 'DELETE' }),
|
|
|
|
getMessages: (convId: string) =>
|
|
request<ChatMessage[]>(`/chat/conversations/${convId}/messages`),
|
|
|
|
sendMessage: (convId: string, data: { content: string; model?: string }) =>
|
|
request<ChatMessage>(`/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<KnowledgeBase[]>('/knowledge/bases'),
|
|
|
|
createBase: (data: { name: string; description?: string }) =>
|
|
request<KnowledgeBase>('/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<MarketApp[]>('/market/apps'),
|
|
|
|
getMyApps: () => request<MarketApp[]>('/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<ImBotStatus>('/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<UserProfile>('/profile/'),
|
|
update: (data: Partial<UserProfile>) =>
|
|
request<UserProfile>('/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<TokenOverview>('/token/overview'),
|
|
|
|
getRankings: () => data<TokenRankings>('/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<TokenLogPage>(`/token/logs${q ? `?${q}` : ''}`);
|
|
},
|
|
|
|
getBalance: () => data<TokenBalance>('/token/balance'),
|
|
};
|