#978 safe-align: 固化 frontend-v2 空白页修复真相源
- 新增 frontend-v2/ 为生产前端真相源(此前仅容器 dist,未入 git) - vite.config.ts: minify:false 根治压缩器压坏 react-router 6.28 (o.slice) - useApi.ts: asArray select 防御非裸数组返回值 - LandingPage/AppServices: Array.isArray coerce 双重防御 (apps.slice) - 关联: INCIDENT-2026-08-23-blank-page.md / SOP-FRONTEND-ITERATIVE-DEVELOPMENT.md - 公网验证通过 2026-08-23 09:31 (DOM 24493B, 无 slice 错误)
This commit is contained in:
1 parent
552a962123
commit
8f57956904
36 files changed
+7436
No files matched your search
@@ -0,0 +1,247 @@
|
||||
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();
|
||||
}
|
||||
|
||||
// ─── 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'),
|
||||
};
|
||||
|
||||
// ─── Dashboard / Stats ──────────────────────────────────────────────────────────
|
||||
|
||||
export const dashboardApi = {
|
||||
getStats: () =>
|
||||
request<{
|
||||
active_users: number;
|
||||
today_conversations: number;
|
||||
skill_count: number;
|
||||
revenue: number;
|
||||
user_change: string;
|
||||
conv_change: string;
|
||||
}>('/token/usage'),
|
||||
|
||||
getHotSkills: () =>
|
||||
request<{ name: string; downloads: string }[]>('/market/apps'),
|
||||
};
|
||||
Reference in new issue
Block a user