abcced35b8
- Increase RAG retrieval from 5 to 50 docs with relevance threshold (0.15) - LLM inline citations with [来源N] format and reference list - Clickable citation links scroll to source cards with highlight animation - Source previews with full chunk text and answer-matched highlighting - Message-scoped source IDs to fix cross-round citation targeting - Admin panel pages (knowledge, users, forum, course, settings) - Add rehype-raw dependency for HTML-in-markdown rendering Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1199 lines
32 KiB
TypeScript
1199 lines
32 KiB
TypeScript
import { z } from "zod";
|
|
import type {
|
|
CourseModule,
|
|
Book,
|
|
Chapter,
|
|
BookStructure,
|
|
User,
|
|
ForumCategory,
|
|
ForumPostSummary,
|
|
ForumPostDetail,
|
|
ForumReply,
|
|
} from "@/types";
|
|
|
|
// API基础配置
|
|
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || (typeof window !== 'undefined' ? `${window.location.protocol}//${window.location.host}/api` : "http://127.0.0.1:8000");
|
|
|
|
// 请求拦截器
|
|
async function apiRequest<T>(
|
|
endpoint: string,
|
|
options: RequestInit = {}
|
|
): Promise<T> {
|
|
const url = `${API_BASE_URL}${endpoint}`;
|
|
|
|
const defaultHeaders: Record<string, string> = {
|
|
"Content-Type": "application/json",
|
|
};
|
|
|
|
// 添加认证头
|
|
const token = localStorage.getItem("auth_token");
|
|
if (token) {
|
|
defaultHeaders.Authorization = `Bearer ${token}`;
|
|
}
|
|
|
|
const config: RequestInit = {
|
|
...options,
|
|
headers: {
|
|
...defaultHeaders,
|
|
...options.headers,
|
|
},
|
|
};
|
|
|
|
try {
|
|
const response = await fetch(url, config);
|
|
|
|
if (!response.ok) {
|
|
if (response.status === 401) {
|
|
localStorage.removeItem("auth_token");
|
|
if (typeof window !== 'undefined' &&
|
|
!window.location.pathname.includes('/login') &&
|
|
!window.location.pathname.includes('/register')) {
|
|
window.location.replace("/login");
|
|
}
|
|
throw new Error("认证已过期,请重新登录");
|
|
}
|
|
|
|
const errorData = await response.json().catch(() => ({}));
|
|
throw new Error(errorData.detail || `HTTP ${response.status}`);
|
|
}
|
|
|
|
return await response.json();
|
|
} catch (error) {
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// 认证相关API
|
|
export const authAPI = {
|
|
async login(username: string, password: string) {
|
|
return apiRequest<{
|
|
access_token: string;
|
|
token_type: string;
|
|
}>("/auth/login", {
|
|
method: "POST",
|
|
body: JSON.stringify({ username, password }),
|
|
});
|
|
},
|
|
|
|
async register(userData: {
|
|
username: string;
|
|
email: string;
|
|
password: string;
|
|
full_name?: string;
|
|
}) {
|
|
return apiRequest<User>("/auth/register", {
|
|
method: "POST",
|
|
body: JSON.stringify(userData),
|
|
});
|
|
},
|
|
|
|
async getCurrentUser() {
|
|
return apiRequest<User>("/auth/me");
|
|
},
|
|
|
|
async updateUserInfo(data: { email?: string; full_name?: string }) {
|
|
return apiRequest<User>("/auth/me", {
|
|
method: "PUT",
|
|
body: JSON.stringify(data),
|
|
});
|
|
},
|
|
|
|
async changePassword(data: { old_password: string; new_password: string }) {
|
|
return apiRequest<{ message: string }>("/auth/change-password", {
|
|
method: "POST",
|
|
body: JSON.stringify(data),
|
|
});
|
|
},
|
|
|
|
async verifyEmail(token: string) {
|
|
return apiRequest<{
|
|
success: boolean;
|
|
message: string;
|
|
user?: User;
|
|
}>("/auth/verify-email", {
|
|
method: "POST",
|
|
body: JSON.stringify({ token }),
|
|
});
|
|
},
|
|
};
|
|
|
|
// 图像生成相关API
|
|
export const imageAPI = {
|
|
// 文生图 API
|
|
async textToImage(request: {
|
|
prompt: string;
|
|
model?: string;
|
|
template?: string;
|
|
style?: string;
|
|
size?: string;
|
|
num_images?: number;
|
|
}) {
|
|
return apiRequest<{
|
|
images: Array<{
|
|
id: string;
|
|
url: string;
|
|
prompt: string;
|
|
model: string;
|
|
metadata: any;
|
|
}>;
|
|
total: number;
|
|
}>("/image/text-to-image", {
|
|
method: "POST",
|
|
body: JSON.stringify(request),
|
|
});
|
|
},
|
|
|
|
// 图生图 - 图像编辑
|
|
async imageEdit(
|
|
image: File,
|
|
prompt: string,
|
|
mode: string = "optimize",
|
|
mask?: File,
|
|
strength: number = 0.8
|
|
) {
|
|
const formData = new FormData();
|
|
formData.append("image", image);
|
|
formData.append("prompt", prompt);
|
|
formData.append("mode", mode);
|
|
formData.append("strength", strength.toString());
|
|
if (mask) {
|
|
formData.append("mask", mask);
|
|
}
|
|
|
|
const token = localStorage.getItem("auth_token");
|
|
|
|
const response = await fetch(`${API_BASE_URL}/image/image-to-image/edit`, {
|
|
method: "POST",
|
|
headers: {
|
|
...(token && { Authorization: `Bearer ${token}` }),
|
|
},
|
|
body: formData,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json().catch(() => ({}));
|
|
throw new Error(errorData.detail || `HTTP ${response.status}`);
|
|
}
|
|
|
|
return await response.json();
|
|
},
|
|
|
|
// 图生图 - 生成变体
|
|
async imageVariations(image: File, num_variations: number = 3) {
|
|
const formData = new FormData();
|
|
formData.append("image", image);
|
|
formData.append("num_variations", num_variations.toString());
|
|
|
|
const token = localStorage.getItem("auth_token");
|
|
|
|
const response = await fetch(`${API_BASE_URL}/image/image-to-image/variations`, {
|
|
method: "POST",
|
|
headers: {
|
|
...(token && { Authorization: `Bearer ${token}` }),
|
|
},
|
|
body: formData,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json().catch(() => ({}));
|
|
throw new Error(errorData.detail || `HTTP ${response.status}`);
|
|
}
|
|
|
|
return await response.json();
|
|
},
|
|
|
|
// 图生图 - 风格转换
|
|
async styleTransfer(image: File, style_prompt: string, strength: number = 0.8) {
|
|
const formData = new FormData();
|
|
formData.append("image", image);
|
|
formData.append("style_prompt", style_prompt);
|
|
formData.append("strength", strength.toString());
|
|
|
|
const token = localStorage.getItem("auth_token");
|
|
|
|
const response = await fetch(`${API_BASE_URL}/image/image-to-image/style-transfer`, {
|
|
method: "POST",
|
|
headers: {
|
|
...(token && { Authorization: `Bearer ${token}` }),
|
|
},
|
|
body: formData,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json().catch(() => ({}));
|
|
throw new Error(errorData.detail || `HTTP ${response.status}`);
|
|
}
|
|
|
|
return await response.json();
|
|
},
|
|
|
|
// 图生图 - 图像优化
|
|
async optimizeImage(
|
|
image: File,
|
|
optimization_prompt: string = "优化图像质量,增强细节,提高清晰度",
|
|
strength: number = 0.6
|
|
) {
|
|
const formData = new FormData();
|
|
formData.append("image", image);
|
|
formData.append("optimization_prompt", optimization_prompt);
|
|
formData.append("strength", strength.toString());
|
|
|
|
const token = localStorage.getItem("auth_token");
|
|
|
|
const response = await fetch(`${API_BASE_URL}/image/image-to-image/optimize`, {
|
|
method: "POST",
|
|
headers: {
|
|
...(token && { Authorization: `Bearer ${token}` }),
|
|
},
|
|
body: formData,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json().catch(() => ({}));
|
|
throw new Error(errorData.detail || `HTTP ${response.status}`);
|
|
}
|
|
|
|
return await response.json();
|
|
},
|
|
|
|
// 图生图 - 图像扩展
|
|
async outpaintImage(image: File, expansion_prompt: string, strength: number = 0.7) {
|
|
const formData = new FormData();
|
|
formData.append("image", image);
|
|
formData.append("expansion_prompt", expansion_prompt);
|
|
formData.append("strength", strength.toString());
|
|
|
|
const token = localStorage.getItem("auth_token");
|
|
|
|
const response = await fetch(`${API_BASE_URL}/image/image-to-image/outpaint`, {
|
|
method: "POST",
|
|
headers: {
|
|
...(token && { Authorization: `Bearer ${token}` }),
|
|
},
|
|
body: formData,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json().catch(() => ({}));
|
|
throw new Error(errorData.detail || `HTTP ${response.status}`);
|
|
}
|
|
|
|
return await response.json();
|
|
},
|
|
|
|
// 配置信息 API
|
|
async getModels() {
|
|
const response = await apiRequest<{
|
|
models: Array<{
|
|
id: string;
|
|
name: string;
|
|
description: string;
|
|
}>;
|
|
}>("/image/models", {
|
|
method: "GET",
|
|
});
|
|
return response.models;
|
|
},
|
|
|
|
async getTemplates() {
|
|
const response = await apiRequest<{
|
|
templates: Array<{
|
|
id: string;
|
|
name: string;
|
|
description: string;
|
|
}>;
|
|
}>("/image/templates", {
|
|
method: "GET",
|
|
});
|
|
return response.templates;
|
|
},
|
|
|
|
async getStyles() {
|
|
const response = await apiRequest<{
|
|
styles: Array<{
|
|
id: string;
|
|
name: string;
|
|
description: string;
|
|
}>;
|
|
}>("/image/styles", {
|
|
method: "GET",
|
|
});
|
|
return response.styles;
|
|
},
|
|
|
|
async getSizes() {
|
|
const response = await apiRequest<{
|
|
sizes: Array<{
|
|
id: string;
|
|
name: string;
|
|
description: string;
|
|
}>;
|
|
}>("/image/sizes", {
|
|
method: "GET",
|
|
});
|
|
return response.sizes;
|
|
},
|
|
|
|
async getEditModes() {
|
|
const response = await apiRequest<{
|
|
modes: Array<{
|
|
id: string;
|
|
name: string;
|
|
description: string;
|
|
}>;
|
|
}>("/image/edit-modes", {
|
|
method: "GET",
|
|
});
|
|
return response.modes;
|
|
},
|
|
|
|
async getStylePresets() {
|
|
const response = await apiRequest<{
|
|
presets: Array<{
|
|
id: string;
|
|
name: string;
|
|
description: string;
|
|
}>;
|
|
}>("/image/style-presets", {
|
|
method: "GET",
|
|
});
|
|
return response.presets;
|
|
},
|
|
|
|
// 历史记录 API
|
|
async getImageHistory(page: number = 1, page_size: number = 20) {
|
|
return apiRequest<{
|
|
images: Array<{
|
|
id: string;
|
|
url: string;
|
|
prompt: string;
|
|
style: string;
|
|
size: string;
|
|
created_at: string;
|
|
metadata: any;
|
|
}>;
|
|
total: number;
|
|
page: number;
|
|
page_size: number;
|
|
has_next: boolean;
|
|
has_prev: boolean;
|
|
}>(`/image/history?page=${page}&page_size=${page_size}`, {
|
|
method: "GET",
|
|
});
|
|
},
|
|
|
|
async deleteImage(imageId: string) {
|
|
return apiRequest<{ message: string }>(`/image/${imageId}`, {
|
|
method: "DELETE",
|
|
});
|
|
},
|
|
};
|
|
|
|
// 聊天相关API
|
|
export const chatAPI = {
|
|
async createSession(title: string = "新对话") {
|
|
return apiRequest<{
|
|
id: number;
|
|
title: string;
|
|
created_at: string;
|
|
updated_at: string;
|
|
message_count: number;
|
|
}>("/chat/sessions", {
|
|
method: "POST",
|
|
body: JSON.stringify({ title }),
|
|
});
|
|
},
|
|
|
|
async sendMessage(message: string, sessionId?: number) {
|
|
return apiRequest<{
|
|
answer: string;
|
|
sources: Array<{
|
|
title: string;
|
|
filename: string;
|
|
page: number;
|
|
score: number;
|
|
preview: string;
|
|
}>;
|
|
session_id: number;
|
|
message_id: number;
|
|
}>("/chat/send", {
|
|
method: "POST",
|
|
body: JSON.stringify({ message, session_id: sessionId }),
|
|
});
|
|
},
|
|
|
|
async streamMessage(
|
|
message: string,
|
|
sessionId: number | undefined,
|
|
mode: string,
|
|
knowledgeBaseIds?: string[],
|
|
signal?: AbortSignal,
|
|
onChunk?: (chunk: string) => void,
|
|
onComplete?: (sessionId: number, messageId?: number, userMessageId?: number) => void,
|
|
onError?: (error: string) => void,
|
|
onStatus?: (status: string) => void,
|
|
onThinking?: (step: { step: string; message: string; details?: Record<string, any>; timestamp?: string }) => void,
|
|
onThinkingContent?: (content: string) => void,
|
|
model?: string
|
|
) {
|
|
const url = `${API_BASE_URL}/chat/stream`;
|
|
const token = localStorage.getItem("auth_token");
|
|
|
|
const requestBody: any = {
|
|
message,
|
|
mode: mode,
|
|
};
|
|
|
|
// 添加模型ID
|
|
if (model) {
|
|
requestBody.model = model;
|
|
}
|
|
|
|
// 只在有值时添加 session_id
|
|
if (sessionId !== undefined && sessionId !== null) {
|
|
requestBody.session_id = sessionId;
|
|
}
|
|
|
|
// 只在有值且非空数组时添加 knowledge_base_ids
|
|
if (knowledgeBaseIds && knowledgeBaseIds.length > 0) {
|
|
// 智能提取数字ID
|
|
const numericIds = knowledgeBaseIds
|
|
.map(id => {
|
|
// 如果已经是数字字符串,直接转换
|
|
if (/^\d+$/.test(id)) {
|
|
return parseInt(id);
|
|
}
|
|
// 如果是 "kb-system-1" 格式,提取末尾数字
|
|
const match = id.match(/\d+$/);
|
|
return match ? parseInt(match[0]) : null;
|
|
})
|
|
.filter(id => id !== null && !isNaN(id));
|
|
|
|
// 只有在有有效ID时才添加
|
|
if (numericIds.length > 0) {
|
|
requestBody.knowledge_base_ids = numericIds;
|
|
}
|
|
}
|
|
|
|
console.log("[DEBUG-CHAT] 发送流式请求:", {
|
|
url,
|
|
body: requestBody,
|
|
bodyJSON: JSON.stringify(requestBody),
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
...(token && { Authorization: `Bearer ${token}` }),
|
|
}
|
|
});
|
|
|
|
console.log("[DEBUG-CHAT] 请求详情:", {
|
|
message: message,
|
|
sessionId: sessionId,
|
|
mode: mode,
|
|
knowledgeBaseIds: knowledgeBaseIds,
|
|
model: model,
|
|
hasToken: !!token
|
|
});
|
|
|
|
let response;
|
|
try {
|
|
response = await fetch(url, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
...(token && { Authorization: `Bearer ${token}` }),
|
|
},
|
|
body: JSON.stringify(requestBody),
|
|
signal, // 新增:传递 signal
|
|
});
|
|
} catch (fetchError) {
|
|
// 检查是否是用户中止
|
|
if (fetchError instanceof Error && fetchError.name === 'AbortError') {
|
|
console.log("[DEBUG-STOP] 用户中止了请求");
|
|
onError?.("已停止生成");
|
|
return;
|
|
}
|
|
console.error("❌ Fetch failed:", fetchError);
|
|
onError?.(`Network error: ${fetchError}`);
|
|
return;
|
|
}
|
|
|
|
console.log("[DEBUG-STREAM] 流式响应:", {
|
|
status: response.status,
|
|
statusText: response.statusText,
|
|
ok: response.ok,
|
|
headers: Object.fromEntries(response.headers.entries())
|
|
});
|
|
|
|
if (!response.ok) {
|
|
console.error("❌ Chat stream failed:", {
|
|
status: response.status,
|
|
statusText: response.statusText,
|
|
url: response.url,
|
|
ok: response.ok
|
|
});
|
|
|
|
try {
|
|
const errorData = await response.json();
|
|
console.error("❌ Error response body:", errorData);
|
|
onError?.(`HTTP ${response.status}: ${errorData.detail || errorData.message || response.statusText}`);
|
|
} catch (e) {
|
|
console.error("❌ Could not parse error response:", e);
|
|
onError?.(`HTTP ${response.status}: ${response.statusText}`);
|
|
}
|
|
return;
|
|
}
|
|
|
|
const reader = response.body?.getReader();
|
|
if (!reader) {
|
|
onError?.("无法读取响应流");
|
|
return;
|
|
}
|
|
|
|
const decoder = new TextDecoder();
|
|
let buffer = ""; // Buffer for incomplete lines
|
|
|
|
try {
|
|
while (true) {
|
|
const { done, value } = await reader.read();
|
|
|
|
if (done) break;
|
|
|
|
const chunk = decoder.decode(value, { stream: true });
|
|
buffer += chunk; // Accumulate data
|
|
|
|
const lines = buffer.split("\n");
|
|
// Keep the last incomplete line in buffer
|
|
buffer = lines.pop() || "";
|
|
|
|
for (const line of lines) {
|
|
if (line.startsWith("data: ")) {
|
|
try {
|
|
const data = JSON.parse(line.slice(6));
|
|
|
|
if (data.type === "status") {
|
|
console.log("[DEBUG-STREAM] 状态更新:", data.message);
|
|
onStatus?.(data.message);
|
|
} else if (data.type === "thinking") {
|
|
console.log("[DEBUG-STREAM] 思考中:", data.message);
|
|
// 将thinking数据作为JSON字符串传递给onChunk,让Zustand store处理
|
|
onChunk?.(JSON.stringify(data));
|
|
} else if (data.type === "thinking_content") {
|
|
console.log("[DEBUG-STREAM] 思考内容:", data.content);
|
|
onThinkingContent?.(data.content);
|
|
} else if (data.type === "chunk") {
|
|
console.log("[DEBUG-STREAM] 接收chunk:", data.content);
|
|
onChunk?.(data.content);
|
|
} else if (data.type === "sources") {
|
|
console.log("[DEBUG-STREAM] 收到sources:", data.sources?.length, "个来源");
|
|
onChunk?.(JSON.stringify(data));
|
|
} else if (data.type === "done") {
|
|
console.log("[DEBUG-STREAM] 流式完成, session_id:", data.session_id, "message_id:", data.message_id, "user_message_id:", data.user_message_id);
|
|
onComplete?.(data.session_id, data.message_id, data.user_message_id);
|
|
} else if (data.error) {
|
|
console.log("[DEBUG-STREAM] 流式错误:", data.error);
|
|
onError?.(data.error);
|
|
}
|
|
} catch (e) {
|
|
// 忽略解析错误
|
|
console.warn("[DEBUG-STREAM] 解析SSE数据失败:", line, e);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
// 检查是否是用户中止
|
|
if (error instanceof Error && error.name === 'AbortError') {
|
|
console.log("[DEBUG-STOP] 读取流时被中止");
|
|
onError?.("已停止生成");
|
|
} else {
|
|
onError?.(`流式请求失败: ${error}`);
|
|
}
|
|
} finally {
|
|
reader.releaseLock();
|
|
}
|
|
},
|
|
|
|
async getSessions() {
|
|
return apiRequest<Array<{
|
|
id: number;
|
|
title: string;
|
|
created_at: string;
|
|
updated_at: string;
|
|
message_count: number;
|
|
}>>("/chat/sessions");
|
|
},
|
|
|
|
async getSessionMessages(sessionId: number) {
|
|
return apiRequest<Array<{
|
|
id: number;
|
|
role: "user" | "assistant" | "system";
|
|
content: string;
|
|
created_at: string;
|
|
metadata?: any;
|
|
}>>(`/chat/sessions/${sessionId}/messages`);
|
|
},
|
|
|
|
// 会话管理 API
|
|
async renameSession(sessionId: number, title: string) {
|
|
return apiRequest<{ message: string; title: string }>(`/chat/sessions/${sessionId}`, {
|
|
method: "PUT",
|
|
body: JSON.stringify({ title }),
|
|
});
|
|
},
|
|
|
|
async deleteSession(sessionId: number) {
|
|
return apiRequest<{ message: string }>(`/chat/sessions/${sessionId}`, {
|
|
method: "DELETE",
|
|
});
|
|
},
|
|
|
|
async exportSession(sessionId: number, format: string = "json") {
|
|
return apiRequest<{
|
|
session_id: number;
|
|
title: string;
|
|
messages: Array<{
|
|
id: number;
|
|
role: string;
|
|
content: string;
|
|
created_at: string;
|
|
feedback?: string;
|
|
edited: boolean;
|
|
}>;
|
|
export_format: string;
|
|
created_at: string;
|
|
}>(`/chat/sessions/${sessionId}/export?format=${format}`, {
|
|
method: "POST",
|
|
});
|
|
},
|
|
|
|
// 消息操作 API
|
|
async editMessage(messageId: number, content: string) {
|
|
return apiRequest<{ message: string }>(`/chat/messages/${messageId}`, {
|
|
method: "PUT",
|
|
body: JSON.stringify({ content }),
|
|
});
|
|
},
|
|
|
|
async regenerateMessage(messageId: number, model?: string) {
|
|
return apiRequest<{
|
|
message: string;
|
|
new_message_id: number;
|
|
content: string;
|
|
}>(`/chat/messages/${messageId}/regenerate`, {
|
|
method: "POST",
|
|
body: JSON.stringify({ model: model || null }),
|
|
});
|
|
},
|
|
|
|
async feedbackMessage(messageId: number, feedback: "like" | "dislike") {
|
|
return apiRequest<{ message: string; feedback: string }>(`/chat/messages/${messageId}/feedback`, {
|
|
method: "POST",
|
|
body: JSON.stringify({ feedback }),
|
|
});
|
|
},
|
|
};
|
|
|
|
// 文档相关API
|
|
export const documentAPI = {
|
|
/**
|
|
* @deprecated 此方法已废弃,请使用 knowledgeBaseAPI.uploadDocument()
|
|
*/
|
|
async uploadDocument(file: File) {
|
|
throw new Error("此方法已废弃,请使用知识库上传接口:knowledgeBaseAPI.uploadDocument()");
|
|
},
|
|
|
|
async getDocuments() {
|
|
return apiRequest<Array<{
|
|
id: number;
|
|
filename: string;
|
|
title: string;
|
|
file_size: number;
|
|
file_type: string;
|
|
is_processed: boolean;
|
|
is_public: boolean;
|
|
created_at: string;
|
|
updated_at: string;
|
|
}>>("/documents");
|
|
},
|
|
};
|
|
|
|
// 知识库相关API
|
|
export const knowledgeBaseAPI = {
|
|
// 获取用户的所有知识库
|
|
async getKnowledgeBases() {
|
|
return apiRequest<Array<{
|
|
id: number;
|
|
name: string;
|
|
description?: string;
|
|
user_id: number;
|
|
document_count: number;
|
|
created_at: string;
|
|
updated_at: string;
|
|
is_system?: boolean;
|
|
}>>("/knowledge-bases");
|
|
},
|
|
|
|
// 创建新知识库
|
|
async createKnowledgeBase(data: {
|
|
name: string;
|
|
description?: string;
|
|
}) {
|
|
return apiRequest<{
|
|
id: number;
|
|
name: string;
|
|
description?: string;
|
|
user_id: number;
|
|
document_count: number;
|
|
created_at: string;
|
|
updated_at: string;
|
|
}>("/knowledge-bases", {
|
|
method: "POST",
|
|
body: JSON.stringify(data),
|
|
});
|
|
},
|
|
|
|
// 获取知识库详情(包括文档列表)
|
|
async getKnowledgeBase(id: string) {
|
|
return apiRequest<{
|
|
id: number;
|
|
name: string;
|
|
description?: string;
|
|
user_id: number;
|
|
document_count: number;
|
|
created_at: string;
|
|
updated_at: string;
|
|
documents: Array<{
|
|
id: number;
|
|
filename: string;
|
|
title: string;
|
|
file_size: number;
|
|
file_type: string;
|
|
is_processed: boolean;
|
|
is_public: boolean;
|
|
knowledge_base_id: number;
|
|
created_at: string;
|
|
updated_at: string;
|
|
}>;
|
|
}>(`/knowledge-bases/${id}`);
|
|
},
|
|
|
|
// 更新知识库信息
|
|
async updateKnowledgeBase(id: string, data: {
|
|
name?: string;
|
|
description?: string;
|
|
}) {
|
|
return apiRequest<{
|
|
id: number;
|
|
name: string;
|
|
description?: string;
|
|
user_id: number;
|
|
document_count: number;
|
|
created_at: string;
|
|
updated_at: string;
|
|
}>(`/knowledge-bases/${id}`, {
|
|
method: "PUT",
|
|
body: JSON.stringify(data),
|
|
});
|
|
},
|
|
|
|
// 删除知识库
|
|
async deleteKnowledgeBase(id: string) {
|
|
return apiRequest<void>(`/knowledge-bases/${id}`, {
|
|
method: "DELETE",
|
|
});
|
|
},
|
|
|
|
// 上传文档到指定知识库
|
|
async uploadDocument(
|
|
knowledgeBaseId: string,
|
|
file: File,
|
|
title: string,
|
|
description?: string
|
|
) {
|
|
const formData = new FormData();
|
|
formData.append("file", file);
|
|
formData.append("title", title);
|
|
if (description) {
|
|
formData.append("description", description);
|
|
}
|
|
|
|
const token = localStorage.getItem("auth_token");
|
|
|
|
const response = await fetch(`${API_BASE_URL}/knowledge-bases/${knowledgeBaseId}/documents`, {
|
|
method: "POST",
|
|
headers: {
|
|
...(token && { Authorization: `Bearer ${token}` }),
|
|
},
|
|
body: formData,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json().catch(() => ({}));
|
|
throw new Error(errorData.detail || `HTTP ${response.status}`);
|
|
}
|
|
|
|
return await response.json();
|
|
},
|
|
|
|
// 获取知识库的文档列表
|
|
async getKnowledgeBaseDocuments(knowledgeBaseId: string) {
|
|
return apiRequest<Array<{
|
|
id: number;
|
|
filename: string;
|
|
title: string;
|
|
file_size: number;
|
|
file_type: string;
|
|
is_processed: boolean;
|
|
is_public: boolean;
|
|
knowledge_base_id: number;
|
|
created_at: string;
|
|
updated_at: string;
|
|
}>>(`/knowledge-bases/${knowledgeBaseId}/documents`);
|
|
},
|
|
|
|
// 删除文档
|
|
async deleteDocument(documentId: string) {
|
|
return apiRequest<void>(`/documents/${documentId}`, {
|
|
method: "DELETE",
|
|
});
|
|
},
|
|
};
|
|
|
|
// 论坛 API
|
|
export const forumAPI = {
|
|
async getCategories() {
|
|
return apiRequest<ForumCategory[]>("/forum/categories");
|
|
},
|
|
|
|
async getPosts(categoryId: number, limit: number = 20) {
|
|
const params = new URLSearchParams({ limit: String(limit) });
|
|
return apiRequest<ForumPostSummary[]>(
|
|
`/forum/categories/${categoryId}/posts?${params.toString()}`
|
|
);
|
|
},
|
|
|
|
async createPost(categoryId: number, data: { title: string; content: string }) {
|
|
return apiRequest<ForumPostDetail>(`/forum/categories/${categoryId}/posts`, {
|
|
method: "POST",
|
|
body: JSON.stringify(data),
|
|
});
|
|
},
|
|
|
|
async getPost(postId: number) {
|
|
return apiRequest<ForumPostDetail>(`/forum/posts/${postId}`);
|
|
},
|
|
|
|
async createReply(postId: number, content: string) {
|
|
return apiRequest<ForumReply>(`/forum/posts/${postId}/replies`, {
|
|
method: "POST",
|
|
body: JSON.stringify({ content }),
|
|
});
|
|
},
|
|
};
|
|
|
|
// 学习分析 API
|
|
export const analyticsAPI = {
|
|
async getPlatformStats() {
|
|
return apiRequest<{
|
|
active_users: number;
|
|
knowledge_documents: number;
|
|
qa_dialogues: number;
|
|
generated_images: number;
|
|
}>("/analytics/platform-stats", {
|
|
method: "GET",
|
|
});
|
|
},
|
|
// 获取用户统计数据
|
|
async getStatistics() {
|
|
return apiRequest<{
|
|
total_sessions: number;
|
|
total_messages: number;
|
|
total_documents: number;
|
|
active_days: number;
|
|
user_since: string;
|
|
last_login: string;
|
|
}>("/analytics/statistics", {
|
|
method: "GET",
|
|
});
|
|
},
|
|
|
|
// 获取学习报告
|
|
async getLearningReport() {
|
|
return apiRequest<{
|
|
user_id: number;
|
|
total_questions: number;
|
|
topics_covered: string[];
|
|
learning_progress: number;
|
|
recommendations: string[];
|
|
study_time: number;
|
|
knowledge_gaps: string[];
|
|
}>("/analytics/learning-report", {
|
|
method: "GET",
|
|
});
|
|
},
|
|
|
|
// 获取学习趋势
|
|
async getLearningTrends(days: number = 30) {
|
|
return apiRequest<{
|
|
success: boolean;
|
|
data: Array<{
|
|
date: string;
|
|
sessions: number;
|
|
messages: number;
|
|
}>;
|
|
}>(`/analytics/trends?days=${days}`, {
|
|
method: "GET",
|
|
});
|
|
},
|
|
|
|
// 获取热门问题
|
|
async getPopularQuestions(limit: number = 10) {
|
|
return apiRequest<{
|
|
success: boolean;
|
|
data: Array<{
|
|
question: string;
|
|
count: number;
|
|
category: string;
|
|
}>;
|
|
}>(`/analytics/popular-questions?limit=${limit}`, {
|
|
method: "GET",
|
|
});
|
|
},
|
|
|
|
// 获取知识覆盖度
|
|
async getKnowledgeCoverage() {
|
|
return apiRequest<{
|
|
success: boolean;
|
|
data: Array<{
|
|
topic: string;
|
|
coverage: number;
|
|
questions: number;
|
|
}>;
|
|
}>("/analytics/knowledge-coverage", {
|
|
method: "GET",
|
|
});
|
|
},
|
|
|
|
// 获取完整分析数据
|
|
async getFullAnalytics() {
|
|
return apiRequest<{
|
|
success: boolean;
|
|
data: {
|
|
statistics: {
|
|
total_sessions: number;
|
|
total_messages: number;
|
|
total_documents: number;
|
|
active_days: number;
|
|
user_since: string;
|
|
last_login: string;
|
|
};
|
|
learning_trends: Array<{
|
|
date: string;
|
|
sessions: number;
|
|
messages: number;
|
|
}>;
|
|
popular_questions: Array<{
|
|
question: string;
|
|
count: number;
|
|
category: string;
|
|
}>;
|
|
knowledge_coverage: Array<{
|
|
topic: string;
|
|
coverage: number;
|
|
questions: number;
|
|
}>;
|
|
learning_report: {
|
|
user_id: number;
|
|
total_questions: number;
|
|
topics_covered: string[];
|
|
learning_progress: number;
|
|
recommendations: string[];
|
|
study_time: number;
|
|
knowledge_gaps: string[];
|
|
};
|
|
};
|
|
}>("/analytics/full-analytics", {
|
|
method: "GET",
|
|
});
|
|
},
|
|
};
|
|
|
|
// 课程内容API
|
|
export const courseContentAPI = {
|
|
// 获取书籍层级结构(新版本)
|
|
async getCourseContent(): Promise<BookStructure> {
|
|
return apiRequest<BookStructure>("/course-content", {
|
|
method: "GET",
|
|
});
|
|
},
|
|
|
|
// 获取章节完整内容
|
|
async getChapterContent(chapterId: number): Promise<{ content: string }> {
|
|
return apiRequest<{ content: string }>(`/course-content/chapters/${chapterId}/content`, {
|
|
method: "GET",
|
|
});
|
|
},
|
|
|
|
// 获取节完整内容
|
|
async getSectionContent(sectionId: number): Promise<{ content: string }> {
|
|
return apiRequest<{ content: string }>(`/course-content/sections/${sectionId}/content`, {
|
|
method: "GET",
|
|
});
|
|
},
|
|
|
|
// 获取小节完整内容(知识点)
|
|
async getSubsectionContent(subsectionId: number): Promise<{ content: string }> {
|
|
return apiRequest<{ content: string }>(`/course-content/subsections/${subsectionId}/content`, {
|
|
method: "GET",
|
|
});
|
|
},
|
|
};
|
|
|
|
// 书籍相关API
|
|
export const bookAPI = {
|
|
// 获取所有书籍列表
|
|
async getBooks(): Promise<Book[]> {
|
|
return apiRequest<Book[]>("/books", {
|
|
method: "GET",
|
|
});
|
|
},
|
|
|
|
// 获取书籍章节列表
|
|
async getBookChapters(bookId: number): Promise<Chapter[]> {
|
|
return apiRequest<Chapter[]>(`/books/${bookId}/chapters`, {
|
|
method: "GET",
|
|
});
|
|
},
|
|
|
|
// 获取PDF文件URL
|
|
getBookFileUrl(bookId: number): string {
|
|
return `${API_BASE_URL}/books/${bookId}/file`;
|
|
},
|
|
};
|
|
|
|
// ===== 后台管理 API =====
|
|
export const adminAPI = {
|
|
// 仪表盘
|
|
async getDashboard() {
|
|
return apiRequest<{
|
|
total_users: number;
|
|
active_users_7d: number;
|
|
total_sessions: number;
|
|
total_messages: number;
|
|
total_documents: number;
|
|
total_knowledge_bases: number;
|
|
total_forum_posts: number;
|
|
total_forum_replies: number;
|
|
total_generated_images: number;
|
|
}>("/admin/dashboard");
|
|
},
|
|
|
|
async getUserTrends(days = 30) {
|
|
return apiRequest<{ date: string; count: number }[]>(`/admin/trends/users?days=${days}`);
|
|
},
|
|
|
|
async getMessageTrends(days = 30) {
|
|
return apiRequest<{ date: string; count: number }[]>(`/admin/trends/messages?days=${days}`);
|
|
},
|
|
|
|
// 用户管理
|
|
async listUsers(skip = 0, limit = 50) {
|
|
return apiRequest<{
|
|
id: number;
|
|
username: string;
|
|
email: string;
|
|
full_name: string | null;
|
|
is_active: boolean;
|
|
is_superuser: boolean;
|
|
created_at: string | null;
|
|
last_login: string | null;
|
|
session_count: number;
|
|
message_count: number;
|
|
}[]>(`/admin/users?skip=${skip}&limit=${limit}`);
|
|
},
|
|
|
|
async toggleUserActive(userId: number) {
|
|
return apiRequest<{ success: boolean; is_active: boolean }>(`/admin/users/${userId}/toggle-active`, { method: "PUT" });
|
|
},
|
|
|
|
async toggleUserAdmin(userId: number) {
|
|
return apiRequest<{ success: boolean; is_superuser: boolean }>(`/admin/users/${userId}/toggle-admin`, { method: "PUT" });
|
|
},
|
|
|
|
async deleteUser(userId: number) {
|
|
return apiRequest<{ success: boolean }>(`/admin/users/${userId}`, { method: "DELETE" });
|
|
},
|
|
|
|
// 论坛管理
|
|
async listForumCategories() {
|
|
return apiRequest<{
|
|
id: number;
|
|
slug: string;
|
|
name: string;
|
|
description: string | null;
|
|
post_count: number;
|
|
}[]>("/admin/forum/categories");
|
|
},
|
|
|
|
async createForumCategory(data: { name: string; slug: string; description?: string }) {
|
|
return apiRequest("/admin/forum/categories", {
|
|
method: "POST",
|
|
body: JSON.stringify(data),
|
|
});
|
|
},
|
|
|
|
async updateForumCategory(categoryId: number, data: { name: string; slug: string; description?: string }) {
|
|
return apiRequest(`/admin/forum/categories/${categoryId}`, {
|
|
method: "PUT",
|
|
body: JSON.stringify(data),
|
|
});
|
|
},
|
|
|
|
async deleteForumCategory(categoryId: number) {
|
|
return apiRequest<{ success: boolean }>(`/admin/forum/categories/${categoryId}`, { method: "DELETE" });
|
|
},
|
|
|
|
async listForumPosts(skip = 0, limit = 50) {
|
|
return apiRequest<{
|
|
id: number;
|
|
title: string;
|
|
author_name: string;
|
|
category_name: string;
|
|
reply_count: number;
|
|
created_at: string;
|
|
}[]>(`/admin/forum/posts?skip=${skip}&limit=${limit}`);
|
|
},
|
|
|
|
async deleteForumPost(postId: number) {
|
|
return apiRequest<{ success: boolean }>(`/admin/forum/posts/${postId}`, { method: "DELETE" });
|
|
},
|
|
|
|
// 知识库管理
|
|
async listKnowledgeBases() {
|
|
return apiRequest<{
|
|
id: number;
|
|
name: string;
|
|
description: string | null;
|
|
owner_name: string;
|
|
is_system: boolean;
|
|
document_count: number;
|
|
chunk_count: number;
|
|
created_at: string;
|
|
}[]>("/admin/knowledge-bases");
|
|
},
|
|
|
|
async deleteKnowledgeBase(kbId: number) {
|
|
return apiRequest<{ success: boolean }>(`/admin/knowledge-bases/${kbId}`, { method: "DELETE" });
|
|
},
|
|
|
|
// 系统状态
|
|
async getSystemStatus() {
|
|
return apiRequest<{
|
|
database: { status: string };
|
|
vector_store: { status: string; vector_count: number };
|
|
llm_model: string;
|
|
embedding_model: string;
|
|
}>("/admin/system/status");
|
|
},
|
|
};
|