1378 lines
34 KiB
TypeScript
1378 lines
34 KiB
TypeScript
/**
|
||
* 后端 FastAPI 调用封装
|
||
* 基于大语言模型的文档理解与多源数据融合系统
|
||
*/
|
||
|
||
const BACKEND_BASE_URL = import.meta.env.VITE_BACKEND_API_URL || 'http://localhost:8000/api/v1';
|
||
|
||
// ==================== 类型定义 ====================
|
||
|
||
// 文档类型
|
||
export type DocumentType = 'docx' | 'xlsx' | 'md' | 'txt';
|
||
|
||
// 任务状态
|
||
export type TaskStatus = 'pending' | 'processing' | 'success' | 'failure';
|
||
|
||
// 解析选项
|
||
export interface DocumentUploadOptions {
|
||
docType?: DocumentType;
|
||
parseAllSheets?: boolean;
|
||
sheetName?: string;
|
||
headerRow?: number;
|
||
}
|
||
|
||
// 任务状态响应
|
||
export interface TaskStatusResponse {
|
||
task_id: string;
|
||
status: TaskStatus;
|
||
progress: number;
|
||
message?: string;
|
||
result?: any;
|
||
error?: string;
|
||
}
|
||
|
||
// 文档元数据
|
||
export interface DocumentMetadata {
|
||
doc_id?: string;
|
||
filename?: string;
|
||
original_filename?: string;
|
||
extension?: string;
|
||
file_size?: number;
|
||
doc_type?: DocumentType;
|
||
sheet_count?: number;
|
||
sheet_names?: string[];
|
||
row_count?: number;
|
||
column_count?: number;
|
||
columns?: string[];
|
||
saved_path?: string;
|
||
created_at?: string;
|
||
}
|
||
|
||
// 解析结果
|
||
export interface DocumentParseResult {
|
||
success: boolean;
|
||
data?: {
|
||
columns?: string[];
|
||
rows?: Record<string, any>[];
|
||
row_count?: number;
|
||
column_count?: number;
|
||
sheets?: Record<string, any>;
|
||
content?: string; // 文本内容 (非结构化文档)
|
||
};
|
||
metadata?: DocumentMetadata;
|
||
error?: string;
|
||
}
|
||
|
||
// 上传响应
|
||
export interface UploadResponse {
|
||
task_id: string;
|
||
file_count: number;
|
||
message: string;
|
||
status_url: string;
|
||
}
|
||
|
||
// 文档库项
|
||
export interface DocumentItem {
|
||
doc_id: string;
|
||
filename: string;
|
||
original_filename: string;
|
||
doc_type: DocumentType;
|
||
file_size: number;
|
||
created_at: string;
|
||
metadata?: {
|
||
row_count?: number;
|
||
column_count?: number;
|
||
columns?: string[];
|
||
};
|
||
}
|
||
|
||
// 表格模板字段
|
||
export interface TemplateField {
|
||
cell: string;
|
||
name: string;
|
||
field_type: string;
|
||
required: boolean;
|
||
hint?: string;
|
||
}
|
||
|
||
// 表格填写结果
|
||
export interface FillResult {
|
||
success: boolean;
|
||
filled_data: Record<string, any>;
|
||
fill_details: Array<{
|
||
field: string;
|
||
value: any;
|
||
source: string;
|
||
confidence?: number;
|
||
}>;
|
||
source_doc_count?: number;
|
||
error?: string;
|
||
}
|
||
|
||
// ==================== Excel 相关类型 (保留) ====================
|
||
|
||
export interface ExcelUploadOptions {
|
||
parseAllSheets?: boolean;
|
||
sheetName?: string;
|
||
headerRow?: number;
|
||
}
|
||
|
||
export interface ExcelParseResult {
|
||
success: boolean;
|
||
data?: {
|
||
columns?: string[];
|
||
rows?: Record<string, any>[];
|
||
row_count?: number;
|
||
column_count?: number;
|
||
sheets?: Record<string, any>;
|
||
};
|
||
error?: string;
|
||
metadata?: {
|
||
filename?: string;
|
||
extension?: string;
|
||
sheet_count?: number;
|
||
sheet_names?: string[];
|
||
row_count?: number;
|
||
column_count?: number;
|
||
columns?: string[];
|
||
file_size?: number;
|
||
saved_path?: string;
|
||
original_filename?: string;
|
||
};
|
||
}
|
||
|
||
export interface ExcelExportOptions {
|
||
columns?: string[];
|
||
sheetName?: string;
|
||
}
|
||
|
||
// ==================== AI 分析相关类型 ====================
|
||
|
||
export interface AIAnalyzeOptions {
|
||
userPrompt?: string;
|
||
analysisType?: 'general' | 'summary' | 'statistics' | 'insights';
|
||
parseAllSheets?: boolean;
|
||
}
|
||
|
||
export interface ExcelData {
|
||
columns?: string[];
|
||
rows?: Record<string, any>[];
|
||
row_count?: number;
|
||
column_count?: number;
|
||
}
|
||
|
||
export interface AIAnalysisResult {
|
||
success: boolean;
|
||
analysis?: string;
|
||
model?: string;
|
||
analysisType?: string;
|
||
error?: string;
|
||
}
|
||
|
||
// ==================== Markdown AI 分析类型 ====================
|
||
|
||
export interface AIMarkdownAnalyzeResult {
|
||
success: boolean;
|
||
filename?: string;
|
||
analysis_type?: string;
|
||
section?: string;
|
||
word_count?: number;
|
||
structure?: {
|
||
title_count?: number;
|
||
code_block_count?: number;
|
||
table_count?: number;
|
||
section_count?: number;
|
||
};
|
||
sections?: MarkdownSection[];
|
||
analysis?: string;
|
||
chart_data?: {
|
||
tables?: Array<{
|
||
description?: string;
|
||
columns?: string[];
|
||
rows?: string[][];
|
||
visualization?: {
|
||
statistics?: any;
|
||
charts?: any;
|
||
distributions?: any;
|
||
};
|
||
}>;
|
||
key_statistics?: Array<{
|
||
name?: string;
|
||
value?: string;
|
||
trend?: string;
|
||
description?: string;
|
||
}>;
|
||
chart_suggestions?: Array<{
|
||
chart_type?: string;
|
||
title?: string;
|
||
data_source?: string;
|
||
}>;
|
||
};
|
||
error?: string;
|
||
}
|
||
|
||
export interface MarkdownSection {
|
||
number: string;
|
||
title: string;
|
||
level: number;
|
||
content_preview?: string;
|
||
line_start: number;
|
||
line_end?: number;
|
||
subsections?: MarkdownSection[];
|
||
}
|
||
|
||
export interface MarkdownOutlineResult {
|
||
success: boolean;
|
||
outline?: MarkdownSection[];
|
||
error?: string;
|
||
}
|
||
|
||
export type MarkdownAnalysisType = 'summary' | 'outline' | 'key_points' | 'questions' | 'tags' | 'qa' | 'statistics' | 'section' | 'charts';
|
||
|
||
export interface AIExcelAnalyzeResult {
|
||
success: boolean;
|
||
excel?: {
|
||
data?: ExcelData;
|
||
sheets?: Record<string, ExcelData>;
|
||
metadata?: any;
|
||
saved_path?: string;
|
||
};
|
||
analysis?: {
|
||
analysis?: string;
|
||
model?: string;
|
||
analysisType?: string;
|
||
is_template?: boolean;
|
||
sheets?: Record<string, AIAnalysisResult>;
|
||
total_sheets?: number;
|
||
successful?: number;
|
||
errors?: Record<string, string>;
|
||
};
|
||
error?: string;
|
||
}
|
||
|
||
// ==================== API 封装 ====================
|
||
|
||
export const backendApi = {
|
||
|
||
// ==================== 健康检查 ====================
|
||
|
||
/**
|
||
* 健康检查
|
||
*/
|
||
async healthCheck(): Promise<{
|
||
status: string;
|
||
service: string;
|
||
databases?: { mysql: string; mongodb: string; redis: string };
|
||
}> {
|
||
try {
|
||
const response = await fetch(`${BACKEND_BASE_URL.replace('/api/v1', '')}/health`);
|
||
if (!response.ok) throw new Error('健康检查失败');
|
||
return await response.json();
|
||
} catch (error) {
|
||
console.error('健康检查失败:', error);
|
||
throw error;
|
||
}
|
||
},
|
||
|
||
// ==================== 文档上传与解析 ====================
|
||
|
||
/**
|
||
* 上传单个文档 (支持 docx/xlsx/md/txt)
|
||
* 文件会存入 MySQL (结构化) 和 MongoDB (非结构化),并建立 RAG 索引
|
||
*/
|
||
async uploadDocument(
|
||
file: File,
|
||
options: DocumentUploadOptions = {}
|
||
): Promise<UploadResponse> {
|
||
const formData = new FormData();
|
||
formData.append('file', file);
|
||
|
||
const params = new URLSearchParams();
|
||
if (options.docType) {
|
||
params.append('doc_type', options.docType);
|
||
}
|
||
if (options.parseAllSheets === true) {
|
||
params.append('parse_all_sheets', 'true');
|
||
}
|
||
if (options.sheetName) {
|
||
params.append('sheet_name', options.sheetName);
|
||
}
|
||
if (options.headerRow !== undefined) {
|
||
params.append('header_row', String(options.headerRow));
|
||
}
|
||
|
||
const url = `${BACKEND_BASE_URL}/upload/document?${params.toString()}`;
|
||
|
||
try {
|
||
const response = await fetch(url, {
|
||
method: 'POST',
|
||
body: formData,
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const error = await response.json();
|
||
throw new Error(error.detail || '上传失败');
|
||
}
|
||
|
||
return await response.json();
|
||
} catch (error) {
|
||
console.error('上传文档失败:', error);
|
||
throw error;
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 批量上传文档
|
||
*/
|
||
async uploadDocuments(
|
||
files: File[],
|
||
options: DocumentUploadOptions = {}
|
||
): Promise<UploadResponse> {
|
||
const formData = new FormData();
|
||
files.forEach(file => formData.append('files', file));
|
||
|
||
const params = new URLSearchParams();
|
||
if (options.docType) {
|
||
params.append('doc_type', options.docType);
|
||
}
|
||
|
||
const url = `${BACKEND_BASE_URL}/upload/documents?${params.toString()}`;
|
||
|
||
try {
|
||
const response = await fetch(url, {
|
||
method: 'POST',
|
||
body: formData,
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const error = await response.json();
|
||
throw new Error(error.detail || '批量上传失败');
|
||
}
|
||
|
||
return await response.json();
|
||
} catch (error) {
|
||
console.error('批量上传文档失败:', error);
|
||
throw error;
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 解析已上传的文档
|
||
*/
|
||
async parseDocument(filePath: string): Promise<DocumentParseResult> {
|
||
const url = `${BACKEND_BASE_URL}/upload/document/parse?file_path=${encodeURIComponent(filePath)}`;
|
||
|
||
try {
|
||
const response = await fetch(url, {
|
||
method: 'POST',
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const error = await response.json();
|
||
throw new Error(error.detail || '解析失败');
|
||
}
|
||
|
||
return await response.json();
|
||
} catch (error) {
|
||
console.error('解析文档失败:', error);
|
||
throw error;
|
||
}
|
||
},
|
||
|
||
// ==================== 任务状态查询 ====================
|
||
|
||
/**
|
||
* 查询任务状态
|
||
*/
|
||
async getTaskStatus(taskId: string): Promise<TaskStatusResponse> {
|
||
const url = `${BACKEND_BASE_URL}/tasks/${taskId}`;
|
||
|
||
try {
|
||
const response = await fetch(url);
|
||
if (!response.ok) {
|
||
const error = await response.json();
|
||
throw new Error(error.detail || '获取任务状态失败');
|
||
}
|
||
return await response.json();
|
||
} catch (error) {
|
||
console.error('获取任务状态失败:', error);
|
||
throw error;
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 轮询任务状态直到完成
|
||
*/
|
||
async pollTaskStatus(
|
||
taskId: string,
|
||
onProgress?: (status: TaskStatusResponse) => void,
|
||
interval: number = 2000,
|
||
maxAttempts: number = 90 // 最多90秒 (比赛要求)
|
||
): Promise<TaskStatusResponse> {
|
||
return new Promise((resolve, reject) => {
|
||
let attempts = 0;
|
||
|
||
const poll = async () => {
|
||
try {
|
||
const status = await backendApi.getTaskStatus(taskId);
|
||
onProgress?.(status);
|
||
|
||
if (status.status === 'success') {
|
||
resolve(status);
|
||
return;
|
||
}
|
||
|
||
if (status.status === 'failure') {
|
||
reject(new Error(status.error || '任务失败'));
|
||
return;
|
||
}
|
||
|
||
attempts++;
|
||
if (attempts >= maxAttempts) {
|
||
reject(new Error('任务超时'));
|
||
return;
|
||
}
|
||
|
||
setTimeout(poll, interval);
|
||
} catch (error) {
|
||
reject(error);
|
||
}
|
||
};
|
||
|
||
poll();
|
||
});
|
||
},
|
||
|
||
// ==================== 文档库管理 ====================
|
||
|
||
/**
|
||
* 获取文档列表
|
||
*/
|
||
async getDocuments(
|
||
docType?: DocumentType,
|
||
limit: number = 50
|
||
): Promise<{ success: boolean; documents: DocumentItem[] }> {
|
||
const params = new URLSearchParams();
|
||
if (docType) params.append('doc_type', docType);
|
||
params.append('limit', String(limit));
|
||
|
||
const url = `${BACKEND_BASE_URL}/documents?${params.toString()}`;
|
||
|
||
try {
|
||
const response = await fetch(url);
|
||
if (!response.ok) throw new Error('获取文档列表失败');
|
||
return await response.json();
|
||
} catch (error) {
|
||
console.error('获取文档列表失败:', error);
|
||
throw error;
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 获取单个文档详情
|
||
*/
|
||
async getDocument(docId: string): Promise<{
|
||
success: boolean;
|
||
document?: DocumentItem & { content?: string; structured_data?: any };
|
||
error?: string;
|
||
}> {
|
||
const url = `${BACKEND_BASE_URL}/documents/${docId}`;
|
||
|
||
try {
|
||
const response = await fetch(url);
|
||
if (!response.ok) throw new Error('获取文档详情失败');
|
||
return await response.json();
|
||
} catch (error) {
|
||
console.error('获取文档详情失败:', error);
|
||
throw error;
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 删除文档
|
||
*/
|
||
async deleteDocument(docId: string): Promise<{ success: boolean; message: string }> {
|
||
const url = `${BACKEND_BASE_URL}/documents/${docId}`;
|
||
|
||
try {
|
||
const response = await fetch(url, {
|
||
method: 'DELETE',
|
||
});
|
||
if (!response.ok) {
|
||
const error = await response.json();
|
||
throw new Error(error.detail || '删除失败');
|
||
}
|
||
return await response.json();
|
||
} catch (error) {
|
||
console.error('删除文档失败:', error);
|
||
throw error;
|
||
}
|
||
},
|
||
|
||
// ==================== RAG 检索 ====================
|
||
|
||
/**
|
||
* 检索相关文档/字段
|
||
*/
|
||
async searchRAG(
|
||
query: string,
|
||
topK: number = 5
|
||
): Promise<{
|
||
success: boolean;
|
||
results: Array<{
|
||
content: string;
|
||
metadata: Record<string, any>;
|
||
score: number;
|
||
doc_id: string;
|
||
}>;
|
||
}> {
|
||
const url = `${BACKEND_BASE_URL}/rag/search`;
|
||
|
||
try {
|
||
const response = await fetch(url, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ query, top_k: topK }),
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const error = await response.json();
|
||
throw new Error(error.detail || '检索失败');
|
||
}
|
||
return await response.json();
|
||
} catch (error) {
|
||
console.error('RAG 检索失败:', error);
|
||
throw error;
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 获取 RAG 索引状态
|
||
*/
|
||
async getRAGStatus(): Promise<{
|
||
success: boolean;
|
||
vector_count: number;
|
||
collections: string[];
|
||
}> {
|
||
const url = `${BACKEND_BASE_URL}/rag/status`;
|
||
|
||
try {
|
||
const response = await fetch(url);
|
||
if (!response.ok) throw new Error('获取 RAG 状态失败');
|
||
return await response.json();
|
||
} catch (error) {
|
||
console.error('获取 RAG 状态失败:', error);
|
||
throw error;
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 重建 RAG 索引
|
||
*/
|
||
async rebuildRAGIndex(): Promise<{
|
||
success: boolean;
|
||
message: string;
|
||
}> {
|
||
const url = `${BACKEND_BASE_URL}/rag/rebuild`;
|
||
|
||
try {
|
||
const response = await fetch(url, {
|
||
method: 'POST',
|
||
});
|
||
if (!response.ok) {
|
||
const error = await response.json();
|
||
throw new Error(error.detail || '重建索引失败');
|
||
}
|
||
return await response.json();
|
||
} catch (error) {
|
||
console.error('重建 RAG 索引失败:', error);
|
||
throw error;
|
||
}
|
||
},
|
||
|
||
// ==================== 表格填写 ====================
|
||
|
||
/**
|
||
* 上传表格模板
|
||
*/
|
||
async uploadTemplate(file: File): Promise<{
|
||
success: boolean;
|
||
template_id: string;
|
||
fields: TemplateField[];
|
||
sheets: string[];
|
||
}> {
|
||
const formData = new FormData();
|
||
formData.append('file', file);
|
||
|
||
const url = `${BACKEND_BASE_URL}/templates/upload`;
|
||
|
||
try {
|
||
const response = await fetch(url, {
|
||
method: 'POST',
|
||
body: formData,
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const error = await response.json();
|
||
throw new Error(error.detail || '上传模板失败');
|
||
}
|
||
return await response.json();
|
||
} catch (error) {
|
||
console.error('上传模板失败:', error);
|
||
throw error;
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 从已上传的模板提取字段定义
|
||
*/
|
||
async extractTemplateFields(
|
||
templateId: string,
|
||
fileType: string = 'xlsx'
|
||
): Promise<{
|
||
success: boolean;
|
||
fields: TemplateField[];
|
||
}> {
|
||
const url = `${BACKEND_BASE_URL}/templates/fields`;
|
||
|
||
try {
|
||
const response = await fetch(url, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
template_id: templateId,
|
||
file_type: fileType,
|
||
}),
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const error = await response.json();
|
||
throw new Error(error.detail || '提取字段失败');
|
||
}
|
||
return await response.json();
|
||
} catch (error) {
|
||
console.error('提取字段失败:', error);
|
||
throw error;
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 联合上传模板和源文档
|
||
*/
|
||
async uploadTemplateAndSources(
|
||
templateFile: File,
|
||
sourceFiles: File[]
|
||
): Promise<{
|
||
success: boolean;
|
||
template_id: string;
|
||
filename: string;
|
||
file_type: string;
|
||
fields: TemplateField[];
|
||
field_count: number;
|
||
source_file_paths: string[];
|
||
source_filenames: string[];
|
||
task_id: string;
|
||
}> {
|
||
const formData = new FormData();
|
||
formData.append('template_file', templateFile);
|
||
sourceFiles.forEach(file => formData.append('source_files', file));
|
||
|
||
const url = `${BACKEND_BASE_URL}/templates/upload-joint`;
|
||
|
||
try {
|
||
const response = await fetch(url, {
|
||
method: 'POST',
|
||
body: formData,
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const error = await response.json();
|
||
throw new Error(error.detail || '联合上传失败');
|
||
}
|
||
return await response.json();
|
||
} catch (error) {
|
||
console.error('联合上传失败:', error);
|
||
throw error;
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 执行表格填写
|
||
*/
|
||
async fillTemplate(
|
||
templateId: string,
|
||
templateFields: TemplateField[],
|
||
sourceDocIds?: string[],
|
||
sourceFilePaths?: string[],
|
||
userHint?: string
|
||
): Promise<FillResult> {
|
||
const url = `${BACKEND_BASE_URL}/templates/fill`;
|
||
|
||
try {
|
||
const response = await fetch(url, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
template_id: templateId,
|
||
template_fields: templateFields,
|
||
source_doc_ids: sourceDocIds || [],
|
||
source_file_paths: sourceFilePaths || [],
|
||
user_hint: userHint || null,
|
||
}),
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const error = await response.json();
|
||
throw new Error(error.detail || '填写表格失败');
|
||
}
|
||
return await response.json();
|
||
} catch (error) {
|
||
console.error('填写表格失败:', error);
|
||
throw error;
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 导出填写后的表格
|
||
*/
|
||
async exportFilledTemplate(
|
||
templateId: string,
|
||
filledData: Record<string, any>,
|
||
format: 'xlsx' | 'docx' = 'xlsx'
|
||
): Promise<Blob> {
|
||
const url = `${BACKEND_BASE_URL}/templates/export`;
|
||
|
||
try {
|
||
const response = await fetch(url, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
template_id: templateId,
|
||
filled_data: filledData,
|
||
format,
|
||
}),
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const error = await response.json();
|
||
throw new Error(error.detail || '导出失败');
|
||
}
|
||
return await response.blob();
|
||
} catch (error) {
|
||
console.error('导出表格失败:', error);
|
||
throw error;
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 填充原始模板并导出
|
||
*
|
||
* 直接打开原始模板文件,将数据填入模板的表格/单元格中,然后导出
|
||
* 适用于比赛场景:保持原始模板格式不变
|
||
*/
|
||
async fillAndExportTemplate(
|
||
templatePath: string,
|
||
filledData: Record<string, any>,
|
||
format: 'xlsx' | 'docx' = 'xlsx'
|
||
): Promise<Blob> {
|
||
const url = `${BACKEND_BASE_URL}/templates/fill-and-export`;
|
||
|
||
try {
|
||
const response = await fetch(url, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
template_path: templatePath,
|
||
filled_data: filledData,
|
||
format,
|
||
}),
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const error = await response.json();
|
||
throw new Error(error.detail || '填充模板失败');
|
||
}
|
||
return await response.blob();
|
||
} catch (error) {
|
||
console.error('填充模板失败:', error);
|
||
throw error;
|
||
}
|
||
},
|
||
|
||
// ==================== Excel 专用接口 (保留兼容) ====================
|
||
|
||
/**
|
||
* 上传并解析 Excel 文件
|
||
*/
|
||
async uploadExcel(
|
||
file: File,
|
||
options: ExcelUploadOptions = {}
|
||
): Promise<ExcelParseResult> {
|
||
const formData = new FormData();
|
||
formData.append('file', file);
|
||
|
||
const params = new URLSearchParams();
|
||
if (options.parseAllSheets === true) {
|
||
params.append('parse_all_sheets', 'true');
|
||
}
|
||
if (options.sheetName) {
|
||
params.append('sheet_name', options.sheetName);
|
||
}
|
||
if (options.headerRow !== undefined) {
|
||
params.append('header_row', String(options.headerRow));
|
||
}
|
||
|
||
const url = `${BACKEND_BASE_URL}/upload/excel?${params.toString()}`;
|
||
|
||
try {
|
||
const response = await fetch(url, {
|
||
method: 'POST',
|
||
body: formData,
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const error = await response.json();
|
||
throw new Error(error.detail || '上传失败');
|
||
}
|
||
|
||
return await response.json();
|
||
} catch (error) {
|
||
console.error('上传 Excel 文件失败:', error);
|
||
throw error;
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 导出 Excel 文件
|
||
*/
|
||
async exportExcel(
|
||
filePath: string,
|
||
options: ExcelExportOptions = {}
|
||
): Promise<Blob> {
|
||
const params = new URLSearchParams();
|
||
if (options.sheetName) {
|
||
params.append('sheet_name', options.sheetName);
|
||
}
|
||
if (options.columns && options.columns.length > 0) {
|
||
params.append('columns', options.columns.join(','));
|
||
}
|
||
|
||
const url = `${BACKEND_BASE_URL}/upload/excel/export/${encodeURIComponent(filePath)}?${params.toString()}`;
|
||
|
||
try {
|
||
const response = await fetch(url);
|
||
if (!response.ok) {
|
||
const error = await response.json();
|
||
throw new Error(error.detail || '导出失败');
|
||
}
|
||
return await response.blob();
|
||
} catch (error) {
|
||
console.error('导出 Excel 文件失败:', error);
|
||
throw error;
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 获取 Excel 文件预览
|
||
*/
|
||
async getExcelPreview(
|
||
filePath: string,
|
||
sheetName?: string,
|
||
maxRows: number = 10
|
||
): Promise<ExcelParseResult> {
|
||
const params = new URLSearchParams();
|
||
if (sheetName !== undefined) {
|
||
params.append('sheet_name', sheetName);
|
||
}
|
||
params.append('max_rows', String(maxRows));
|
||
|
||
const url = `${BACKEND_BASE_URL}/upload/excel/preview/${encodeURIComponent(filePath)}?${params.toString()}`;
|
||
|
||
try {
|
||
const response = await fetch(url);
|
||
if (!response.ok) {
|
||
const error = await response.json();
|
||
throw new Error(error.detail || '获取预览失败');
|
||
}
|
||
return await response.json();
|
||
} catch (error) {
|
||
console.error('获取预览失败:', error);
|
||
throw error;
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 删除已上传的文件
|
||
*/
|
||
async deleteUploadedFile(filePath: string): Promise<{ success: boolean; message: string }> {
|
||
const url = `${BACKEND_BASE_URL}/upload/file?file_path=${encodeURIComponent(filePath)}`;
|
||
|
||
try {
|
||
const response = await fetch(url, {
|
||
method: 'DELETE',
|
||
});
|
||
if (!response.ok) {
|
||
const error = await response.json();
|
||
throw new Error(error.detail || '删除失败');
|
||
}
|
||
return await response.json();
|
||
} catch (error) {
|
||
console.error('删除文件失败:', error);
|
||
throw error;
|
||
}
|
||
},
|
||
};
|
||
|
||
// ==================== AI 分析 API ====================
|
||
|
||
export interface AIChartRequest {
|
||
analysis_text: string;
|
||
original_filename?: string;
|
||
file_type?: string;
|
||
}
|
||
|
||
export interface VisualizationResult {
|
||
success: boolean;
|
||
statistics?: {
|
||
numeric?: Record<string, any>;
|
||
categorical?: Record<string, any>;
|
||
};
|
||
charts?: {
|
||
histograms?: Array<any>;
|
||
bar_charts?: Array<any>;
|
||
box_plots?: Array<any>;
|
||
correlation?: any;
|
||
};
|
||
distributions?: Record<string, any>;
|
||
row_count?: number;
|
||
column_count?: number;
|
||
error?: string;
|
||
}
|
||
|
||
export const aiApi = {
|
||
|
||
/**
|
||
* 上传并使用 AI 分析 Excel 文件
|
||
*/
|
||
async analyzeExcel(
|
||
file: File,
|
||
options: AIAnalyzeOptions = {}
|
||
): Promise<AIExcelAnalyzeResult> {
|
||
const formData = new FormData();
|
||
formData.append('file', file);
|
||
|
||
const params = new URLSearchParams();
|
||
if (options.userPrompt) {
|
||
params.append('user_prompt', options.userPrompt);
|
||
}
|
||
if (options.analysisType) {
|
||
params.append('analysis_type', options.analysisType);
|
||
}
|
||
if (options.parseAllSheets === true) {
|
||
params.append('parse_all_sheets', 'true');
|
||
}
|
||
|
||
const url = `${BACKEND_BASE_URL}/ai/analyze/excel?${params.toString()}`;
|
||
|
||
try {
|
||
const response = await fetch(url, {
|
||
method: 'POST',
|
||
body: formData,
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const error = await response.json();
|
||
throw new Error(error.detail || 'AI 分析失败');
|
||
}
|
||
|
||
return await response.json();
|
||
} catch (error) {
|
||
console.error('AI 分析失败:', error);
|
||
throw error;
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 对已解析的 Excel 数据进行 AI 分析
|
||
*/
|
||
async analyzeText(
|
||
excelData: ExcelData,
|
||
userPrompt: string = '',
|
||
analysisType: string = 'general'
|
||
): Promise<AIAnalysisResult> {
|
||
const url = `${BACKEND_BASE_URL}/ai/analyze/text`;
|
||
|
||
try {
|
||
const response = await fetch(url, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
excel_data: excelData,
|
||
user_prompt: userPrompt,
|
||
analysis_type: analysisType,
|
||
}),
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const error = await response.json();
|
||
throw new Error(error.detail || '文本分析失败');
|
||
}
|
||
|
||
return await response.json();
|
||
} catch (error) {
|
||
console.error('文本分析失败:', error);
|
||
throw error;
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 获取支持的分析类型
|
||
*/
|
||
async getAnalysisTypes(): Promise<{
|
||
types: Array<{ value: string; label: string; description: string }>;
|
||
}> {
|
||
const url = `${BACKEND_BASE_URL}/ai/analysis/types`;
|
||
|
||
try {
|
||
const response = await fetch(url);
|
||
if (!response.ok) throw new Error('获取分析类型失败');
|
||
return await response.json();
|
||
} catch (error) {
|
||
console.error('获取分析类型失败:', error);
|
||
throw error;
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 上传并使用 AI 分析 Markdown 文件
|
||
*/
|
||
async analyzeMarkdown(
|
||
file: File,
|
||
options: {
|
||
analysisType?: MarkdownAnalysisType;
|
||
userPrompt?: string;
|
||
sectionNumber?: string;
|
||
} = {}
|
||
): Promise<AIMarkdownAnalyzeResult> {
|
||
const formData = new FormData();
|
||
formData.append('file', file);
|
||
|
||
const params = new URLSearchParams();
|
||
if (options.analysisType) {
|
||
params.append('analysis_type', options.analysisType);
|
||
}
|
||
if (options.userPrompt) {
|
||
params.append('user_prompt', options.userPrompt);
|
||
}
|
||
if (options.sectionNumber) {
|
||
params.append('section_number', options.sectionNumber);
|
||
}
|
||
|
||
const url = `${BACKEND_BASE_URL}/ai/analyze/md?${params.toString()}`;
|
||
|
||
try {
|
||
const response = await fetch(url, {
|
||
method: 'POST',
|
||
body: formData,
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const error = await response.json();
|
||
throw new Error(error.detail || 'Markdown AI 分析失败');
|
||
}
|
||
|
||
return await response.json();
|
||
} catch (error) {
|
||
console.error('Markdown AI 分析失败:', error);
|
||
throw error;
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 流式分析 Markdown 文件 (SSE)
|
||
*/
|
||
async analyzeMarkdownStream(
|
||
file: File,
|
||
options: {
|
||
analysisType?: MarkdownAnalysisType;
|
||
userPrompt?: string;
|
||
sectionNumber?: string;
|
||
} = {},
|
||
onChunk?: (chunk: { type: string; delta?: string; error?: string }) => void
|
||
): Promise<string> {
|
||
const formData = new FormData();
|
||
formData.append('file', file);
|
||
|
||
const params = new URLSearchParams();
|
||
if (options.analysisType) {
|
||
params.append('analysis_type', options.analysisType);
|
||
}
|
||
if (options.userPrompt) {
|
||
params.append('user_prompt', options.userPrompt);
|
||
}
|
||
if (options.sectionNumber) {
|
||
params.append('section_number', options.sectionNumber);
|
||
}
|
||
|
||
const url = `${BACKEND_BASE_URL}/ai/analyze/md/stream?${params.toString()}`;
|
||
|
||
try {
|
||
const response = await fetch(url, {
|
||
method: 'POST',
|
||
body: formData,
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const error = await response.json();
|
||
throw new Error(error.detail || 'Markdown AI 流式分析失败');
|
||
}
|
||
|
||
const reader = response.body?.getReader();
|
||
if (!reader) throw new Error('无法读取响应流');
|
||
|
||
const decoder = new TextDecoder();
|
||
let fullResponse = '';
|
||
|
||
while (true) {
|
||
const { done, value } = await reader.read();
|
||
if (done) break;
|
||
|
||
const chunk = decoder.decode(value);
|
||
const lines = chunk.split('\n');
|
||
|
||
for (const line of lines) {
|
||
if (line.startsWith('data: ')) {
|
||
const data = line.slice(6);
|
||
if (data === '[DONE]') continue;
|
||
|
||
try {
|
||
const parsed = JSON.parse(data);
|
||
if (parsed.type === 'content' && parsed.delta) {
|
||
fullResponse += parsed.delta;
|
||
onChunk?.({ type: 'content', delta: parsed.delta });
|
||
} else if (parsed.type === 'done') {
|
||
fullResponse = parsed.full_response || fullResponse;
|
||
} else if (parsed.error) {
|
||
onChunk?.({ type: 'error', error: parsed.error });
|
||
}
|
||
} catch {
|
||
// Ignore parse errors for incomplete JSON
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return fullResponse;
|
||
} catch (error) {
|
||
console.error('Markdown AI 流式分析失败:', error);
|
||
throw error;
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 获取 Markdown 文档大纲(分章节信息)
|
||
*/
|
||
async getMarkdownOutline(file: File): Promise<MarkdownOutlineResult> {
|
||
const formData = new FormData();
|
||
formData.append('file', file);
|
||
|
||
const url = `${BACKEND_BASE_URL}/ai/analyze/md/outline`;
|
||
|
||
try {
|
||
const response = await fetch(url, {
|
||
method: 'GET',
|
||
body: formData,
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const error = await response.json();
|
||
throw new Error(error.detail || '获取 Markdown 大纲失败');
|
||
}
|
||
|
||
return await response.json();
|
||
} catch (error) {
|
||
console.error('获取 Markdown 大纲失败:', error);
|
||
throw error;
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 生成统计信息和图表
|
||
*/
|
||
async generateStatistics(
|
||
excelData: ExcelData,
|
||
analysisType: string = 'statistics'
|
||
): Promise<VisualizationResult> {
|
||
const url = `${BACKEND_BASE_URL}/visualization/statistics`;
|
||
|
||
try {
|
||
const response = await fetch(url, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
excel_data: excelData,
|
||
analysis_type: analysisType
|
||
}),
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const error = await response.json();
|
||
throw new Error(error.detail || '生成图表失败');
|
||
}
|
||
|
||
return await response.json();
|
||
} catch (error) {
|
||
console.error('生成图表失败:', error);
|
||
throw error;
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 获取支持的图表类型
|
||
*/
|
||
async getChartTypes(): Promise<{
|
||
chart_types: Array<{ value: string; label: string; description: string }>;
|
||
}> {
|
||
const url = `${BACKEND_BASE_URL}/visualization/chart-types`;
|
||
|
||
try {
|
||
const response = await fetch(url);
|
||
if (!response.ok) throw new Error('获取图表类型失败');
|
||
return await response.json();
|
||
} catch (error) {
|
||
console.error('获取图表类型失败:', error);
|
||
throw error;
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 从 AI 分析结果中提取数据并生成图表
|
||
*/
|
||
async extractAndGenerateCharts(request: AIChartRequest) {
|
||
const url = `${BACKEND_BASE_URL}/analysis/extract-and-chart`;
|
||
|
||
try {
|
||
const response = await fetch(url, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(request),
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const error = await response.json();
|
||
throw new Error(error.detail || '生成图表失败');
|
||
}
|
||
|
||
return await response.json();
|
||
} catch (error) {
|
||
console.error('生成分析结果图表失败:', error);
|
||
throw error;
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 仅提取结构化数据(调试用)
|
||
*/
|
||
async analyzeTextOnly(request: AIChartRequest) {
|
||
const url = `${BACKEND_BASE_URL}/analysis/analyze-text`;
|
||
|
||
try {
|
||
const response = await fetch(url, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(request),
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const error = await response.json();
|
||
throw new Error(error.detail || '分析失败');
|
||
}
|
||
|
||
return await response.json();
|
||
} catch (error) {
|
||
console.error('分析文本失败:', error);
|
||
throw error;
|
||
}
|
||
},
|
||
|
||
// ==================== Word AI 解析 ====================
|
||
|
||
/**
|
||
* 使用 AI 解析 Word 文档,提取结构化数据
|
||
*/
|
||
async analyzeWordWithAI(
|
||
file: File,
|
||
userHint: string = ''
|
||
): Promise<{
|
||
success: boolean;
|
||
type?: string;
|
||
headers?: string[];
|
||
rows?: string[][];
|
||
key_values?: Record<string, string>;
|
||
list_items?: string[];
|
||
summary?: string;
|
||
error?: string;
|
||
}> {
|
||
const formData = new FormData();
|
||
formData.append('file', file);
|
||
if (userHint) {
|
||
formData.append('user_hint', userHint);
|
||
}
|
||
|
||
const url = `${BACKEND_BASE_URL}/ai/analyze/word`;
|
||
|
||
try {
|
||
const response = await fetch(url, {
|
||
method: 'POST',
|
||
body: formData,
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const error = await response.json();
|
||
throw new Error(error.detail || 'Word AI 解析失败');
|
||
}
|
||
|
||
return await response.json();
|
||
} catch (error) {
|
||
console.error('Word AI 解析失败:', error);
|
||
throw error;
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 使用 AI 解析 Word 文档并填写模板
|
||
* 一次性完成:AI解析 + 填表
|
||
*/
|
||
async fillTemplateFromWordAI(
|
||
file: File,
|
||
templateFields: TemplateField[],
|
||
userHint: string = ''
|
||
): Promise<FillResult> {
|
||
const formData = new FormData();
|
||
formData.append('file', file);
|
||
formData.append('template_fields', JSON.stringify(templateFields));
|
||
if (userHint) {
|
||
formData.append('user_hint', userHint);
|
||
}
|
||
|
||
const url = `${BACKEND_BASE_URL}/ai/analyze/word/fill-template`;
|
||
|
||
try {
|
||
const response = await fetch(url, {
|
||
method: 'POST',
|
||
body: formData,
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const error = await response.json();
|
||
throw new Error(error.detail || 'Word AI 填表失败');
|
||
}
|
||
|
||
return await response.json();
|
||
} catch (error) {
|
||
console.error('Word AI 填表失败:', error);
|
||
throw error;
|
||
}
|
||
},
|
||
};
|