AstroResearch/dashboard/src/hooks/useLibrary.ts
Asfmq 5db4cc5998 refactor: 全栈架构重构与质量硬化——API 错误统一、工具域重组、安全加固、前端组件化
后端核心变更:
  - API 层: 新增 AppError 枚举统一错误类型,替代散落的 (StatusCode, String)
  - Agent 工具域: 重组为 astro/system/ 和 astro/research/ 两级域,新增 ProcessPaperTool 流水线工具
  - 安全: 新增 SSRF 双层防护 (同步字符串级 + 异步 DNS 解析级),覆盖 IPv4/IPv6 私网段
  - 弱密码检测: 扩展弱密码列表并增加最小长度检查
  - LLM 客户端: 新增 ChatCompleter/Embedder trait,支持依赖注入与批量向量化 embed_batch
  - 批量处理: AssetBatch 从串行改为 Semaphore 并发池 (BATCH_CONCURRENCY=3)
  - 分块器: 重写为三阶段结构化管线 (章节解析→短节合并→带标题路径子块)
  - RAG: embedding 计算移出事务,RetrievalResult 新增 headings/section_index 字段
  - 检索: ADS/arXiv 并行检索 (tokio::join!),去重改用 HashSet,本地库回填批量 IN 查询
  - 天体查询: Sesame API 升级到 v4,新增视差误差/自行/视向速度/多波段测光字段
  - 迁移: 14 个增量文件合并为单一 init.sql,支持 sqlx::migrate! 内存库集成测试
  - 测试: circuit_breaker/hooks/task_board/session/memory/streaming_executor 新增修正 15+ 测试

  前端架构重构:
  - 目录重组: features/ → pages/ + components/ + hooks/ 三层分离
  - App.tsx 从 1181 行压缩至 ~174 行 (逻辑抽入 9 个自定义 Hook)
  - Agent 面板拆分为 AgentSessionSidebar/AgentMessageList/AgentInputArea 子组件
  - 新增 GlobalDialog/PaperDetailModal/UncachedPaperModal 通用对话框组件
  - 工具函数抽取: celestial.ts (天体坐标格式), paper.tsx (文献信息渲染)
2026-06-25 23:45:37 +08:00

217 lines
7.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// dashboard/src/hooks/useLibrary.ts
import { useState, useEffect } from 'react';
import axios from 'axios';
import type { StandardPaper } from '../types';
interface UseLibraryProps {
isAuthenticated: boolean | null;
showAlert: (message: string, title?: string) => void;
showConfirm: (message: string, onConfirm: () => void, title?: string) => void;
openReader: (paper: StandardPaper, skipTabSwitch?: boolean) => void;
}
export function useLibrary({
isAuthenticated,
showAlert,
showConfirm,
openReader,
}: UseLibraryProps) {
const [library, setLibrary] = useState<StandardPaper[]>([]);
const [selectedPaper, setSelectedPaper] = useState<StandardPaper | null>(null);
const [detailBibcode, setDetailBibcode] = useState<string | null>(null);
const [recentlySelected, setRecentlySelected] = useState<StandardPaper[]>(() => {
try {
const saved = localStorage.getItem('astro_recently_selected');
return saved ? JSON.parse(saved) : [];
} catch (e) {
console.error('Failed to parse recently selected papers', e);
return [];
}
});
const [downloadingBibcodes, setDownloadingBibcodes] = useState<Record<string, boolean>>({});
const [uploadingBibcode, setUploadingBibcode] = useState<string | null>(null);
useEffect(() => {
localStorage.setItem('astro_recently_selected', JSON.stringify(recentlySelected));
}, [recentlySelected]);
const addPaperToRecent = (paper: StandardPaper) => {
setRecentlySelected(prev => {
const filtered = prev.filter(p => p.bibcode !== paper.bibcode);
return [paper, ...filtered].slice(0, 5);
});
};
const openCitation = (
paper: StandardPaper,
setActiveTab: (tab: any) => void,
loadCitations: (bibcode: string, reset?: boolean) => void
) => {
setSelectedPaper(paper);
setActiveTab('citation');
loadCitations(paper.bibcode, true);
addPaperToRecent(paper);
};
const fetchLibrary = async () => {
try {
const res = await axios.get<StandardPaper[]>('/api/library');
setLibrary(res.data);
const lastReadBibcode = localStorage.getItem('last_read_bibcode');
if (lastReadBibcode) {
const lastReadPaper = res.data.find(p => p.bibcode === lastReadBibcode);
if (lastReadPaper) {
const initialTab = localStorage.getItem('astro_active_tab') || 'search';
openReader(lastReadPaper, initialTab !== 'reader');
}
}
} catch (e) {
console.error('加载本地文献库失败', e);
}
};
// 初始化加载本地文献库
useEffect(() => {
if (isAuthenticated === true) {
fetchLibrary();
}
}, [isAuthenticated]);
// 触发文献双格式下载
const handleDownload = async (bibcode: string, force = false, searchResultsSetter?: React.Dispatch<React.SetStateAction<StandardPaper[]>>) => {
setDownloadingBibcodes(prev => ({ ...prev, [bibcode]: true }));
try {
const res = await axios.post<StandardPaper>('/api/download', { bibcode, force });
if (searchResultsSetter) {
searchResultsSetter(prev => prev.map(p => p.bibcode === bibcode ? res.data : p));
}
setLibrary(prev => {
if (prev.some(p => p.bibcode === bibcode)) {
return prev.map(p => p.bibcode === bibcode ? res.data : p);
} else {
return [res.data, ...prev];
}
});
if (selectedPaper?.bibcode === bibcode) {
setSelectedPaper(res.data);
}
} catch (e) {
console.error('下载文献失败', e);
showAlert('文献下载失败,请检查 ADS 网络限制与网络代理!', '下载失败');
} finally {
setDownloadingBibcodes(prev => ({ ...prev, [bibcode]: false }));
}
};
// 手动上传文献文件以绕过反爬/人机验证
const handleManualUpload = async (bibcode: string, type: 'pdf' | 'html', file: File, searchResultsSetter?: React.Dispatch<React.SetStateAction<StandardPaper[]>>) => {
setUploadingBibcode(bibcode);
const formData = new FormData();
formData.append('bibcode', bibcode);
formData.append('type', type);
formData.append('file', file);
try {
const res = await axios.post<StandardPaper>('/api/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
if (searchResultsSetter) {
searchResultsSetter(prev => prev.map(p => p.bibcode === bibcode ? res.data : p));
}
setLibrary(prev => {
if (prev.some(p => p.bibcode === bibcode)) {
return prev.map(p => p.bibcode === bibcode ? res.data : p);
} else {
return [res.data, ...prev];
}
});
if (selectedPaper?.bibcode === bibcode) {
setSelectedPaper(res.data);
}
showAlert('手动文献文件上传导入成功!', '上传成功');
} catch (e: any) {
console.error('手动文件上传失败', e);
const errMsg = e.response?.data || '请确保上传的是合法且完整的文件。';
showAlert(`文件上传失败: ${errMsg}`, '上传出错');
} finally {
setUploadingBibcode(null);
}
};
// 手动标记文献为“无有效全文资源”
const handleMarkNoResource = async (bibcode: string, clear = false, searchResultsSetter?: React.Dispatch<React.SetStateAction<StandardPaper[]>>) => {
const performMark = async () => {
try {
const res = await axios.post<StandardPaper>('/api/no_resource', { bibcode, clear });
if (searchResultsSetter) {
searchResultsSetter(prev => prev.map(p => p.bibcode === bibcode ? res.data : p));
}
setLibrary(prev => {
if (prev.some(p => p.bibcode === bibcode)) {
return prev.map(p => p.bibcode === bibcode ? res.data : p);
} else {
return [res.data, ...prev];
}
});
if (selectedPaper?.bibcode === bibcode) {
setSelectedPaper(res.data);
}
showAlert(
clear
? '已成功清除“无全文资源”标记,该文献已被重新允许重载!'
: '已成功将文献标记为“无全文资源”,后续批量任务将自动跳过!',
'标记更新'
);
} catch (e: any) {
console.error('标记更新失败', e);
const errMsg = e.response?.data || '请稍后重试。';
showAlert(`标记失败: ${errMsg}`, '操作出错');
}
};
if (clear) {
performMark();
} else {
showConfirm(
'确认将此文献标记为“无有效全文资源”吗?\n标记后未来的批量下载/解析重试任务将自动跳过此文献。',
performMark,
'标记无有效全文资源'
);
}
};
// 衍生状态计算
const getDetailPaper = (searchResults: StandardPaper[]) => {
return library.find(p => p.bibcode === detailBibcode) || searchResults.find(p => p.bibcode === detailBibcode);
};
return {
library,
setLibrary,
selectedPaper,
setSelectedPaper,
detailBibcode,
setDetailBibcode,
recentlySelected,
setRecentlySelected,
downloadingBibcodes,
setDownloadingBibcodes,
uploadingBibcode,
addPaperToRecent,
openCitation,
fetchLibrary,
handleDownload,
handleManualUpload,
handleMarkNoResource,
getDetailPaper,
};
}