#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:
QihuAI Design committed 2026-08-23 09:36:35 +08:00
1 parent 552a962123
commit 8f57956904
36 files changed
+7436

No files matched your search

+247
View File
@@ -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'),
};