AstroResearch/dashboard/src/pages/ResearchAgentPanel.tsx
Asfmq 2f1fd19d74 refactor: 观测层双轴正交重构——spectra→observation、工具/API 收敛、安全韧性加固
将"以光谱为中心"的观测数据架构升级为 (Source × ProductType) 双轴正交模型,
光谱降级为与光变/测光/图像平级的产品类型之一;同步把分散的工具、API、缓存表
收敛为统一入口。新增 Gaia 光变曲线(EPOCH_PHOTOMETRY)支持。

【架构】services/spectra 整体替换为 services/observation(双轴正交)
- Source(LAMOST/Gaia/SDSS/DESI)× ProductType(Spectrum/LightCurve/Photometry/Image)
  正交组合,新增源/产品类型为纯加法(OCP)
- ObservationFetcher trait + ObservationRegistry:每个有效组合实现一个 fetcher,
  启动时注册;SDSS specobj/APOGEE 共用 key 按 subtype 二级路由
- cone 缓存逻辑模板方法化(trait 默认方法),消除各源 4 份重复代码
- 多文件 Artifact 模型:一个逻辑产物可含多文件(如 Gaia 光变 G/BP/RP 三波段各一 FITS)
- 统一编排 dispatch.rs:search(仅检索)/ download(检索+下载),支持坐标模式
  (cone→选源→下载)与标识符模式(直按 ID 下载)双输入

【Agent 工具整合】26 → 24
- 新增 find_observation:跨源×跨产品×双模式统一观测下载,取代 find_spectrum
- catalog_operation 升级为 6 合 1(search/describe/query/cone/export/lookup),
  取代独立的 query_vizier + cone_search
- citation_network + library_search 合并为 library.rs

【API 路由】
- 新增 /observation/{search,download,capabilities,list} 命名空间
- 移除 /catalog/{crossmatch,spectrum/download,spectrum/list}
- GET /observation/capabilities 暴露 registry 能力清单,前端动态渲染源/产品/版本
  下拉(不再硬编码各源支持矩阵)

【数据库迁移】
- 新表 observation_cache:新增 product 列 + artifacts_json(多文件产物),无 TTL
  (观测数据不可变,区别于 vizier_query_cache 的 7 天 TTL)
- 20260705140001:spectrum_cache 旧数据迁入 observation_cache,单文件→单元素 artifacts

【前端】
- 新 ObservationPanel(988 行):双视图(检索下载 / 缓存库),选项由 capabilities 动态生成
- 新 useObservation hook、ObservationResultCard、observation/constants、utils/apiError

【安全与韧性加固】
- sessions 锁 Mutex → RwLock(读多写少,降低争用)
- 新增 upload_rate_limiter;login_rate_limiter 容量保护(10000 上限,超限清最旧一半)
- bookmarklet API 密钥 SHA-1 → SHA-256;ADMIN_PASSWORD 长度上限 128
- *_TIMEOUT_SECS / EMBEDDING_DIM 非法值告警并回退默认;DB_POOL_SIZE 可配置(原硬编码 5)
- sqlite-vec 注册逻辑下沉至 utils::register_sqlite_vec_extension
2026-07-07 01:34:02 +08:00

117 lines
4.5 KiB
TypeScript

import { useResearchAgent } from '../hooks/useResearchAgent';
import { AgentSessionSidebar } from '../components/agent/AgentSessionSidebar';
import { AgentMessageList } from '../components/agent/AgentMessageList';
import { AgentInputArea } from '../components/agent/AgentInputArea';
import type { StandardPaper } from '../types';
import type { useCitations } from '../hooks/useCitations';
import type { useLibrary } from '../hooks/useLibrary';
import type { TabId } from '../components/layout/Sidebar';
interface ResearchAgentPanelProps {
showConfirm?: (
message: string,
onConfirm: () => void,
title?: string
) => void;
showAlert?: (message: string, title?: string) => void;
openReader?: (paper: StandardPaper, skipTabSwitch?: boolean) => void;
setActiveTab?: (tab: TabId) => void;
citations?: ReturnType<typeof useCitations>;
library?: ReturnType<typeof useLibrary>;
}
export function ResearchAgentPanel({
showConfirm,
showAlert,
openReader,
setActiveTab,
citations,
library,
}: ResearchAgentPanelProps) {
const state = useResearchAgent({ showConfirm, showAlert });
return (
<div className="w-full flex-1 flex overflow-hidden bg-slate-100 rounded-lg border border-slate-200 h-full lg:h-[calc(100vh-130px)] shadow-sm">
{/* 左侧会话列表侧栏 */}
<AgentSessionSidebar
sessions={state.sessions}
currentSessionId={state.currentSessionId}
setCurrentSessionId={state.setCurrentSessionId}
loadingSessions={state.loadingSessions}
searchQuery={state.searchQuery}
setSearchQuery={state.setSearchQuery}
searchScopeOnlyCurrent={state.searchScopeOnlyCurrent}
setSearchScopeOnlyCurrent={state.setSearchScopeOnlyCurrent}
searchResults={state.searchResults}
loadingSearch={state.loadingSearch}
collapsed={state.sidebarCollapsed}
onToggleCollapse={state.setSidebarCollapsed}
onNewSession={state.handleNewSession}
onDeleteSession={state.handleDeleteSession}
/>
{/* 右侧会话对话展示及输入面板 */}
<div className="flex-1 flex flex-col overflow-hidden bg-white relative">
<AgentMessageList
turns={state.turns}
activeTurn={state.activeTurn}
streaming={state.streaming}
currentSessionId={state.currentSessionId}
expandedThoughts={state.expandedThoughts}
expandedArgs={state.expandedArgs}
expandedResults={state.expandedResults}
collapsedSubAgents={state.collapsedSubAgents}
toggleThought={state.toggleThought}
toggleArgs={state.toggleArgs}
toggleResult={state.toggleResult}
toggleSubAgent={state.toggleSubAgent}
handleRewind={state.handleRewind}
handleRestoreRewind={state.handleRestoreRewind}
handleRetry={state.handleRetry}
handleBranch={state.handleBranch}
handleSend={state.handleSend}
scrollContainerRef={state.scrollContainerRef}
chatEndRef={state.chatEndRef}
handleScroll={state.handleScroll}
showMetrics={state.showMetrics}
setShowMetrics={state.setShowMetrics}
showAuditLog={state.showAuditLog}
setShowAuditLog={state.setShowAuditLog}
hasPendingPermission={state.hasPendingPermission}
setHasPendingPermission={state.setHasPendingPermission}
hasPendingQuestion={state.hasPendingQuestion}
setHasPendingQuestion={state.setHasPendingQuestion}
showAlert={showAlert}
sidebarCollapsed={state.sidebarCollapsed}
setSidebarCollapsed={state.setSidebarCollapsed}
sessions={state.sessions}
handleDeleteSession={state.handleDeleteSession}
loadSessionHistory={state.loadSessionHistory}
openReader={openReader}
setActiveTab={setActiveTab}
citations={citations}
library={library}
/>
<AgentInputArea
input={state.input}
setInput={state.setInput}
streaming={state.streaming}
thinking={state.thinking}
setThinking={state.setThinking}
agentMode={state.agentMode}
setAgentMode={state.setAgentMode}
agentModes={state.agentModes}
pendingImage={state.pendingImage}
setPendingImage={state.setPendingImage}
onSend={state.handleSend}
onStop={state.handleStop}
fileInputRef={state.fileInputRef}
attachImage={state.attachImage}
handlePaste={state.handlePaste}
/>
</div>
</div>
);
}