From f885c0a4a8ec1c76ab848d0dfb854a66a83e0394 Mon Sep 17 00:00:00 2001 From: Asfmq <2696428814@qq.com> Date: Tue, 30 Jun 2026 19:26:01 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=9C=8D=E5=8A=A1=E5=B1=82?= =?UTF-8?q?=E6=8A=BD=E8=B1=A1=E4=B8=8B=E6=B2=89=E3=80=81=E5=BC=82=E6=AD=A5?= =?UTF-8?q?=E9=94=81=E5=85=A8=E6=A0=88=E8=BF=81=E7=A7=BB=E3=80=81=E5=AE=A2?= =?UTF-8?q?=E6=88=B7=E7=AB=AF=E9=9F=A7=E6=80=A7=E5=8A=A0=E5=9B=BA=E4=B8=8E?= =?UTF-8?q?=E7=A7=BB=E5=8A=A8=E7=AB=AF=E9=80=82=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 服务层拆分:删除 api/helpers.rs,新增 citation/note/session/pipeline/paper/vision 独立服务模块 - Agent 工具精简:paper_content+paper_outline 合并为 paper.rs,图片分析逻辑下沉至 services/vision - 并发模型升级:std::sync::{Mutex,RwLock} → tokio::sync::{Mutex,RwLock},消除 async 上下文中的阻塞风险 - 客户端加固:HTTP 客户端统一超时配置、ADS 429 / arXiv 503 自动重试、构造函数返回 Result - 启动安全:全局 panic hook 日志化、空密码拒绝启动、向量表维度不匹配需显式确认 - CLI 扩展:构建完整 AppState 复用服务层,新增 Content/Outline/Citations/Search/Process 子命令 - 前端:移动端汉堡菜单、侧栏滑出面板、引用星系触屏手势(单指拖拽/双指缩放) --- .env.example | 6 +- Cargo.toml | 2 +- dashboard/src/App.tsx | 37 +- .../src/components/CitationGalaxyCanvas.tsx | 84 +++ .../src/components/agent/AgentMessageList.tsx | 2 +- .../components/agent/AgentSessionSidebar.tsx | 343 ++++++------ .../src/components/agent/ToolCallCard.tsx | 2 +- dashboard/src/components/layout/Sidebar.tsx | 404 +++++++------- .../src/components/reader/BilingualViewer.tsx | 29 +- .../components/reader/ReaderNotesSidebar.tsx | 28 +- .../src/components/reader/ReaderToolbar.tsx | 12 +- dashboard/src/hooks/useResearchAgent.ts | 7 +- dashboard/src/pages/ReaderPanel.tsx | 41 +- ...260629000000_fix_foreign_key_on_update.sql | 72 +++ ...add_paper_chunks_content_bibcode_index.sql | 4 + src/agent/hooks/builtins.rs | 53 +- src/agent/hooks/mod.rs | 36 +- src/agent/hooks/registry.rs | 4 +- src/agent/memory/dedup.rs | 2 +- src/agent/memory/mod.rs | 3 +- src/agent/memory/selection.rs | 6 +- src/agent/runtime/circuit_breaker.rs | 1 + src/agent/runtime/error_recovery/mod.rs | 4 +- src/agent/runtime/executor/mod.rs | 45 +- src/agent/runtime/file_cache.rs | 43 +- src/agent/runtime/mod.rs | 67 +-- src/agent/runtime/partitioner.rs | 3 +- src/agent/runtime/session.rs | 9 +- src/agent/runtime/streaming.rs | 9 +- src/agent/runtime/streaming_executor.rs | 36 +- src/agent/skills/mod.rs | 8 +- src/agent/tools/ask_user.rs | 33 +- src/agent/tools/astro/analyze_image.rs | 139 +---- src/agent/tools/astro/mod.rs | 3 +- .../tools/astro/research/citation_network.rs | 425 ++++----------- src/agent/tools/astro/research/metadata.rs | 8 +- src/agent/tools/astro/research/mod.rs | 6 +- src/agent/tools/astro/research/note.rs | 35 +- src/agent/tools/astro/research/paper.rs | 197 +++++++ .../tools/astro/research/paper_content.rs | 166 ------ .../tools/astro/research/paper_outline.rs | 135 ----- src/agent/tools/astro/research/target.rs | 10 +- src/agent/tools/astro/system/process.rs | 200 ++----- src/agent/tools/filesystem/bash.rs | 102 +++- src/agent/tools/filesystem/glob.rs | 6 +- src/agent/tools/filesystem/read.rs | 11 + src/agent/tools/filesystem/security.rs | 9 +- src/agent/tools/mod.rs | 14 +- src/agent/tools/persist.rs | 20 +- src/agent/tools/search_history.rs | 7 +- src/agent/tools/skill.rs | 40 +- src/agent/tools/todo.rs | 35 +- src/api/agent.rs | 301 ++-------- src/api/auth.rs | 213 ++++---- src/api/helpers.rs | 496 ----------------- src/api/mod.rs | 63 +-- src/api/notes.rs | 74 +-- src/api/papers.rs | 516 ++++-------------- src/api/permissions.rs | 120 ++-- src/api/search.rs | 101 +--- src/api/sync.rs | 211 +------ src/api/targets.rs | 157 ++---- src/bin/cli.rs | 460 +++++++++++++++- src/clients/ads.rs | 70 ++- src/clients/arxiv.rs | 126 +++-- src/clients/llm/chat.rs | 56 +- src/clients/llm/embedding.rs | 119 ++-- src/lib.rs | 2 +- src/main.rs | 135 ++++- src/services/batch/asset/mod.rs | 204 ++++++- src/services/batch/meta.rs | 38 ++ src/services/batch/mod.rs | 4 +- src/services/citation.rs | 398 ++++++++++++++ src/services/download/mod.rs | 5 +- src/services/mod.rs | 7 +- src/services/note.rs | 121 ++++ src/services/paper/db.rs | 367 +++++++++++++ src/services/paper/ingest.rs | 138 +++++ src/services/paper/mod.rs | 20 + src/services/paper/model.rs | 110 ++++ .../{section_parser.rs => paper/reader.rs} | 171 +++++- src/services/parser/mod.rs | 47 +- src/services/pipeline.rs | 114 ++++ src/services/rag.rs | 136 ++--- src/services/search.rs | 199 +++++-- src/services/session.rs | 290 ++++++++++ src/services/target.rs | 291 ++++++---- src/services/translation.rs | 53 +- src/services/vision.rs | 129 +++++ src/utils/ssrf.rs | 35 +- 90 files changed, 5184 insertions(+), 3916 deletions(-) create mode 100644 migrations/20260629000000_fix_foreign_key_on_update.sql create mode 100644 migrations/20260630000000_add_paper_chunks_content_bibcode_index.sql create mode 100644 src/agent/tools/astro/research/paper.rs delete mode 100644 src/agent/tools/astro/research/paper_content.rs delete mode 100644 src/agent/tools/astro/research/paper_outline.rs delete mode 100644 src/api/helpers.rs create mode 100644 src/services/citation.rs create mode 100644 src/services/note.rs create mode 100644 src/services/paper/db.rs create mode 100644 src/services/paper/ingest.rs create mode 100644 src/services/paper/mod.rs create mode 100644 src/services/paper/model.rs rename src/services/{section_parser.rs => paper/reader.rs} (56%) create mode 100644 src/services/pipeline.rs create mode 100644 src/services/session.rs create mode 100644 src/services/vision.rs diff --git a/.env.example b/.env.example index afd2dd2..360157d 100644 --- a/.env.example +++ b/.env.example @@ -44,6 +44,8 @@ LLM_MODEL=gpt-4o-mini # EMBEDDING_MODEL=text-embedding-3-small # 向量维度(需与所选模型输出维度一致,默认 1536) # EMBEDDING_DIM=1536 +# 维度变更确认:修改 EMBEDDING_DIM 后需设置此变量为 1 才能启动(防止误改导致数据丢失) +# EMBEDDING_DIM_FORCE_REBUILD=1 # ───────────────────────────────────────────────────────────────────────────── # Chunker 结构化切片参数(影响向量化粒度) @@ -85,8 +87,8 @@ LIBRARY_DIR=./library SKILLS_DIR=./skills # 后端服务监听端口 PORT=8000 -# 认证密码 -ADMIN_PASSWORD=fmq +# 认证密码(必填,启动时未设置将拒绝启动) +ADMIN_PASSWORD=your_strong_password_here # ───────────────────────────────────────────────────────────────────────────── # 5. 日志配置 diff --git a/Cargo.toml b/Cargo.toml index 011168a..0521b09 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ path = "src/bin/cli.rs" [dependencies] tokio = { version = "1", features = ["full"] } axum = { version = "0.7", features = ["macros", "multipart"] } -tower-http = { version = "0.5", features = ["cors", "fs", "trace"] } +tower-http = { version = "0.5", features = ["cors", "fs", "trace", "set-header"] } sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "sqlite", "chrono", "json"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/dashboard/src/App.tsx b/dashboard/src/App.tsx index bd87748..0f2ce19 100644 --- a/dashboard/src/App.tsx +++ b/dashboard/src/App.tsx @@ -1,7 +1,7 @@ // dashboard/src/App.tsx import { useState, useEffect, useCallback, useRef } from 'react'; import axios from 'axios'; -import { Loader, BookOpen, GitFork, Lock } from 'lucide-react'; +import { Loader, BookOpen, GitFork, Lock, Menu } from 'lucide-react'; import { Sidebar } from './components/layout/Sidebar'; import { SearchPanel } from './pages/SearchPanel'; import { LibraryPanel } from './pages/LibraryPanel'; @@ -24,6 +24,9 @@ import { useNotes } from './hooks/useNotes'; import { useCitations } from './hooks/useCitations'; export default function App() { + // 移动端菜单显示状态 + const [isMobileSidebarOpen, setIsMobileSidebarOpen] = useState(false); + // 1. 全局自定义 Dialog 弹窗管理 (Alert / Confirm) const [dialog, setDialog] = useState<{ type: 'alert' | 'confirm'; @@ -318,11 +321,41 @@ export default function App() { selectedPaper={library.selectedPaper} loadCitations={citations.loadCitations} onLogout={auth.handleLogout} + isOpen={isMobileSidebarOpen} + onClose={() => setIsMobileSidebarOpen(false)} /> {/* 主工作区 */}
-
+ {/* 移动端顶部 Header */} +
+
+ +
+
+ +
+ AstroResearch +
+
+ + {activeTab === 'search' && '统一检索'} + {activeTab === 'library' && '馆藏管理'} + {activeTab === 'reader' && '双语阅读'} + {activeTab === 'citation' && '引用星系'} + {activeTab === 'sync' && '批量任务'} + {activeTab === 'agent' && '智能科研'} + +
+ +
diff --git a/dashboard/src/components/CitationGalaxyCanvas.tsx b/dashboard/src/components/CitationGalaxyCanvas.tsx index 617598d..3eed6b7 100644 --- a/dashboard/src/components/CitationGalaxyCanvas.tsx +++ b/dashboard/src/components/CitationGalaxyCanvas.tsx @@ -298,6 +298,29 @@ export function CitationGalaxyCanvas({ networks, activeNetwork, nodeLimit, onNod render(); + let initialTouchDistance = 0; + let initialScale = 1.0; + + const findNodeAtCoordinate = (clientX: number, clientY: number) => { + const rect = canvas.getBoundingClientRect(); + const touchX = clientX - rect.left; + const touchY = clientY - rect.top; + const cx = rect.width / 2; + const cy = rect.height / 2; + const gx = (touchX - cx - offsetX) / scale + cx; + const gy = (touchY - cy - offsetY) / scale + cy; + + for (const node of nodes) { + const dx = node.x - gx; + const dy = node.y - gy; + const dist = Math.sqrt(dx * dx + dy * dy); + if (dist < node.radius + 12) { // 移动端稍微加大触碰敏感区 + return node; + } + } + return null; + }; + const handleMouseDown = (e: MouseEvent) => { isDragging = true; dragStartX = e.clientX - offsetX; @@ -379,12 +402,68 @@ export function CitationGalaxyCanvas({ networks, activeNetwork, nodeLimit, onNod offsetY = mouseY - cy - (gy - cy) * scale; }; + // 触屏手势处理 + const handleTouchStart = (e: TouchEvent) => { + if (e.touches.length === 1) { + isDragging = true; + hasDragged = false; + dragStartX = e.touches[0].clientX - offsetX; + dragStartY = e.touches[0].clientY - offsetY; + + // 触碰选中节点 + hoveredNode = findNodeAtCoordinate(e.touches[0].clientX, e.touches[0].clientY); + } else if (e.touches.length === 2) { + isDragging = false; + const dx = e.touches[0].clientX - e.touches[1].clientX; + const dy = e.touches[0].clientY - e.touches[1].clientY; + initialTouchDistance = Math.sqrt(dx * dx + dy * dy) || 1; + initialScale = scale; + } + }; + + const handleTouchMove = (e: TouchEvent) => { + if (isDragging && e.touches.length === 1) { + const dx = e.touches[0].clientX - dragStartX; + const dy = e.touches[0].clientY - dragStartY; + if (Math.sqrt((dx - offsetX) ** 2 + (dy - offsetY) ** 2) > 3) { + hasDragged = true; + } + offsetX = dx; + offsetY = dy; + e.preventDefault(); // 阻止滚动 + } else if (e.touches.length === 2) { + const dx = e.touches[0].clientX - e.touches[1].clientX; + const dy = e.touches[0].clientY - e.touches[1].clientY; + const dist = Math.sqrt(dx * dx + dy * dy) || 1; + + // 双指开合缩放 + const newScale = initialScale * (dist / initialTouchDistance); + scale = Math.max(0.15, Math.min(5.0, newScale)); + e.preventDefault(); // 阻止默认双指缩放 + } + }; + + const handleTouchEnd = () => { + isDragging = false; + if (!hasDragged && hoveredNode) { + if (hoveredNode.id !== activeNetwork.bibcode) { + onNodeClick(hoveredNode.id); + } + } + hoveredNode = null; + }; + canvas.addEventListener('mousedown', handleMouseDown); canvas.addEventListener('mousemove', handleMouseMove); canvas.addEventListener('mouseup', handleMouseUp); canvas.addEventListener('mouseleave', handleMouseLeave); canvas.addEventListener('click', handleCanvasClick); canvas.addEventListener('wheel', handleWheel, { passive: false }); + + // 注册触控事件 + canvas.addEventListener('touchstart', handleTouchStart, { passive: false }); + canvas.addEventListener('touchmove', handleTouchMove, { passive: false }); + canvas.addEventListener('touchend', handleTouchEnd); return () => { cancelAnimationFrame(animationFrameId); @@ -394,6 +473,11 @@ export function CitationGalaxyCanvas({ networks, activeNetwork, nodeLimit, onNod canvas.removeEventListener('mouseleave', handleMouseLeave); canvas.removeEventListener('click', handleCanvasClick); canvas.removeEventListener('wheel', handleWheel); + + // 注销触控事件 + canvas.removeEventListener('touchstart', handleTouchStart); + canvas.removeEventListener('touchmove', handleTouchMove); + canvas.removeEventListener('touchend', handleTouchEnd); }; }, [networks, activeNetwork, onNodeClick]); diff --git a/dashboard/src/components/agent/AgentMessageList.tsx b/dashboard/src/components/agent/AgentMessageList.tsx index 13dd68d..9d87af9 100644 --- a/dashboard/src/components/agent/AgentMessageList.tsx +++ b/dashboard/src/components/agent/AgentMessageList.tsx @@ -190,7 +190,7 @@ export function AgentMessageList({ : '探索性科研研讨'}

- 科研智能体 (ReAct 循环:Thought → Action → Observation) + 科研智能体

diff --git a/dashboard/src/components/agent/AgentSessionSidebar.tsx b/dashboard/src/components/agent/AgentSessionSidebar.tsx index 6907718..8d665a1 100644 --- a/dashboard/src/components/agent/AgentSessionSidebar.tsx +++ b/dashboard/src/components/agent/AgentSessionSidebar.tsx @@ -37,173 +37,206 @@ export function AgentSessionSidebar({ onNewSession, onDeleteSession, }: AgentSessionSidebarProps) { - return ( -
-
-
- - - {!collapsed && 会话历史} - -
- - -
-
+ + const handleSelectSession = (id: string | null) => { + setCurrentSessionId(id); + if (typeof window !== 'undefined' && window.innerWidth < 1024) { + onToggleCollapse(true); + } + }; - {/* 搜索框区域 */} -
-
- setSearchQuery(e.target.value)} - placeholder="搜索历史会话或消息内容..." - className="w-full pl-8 pr-7 py-1.5 rounded-md bg-slate-50 border border-slate-200 text-slate-800 placeholder-slate-400 focus:outline-none focus:border-blueprint focus:bg-white transition-all text-[11px] font-medium" - /> -
- -
- {searchQuery && ( + const handleCreateNewSession = () => { + onNewSession(); + if (typeof window !== 'undefined' && window.innerWidth < 1024) { + onToggleCollapse(true); + } + }; + + return ( + <> + {/* 移动端背景遮罩层 */} + {!collapsed && ( + + +
+
+ + {/* 搜索框区域 */} +
+
+ setSearchQuery(e.target.value)} + placeholder="搜索历史会话或消息内容..." + className="w-full pl-8 pr-7 py-1.5 rounded-md bg-slate-50 border border-slate-200 text-slate-800 placeholder-slate-400 focus:outline-none focus:border-blueprint focus:bg-white transition-all text-[11px] font-medium" + /> +
+ +
+ {searchQuery && ( + + )} +
+ {currentSessionId && ( + )}
- {currentSessionId && ( - - )} -
-
- {searchQuery.trim() ? ( - loadingSearch ? ( -
- - 检索历史记录中... -
- ) : searchResults.length === 0 ? ( -
- 未找到匹配的历史记录 -
- ) : ( - searchResults.map((result, idx) => { - const isActive = result.session_id === currentSessionId; - const isMessage = result.result_type.startsWith('message/'); - const role = isMessage ? result.result_type.split('/')[1] : null; +
+ {searchQuery.trim() ? ( + loadingSearch ? ( +
+ + 检索历史记录中... +
+ ) : searchResults.length === 0 ? ( +
+ 未找到匹配的历史记录 +
+ ) : ( + searchResults.map((result, idx) => { + const isActive = result.session_id === currentSessionId; + const isMessage = result.result_type.startsWith('message/'); + const role = isMessage ? result.result_type.split('/')[1] : null; - return ( - - ); - }) - ) - ) : ( - // 正常的会话列表渲染 - loadingSessions ? ( -
- - 加载历史会话中... -
- ) : sessions.length === 0 ? ( -
- 暂无历史会话记录 -
- ) : ( - sessions.map(session => { - const isActive = session.session_id === currentSessionId; - return ( - - - ); - }) - ) - )} + ); + }) + ) + ) : ( + // 正常的会话列表渲染 + loadingSessions ? ( +
+ + 加载历史会话中... +
+ ) : sessions.length === 0 ? ( +
+ 暂无历史会话记录 +
+ ) : ( + sessions.map(session => { + const isActive = session.session_id === currentSessionId; + return ( + + + ); + }) + ) + )} +
-
+ ); } diff --git a/dashboard/src/components/agent/ToolCallCard.tsx b/dashboard/src/components/agent/ToolCallCard.tsx index 01d6257..34ce54b 100644 --- a/dashboard/src/components/agent/ToolCallCard.tsx +++ b/dashboard/src/components/agent/ToolCallCard.tsx @@ -75,7 +75,7 @@ export function ToolCallCard({
- 观测与反馈 (Observation) + 观测与反馈 {result!.isError && ( 错误返回 diff --git a/dashboard/src/components/layout/Sidebar.tsx b/dashboard/src/components/layout/Sidebar.tsx index f0d2423..4215e2c 100644 --- a/dashboard/src/components/layout/Sidebar.tsx +++ b/dashboard/src/components/layout/Sidebar.tsx @@ -12,207 +12,225 @@ interface SidebarProps { selectedPaper: StandardPaper | null; loadCitations: (bibcode: string) => void; onLogout: () => void; + isOpen?: boolean; + onClose?: () => void; } -export function Sidebar({ activeTab, setActiveTab, selectedPaper, loadCitations, onLogout }: SidebarProps) { +export function Sidebar({ activeTab, setActiveTab, selectedPaper, loadCitations, onLogout, isOpen = false, onClose }: SidebarProps) { const [isCollapsed, setIsCollapsed] = useState(false); + const effectiveCollapsed = isCollapsed && !isOpen; return ( -
+ + {/* 导航菜单列表 */} + +
+ + {/* 底部当前选定文献提示 (平滑动画版本) 与 退出登录 */} +
+ {selectedPaper ? ( +
+ {/* 折叠下的微型图书图标 */} +
+ +
+ + {/* 展开下的完整详情 */} +
+ 当前选定文献 +

{selectedPaper.title}

+
+ 发表年份: {selectedPaper.year} + {selectedPaper.bibcode} +
+
+
+ ) : ( +
+ {/* 折叠下的微型馆藏图标 */} +
+ +
+ + {/* 展开下的提示文字 */} +
+ 未选定研究目标 +
+
+ )} + + {/* 退出登录按钮 */} + -
- + + + 退出登录 + + + + + ); } diff --git a/dashboard/src/components/reader/BilingualViewer.tsx b/dashboard/src/components/reader/BilingualViewer.tsx index 00768a5..c4d41c0 100644 --- a/dashboard/src/components/reader/BilingualViewer.tsx +++ b/dashboard/src/components/reader/BilingualViewer.tsx @@ -1,4 +1,5 @@ // dashboard/src/components/reader/BilingualViewer.tsx +import { useState, useEffect } from 'react'; import { createPortal } from 'react-dom'; import ReactMarkdown from 'react-markdown'; import remarkMath from 'remark-math'; @@ -128,6 +129,28 @@ export function BilingualViewer({ hoverCardPos, children, }: BilingualViewerProps) { + // 监测屏幕宽度以判断是否在移动端/平板设备上(宽度 < 1024px) + const [isMobileViewport, setIsMobileViewport] = useState(() => { + if (typeof window !== 'undefined') { + return window.innerWidth < 1024; + } + return false; + }); + + useEffect(() => { + const handleResize = () => { + setIsMobileViewport(window.innerWidth < 1024); + }; + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, []); + + const gridTemplateColumns = isMobileViewport + ? (viewMode === 'bilingual' ? '1fr 1fr' : '1fr') + : (viewMode === 'bilingual' + ? (showNotesPanel ? '1fr 1fr 380px' : '1fr 1fr') + : (showNotesPanel ? '1fr 380px' : '1fr')); + // 解析英文和中文段落的 Front Matter 头部元数据 const { metadata: engMeta, pureMarkdown: engPure } = parseMarkdownFrontMatter(englishText); const { metadata: chnMeta, pureMarkdown: chnPure } = parseMarkdownFrontMatter(chineseText); @@ -150,11 +173,7 @@ export function BilingualViewer({ return (
{/* 英文正文视窗 (或双语对照) */} {(viewMode === 'english' || viewMode === 'bilingual') && ( diff --git a/dashboard/src/components/reader/ReaderNotesSidebar.tsx b/dashboard/src/components/reader/ReaderNotesSidebar.tsx index 6f9735b..7ec55c1 100644 --- a/dashboard/src/components/reader/ReaderNotesSidebar.tsx +++ b/dashboard/src/components/reader/ReaderNotesSidebar.tsx @@ -69,15 +69,24 @@ export function ReaderNotesSidebar({ if (!showNotesPanel) return null; return ( -
-
- - {sidebarTab === 'notes' ? '观测记录手札' : '文献 AI 问答'} - - -
+ <> + {/* 移动端遮罩层 */} + +
{/* 标签栏选择器 */}
@@ -281,5 +290,6 @@ export function ReaderNotesSidebar({
)}
+ ); } diff --git a/dashboard/src/components/reader/ReaderToolbar.tsx b/dashboard/src/components/reader/ReaderToolbar.tsx index 0a42bc0..cf29090 100644 --- a/dashboard/src/components/reader/ReaderToolbar.tsx +++ b/dashboard/src/components/reader/ReaderToolbar.tsx @@ -57,18 +57,18 @@ export function ReaderToolbar({ showPdf, }: ReaderToolbarProps) { return ( -
-
+
+

{selectedPaper.title}

-
+
发表期刊: {selectedPaper.pub_journal || '未标注'} - + 文献编码: {selectedPaper.bibcode}
-
+
{/* 快速切换文献菜单 */}