feat: 科研分析层全栈落地——光谱/时域/运动学分析工具链 + JWST/X 射线数据源 + 定时文献同步
数据分析层(新增 services/{spectrum,timeseries,analysis}):
- 光谱参数提取 parameters.rs:LAMOST/SDSS/APOGEE/DESI FITS header 跨源归一化读取
Teff/logg/[Fe/H]/RV 及 ASPCAP 20+ 元素丰度,rayon 并发批量提取
- 谱线测量 lines.rs:内置真空/空气波长谱线表,窗口内极值搜索 + 梯形法积分 EW + FWHM,支持自定义谱线
- 交叉相关测速 cross_correlate.rs:对数波长重采样对齐,内置 Pickles 模板按光谱型插值,
CCF 峰值位置提取 RV 及不确定度
- 周期搜索 periodicity.rs:Lomb-Scargle 周期图(含 FAP 误报概率)+ BLS 凌星检测 + 相位折叠
- 变星分类 classification.rs:振幅/偏度/峰度/过零率/eta 等统计特征 + 规则分类(RR Lyrae/Cepheid/食双星/AGN 等)
- SED 拟合 sed.rs:多波段测光黑体模型拟合,输出 T_eff/半径/消光 A_V/光度及不确定度
- 运动学 kinematics.rs:视差+自行+RV → 银河系 UVW 空间速度,含移动星群成员概率(Banyan Σ 简化版)
- 化学丰度 chemistry.rs:[α/Fe] vs [Fe/H] 计算,厚盘/薄盘/晕星族判别
- 观测规划 observability.rs:目标升落时间/airmass/月相影响/曝光时间估算
- 赫罗图 hr_diagram.rs:Gaia TAP CMD 查询,新增 GET /api/analysis/hr-diagram 端点
数据获取层:
- JWST:clients/mast/jwst.rs 封装 MAST Portal 锥形检索 + JwstSpectrumFetcher(NIRSpec/MIRI 光谱)
- X 射线:clients/heasarc 封装 HEASARC TAP(ADQL)+ XMM-Newton/Chandra 光谱 fetcher
- 图像 cutout:SDSS SkyServer/STScI DSS/Pan-STARRS 三源 cutout + 发现图(Finding Chart)生成
- Source 枚举新增 Jwst/Xmm/Chandra 并注册 ObservationRegistry,前端 SOURCE_THEME 与筛选器同步三源
Agent 工具集(24→35):
- 新增 9 个分析工具:get_spectrum_parameters / measure_spectral_lines / measure_radial_velocity /
find_period / classify_variable_star / fit_sed / analyze_kinematics / analyze_abundance_pattern / plan_observation
- batch_process:批量样本"查询→下载→分析→报告"流水线,并发控制防数据源速率限制
- literature_monitor:按 ADS 查询式/时间窗/最低引用数检查最新文献
定时文献同步:
- sync_queries 表新增 is_scheduled 列(migration 20260713)
- 新增 POST /sync/queries/:id/schedule 端点
- 服务启动时拉起每小时调度器,对 is_scheduled=1 的检索配置静默执行 ADS(entdate 增量)/arXiv 增量同步
- search_history 工具收敛至 services/search::search_agent_history,消除 FTS 查询逻辑重复
其他:
- plotting skill 由占位填充为完整科研绘图规范:光谱/光变/折叠曲线/CMD/SED/[α/Fe]/周期图/Mollweide/发现图 9 类 matplotlib 模板
- 删除死代码 streaming_executor.rs(929 行,仅剩 mod 声明引用,无调用方)
- 新增 docs/roadmap-research-features.md 科研功能路线图及实现状态
This commit is contained in:
@@ -19,7 +19,7 @@ import {
|
||||
import { AgentMarkdown } from './AgentMarkdown';
|
||||
import { ThoughtCard } from './ThoughtCard';
|
||||
import { ToolCallCard } from './ToolCallCard';
|
||||
import { TodoTaskCard } from './SpecialToolRenderers';
|
||||
import { TodoTaskCard, type TodoItem } from './SpecialToolRenderers';
|
||||
import { AnswerCard } from './AnswerCard';
|
||||
import { SubAgentContainer } from './SubAgentContainer';
|
||||
import { PARENT_COLORS } from './constants';
|
||||
@@ -106,7 +106,7 @@ interface AgentMessageListProps {
|
||||
showTaskSidebar?: boolean;
|
||||
setShowTaskSidebar?: (val: boolean) => void;
|
||||
hasTodos?: boolean;
|
||||
activeTodos?: any[];
|
||||
activeTodos?: TodoItem[];
|
||||
}
|
||||
|
||||
// 内部工具(is_internal = true)渲染模式配置:
|
||||
|
||||
@@ -419,7 +419,7 @@ export function PaperListCard({
|
||||
// ==========================================
|
||||
// 3. 任务规划看板卡片 (todo_write)
|
||||
// ==========================================
|
||||
interface TodoItem {
|
||||
export interface TodoItem {
|
||||
id: string;
|
||||
content: string;
|
||||
status: 'pending' | 'in_progress' | 'completed';
|
||||
@@ -453,7 +453,7 @@ export function TodoTaskCard({ todos }: TodoTaskCardProps) {
|
||||
{idx + 1}
|
||||
</div>
|
||||
);
|
||||
let rowClass = 'opacity-70';
|
||||
let rowClass: string;
|
||||
let titleClass = 'text-secondary font-medium';
|
||||
let badgeClass = 'bg-sunken text-tertiary';
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ interface SubAgentContainerProps {
|
||||
export function SubAgentContainer({
|
||||
status,
|
||||
children,
|
||||
summary: _summary,
|
||||
isStreaming: parentStreaming,
|
||||
isCollapsed,
|
||||
onToggle,
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
// 与 ObservationPanel.SearchView 的区别:
|
||||
// - SearchView:单目标 + 单源 + 单产品(精细化参数)
|
||||
// - UnifiedSearchPanel:多目标 + 多源(广覆盖发现)
|
||||
import { useState, useMemo, useCallback, useEffect } from 'react';
|
||||
import { useState, useMemo, useCallback } from 'react';
|
||||
import {
|
||||
Globe,
|
||||
Search,
|
||||
@@ -64,20 +64,20 @@ export function UnifiedSearchPanel({
|
||||
downloadUnifiedSelected,
|
||||
downloading,
|
||||
}: UnifiedSearchPanelProps) {
|
||||
// ── 统一检索的下载结果 ──
|
||||
const [downloadResults, setDownloadResults] = useState<
|
||||
ObservationBatchResult[] | null
|
||||
>(null);
|
||||
|
||||
// 当重新检索结果变化时,重置下载结果
|
||||
useEffect(() => {
|
||||
setDownloadResults(null);
|
||||
}, [unifiedResult]);
|
||||
// ── 统一检索的下载结果(缓存归属哪次检索;重新检索后自动失效)──
|
||||
const [downloadCache, setDownloadCache] = useState<{
|
||||
result: UnifiedSearchResult | null;
|
||||
results: ObservationBatchResult[];
|
||||
} | null>(null);
|
||||
const downloadResults =
|
||||
downloadCache && downloadCache.result === unifiedResult
|
||||
? downloadCache.results
|
||||
: null;
|
||||
|
||||
const handleDownload = useCallback(async () => {
|
||||
const r = await downloadUnifiedSelected();
|
||||
setDownloadResults(r);
|
||||
}, [downloadUnifiedSelected]);
|
||||
setDownloadCache({ result: unifiedResult, results: r });
|
||||
}, [downloadUnifiedSelected, unifiedResult]);
|
||||
|
||||
// ── 输入模式 ──
|
||||
const [inputMode, setInputMode] = useState<InputMode>('coordinates');
|
||||
@@ -206,11 +206,10 @@ export function UnifiedSearchPanel({
|
||||
);
|
||||
|
||||
// 第一次加载完能力后默认选择最新的子版本以及所有发布版本
|
||||
useEffect(() => {
|
||||
if (selectedSources === null && defaultSelectedKeys.length > 0) {
|
||||
setSelectedSources(new Set(defaultSelectedKeys));
|
||||
}
|
||||
}, [defaultSelectedKeys, selectedSources]);
|
||||
// (渲染期一次性初始化,替代 effect 中的 setState,避免级联渲染)
|
||||
if (selectedSources === null && defaultSelectedKeys.length > 0) {
|
||||
setSelectedSources(new Set(defaultSelectedKeys));
|
||||
}
|
||||
|
||||
// 同步选中的有效 Set
|
||||
const effectiveSelected = useMemo(() => {
|
||||
@@ -473,94 +472,101 @@ export function UnifiedSearchPanel({
|
||||
// ── 渲染子孙层级节点 (Level 1, 2, 3) ──
|
||||
const renderNestedNode = useCallback(
|
||||
(node: SourceTreeNode) => {
|
||||
const isExpanded = expandedNodes.has(node.id);
|
||||
const hasChildren = node.children && node.children.length > 0;
|
||||
// 递归渲染定义为内部具名函数,避免 useCallback 闭包自引用
|
||||
const renderNode = (node: SourceTreeNode) => {
|
||||
const isExpanded = expandedNodes.has(node.id);
|
||||
const hasChildren = node.children && node.children.length > 0;
|
||||
|
||||
const allChecked = node.leafKeys.every((key) =>
|
||||
effectiveSelected.has(key)
|
||||
);
|
||||
const someChecked = node.leafKeys.some((key) =>
|
||||
effectiveSelected.has(key)
|
||||
);
|
||||
const isIndeterminate = someChecked && !allChecked;
|
||||
const allChecked = node.leafKeys.every((key) =>
|
||||
effectiveSelected.has(key)
|
||||
);
|
||||
const someChecked = node.leafKeys.some((key) =>
|
||||
effectiveSelected.has(key)
|
||||
);
|
||||
const isIndeterminate = someChecked && !allChecked;
|
||||
|
||||
const handleCheckboxChange = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
setSelectedSources((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (allChecked) {
|
||||
node.leafKeys.forEach((k) => next.delete(k));
|
||||
} else {
|
||||
node.leafKeys.forEach((k) => next.add(k));
|
||||
const handleCheckboxChange = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
setSelectedSources((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (allChecked) {
|
||||
node.leafKeys.forEach((k) => next.delete(k));
|
||||
} else {
|
||||
node.leafKeys.forEach((k) => next.add(k));
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const handleNodeClick = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
if (hasChildren) {
|
||||
toggleExpand(node.id);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
const handleNodeClick = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
if (hasChildren) {
|
||||
toggleExpand(node.id);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div key={node.id} className="flex flex-col select-none">
|
||||
<div
|
||||
onClick={handleNodeClick}
|
||||
className="flex items-center justify-between py-1 px-1.5 rounded hover:bg-sunken/45 transition-all cursor-pointer"
|
||||
style={{ paddingLeft: `${(node.level - 1) * 12}px` }}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{/* 子折叠箭头 */}
|
||||
{hasChildren ? (
|
||||
<span
|
||||
className={`w-3.5 h-3.5 flex items-center justify-center text-[7px] text-tertiary transition-transform duration-200 ${
|
||||
isExpanded ? 'rotate-90 text-blueprint' : ''
|
||||
}`}
|
||||
>
|
||||
▶
|
||||
</span>
|
||||
) : (
|
||||
<span className="w-3.5" />
|
||||
)}
|
||||
|
||||
return (
|
||||
<div key={node.id} className="flex flex-col select-none">
|
||||
<div
|
||||
onClick={handleNodeClick}
|
||||
className="flex items-center justify-between py-1 px-1.5 rounded hover:bg-sunken/45 transition-all cursor-pointer"
|
||||
style={{ paddingLeft: `${(node.level - 1) * 12}px` }}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{/* 子折叠箭头 */}
|
||||
{hasChildren ? (
|
||||
<span
|
||||
className={`w-3.5 h-3.5 flex items-center justify-center text-[7px] text-tertiary transition-transform duration-200 ${
|
||||
isExpanded ? 'rotate-90 text-blueprint' : ''
|
||||
{/* 精美自定义复选框 */}
|
||||
<div
|
||||
onClick={handleCheckboxChange}
|
||||
className={`w-3.5 h-3.5 rounded border flex items-center justify-center transition-all cursor-pointer ${
|
||||
allChecked
|
||||
? 'bg-blueprint border-blueprint text-white'
|
||||
: isIndeterminate
|
||||
? 'bg-blueprint/25 border-blueprint text-blueprint'
|
||||
: 'border-default hover:border-secondary bg-surface'
|
||||
}`}
|
||||
>
|
||||
▶
|
||||
{allChecked && (
|
||||
<svg
|
||||
className="w-2.5 h-2.5 fill-current"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path d="M0 11l2-2 5 5L18 3l2 2L7 18z" />
|
||||
</svg>
|
||||
)}
|
||||
{isIndeterminate && (
|
||||
<div className="w-1.5 h-0.5 bg-blueprint rounded-sm" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 子节点标签 */}
|
||||
<span className="text-[11px] font-bold text-tertiary hover:text-secondary transition-colors">
|
||||
{node.label || '默认'}
|
||||
</span>
|
||||
) : (
|
||||
<span className="w-3.5" />
|
||||
)}
|
||||
|
||||
{/* 精美自定义复选框 */}
|
||||
<div
|
||||
onClick={handleCheckboxChange}
|
||||
className={`w-3.5 h-3.5 rounded border flex items-center justify-center transition-all cursor-pointer ${
|
||||
allChecked
|
||||
? 'bg-blueprint border-blueprint text-white'
|
||||
: isIndeterminate
|
||||
? 'bg-blueprint/25 border-blueprint text-blueprint'
|
||||
: 'border-default hover:border-secondary bg-surface'
|
||||
}`}
|
||||
>
|
||||
{allChecked && (
|
||||
<svg className="w-2.5 h-2.5 fill-current" viewBox="0 0 20 20">
|
||||
<path d="M0 11l2-2 5 5L18 3l2 2L7 18z" />
|
||||
</svg>
|
||||
)}
|
||||
{isIndeterminate && (
|
||||
<div className="w-1.5 h-0.5 bg-blueprint rounded-sm" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 子节点标签 */}
|
||||
<span className="text-[11px] font-bold text-tertiary hover:text-secondary transition-colors">
|
||||
{node.label || '默认'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 虚线连接子孙 */}
|
||||
{hasChildren && isExpanded && (
|
||||
<div className="flex flex-col gap-1 mt-1 border-l border-dashed border-subtle/50 ml-2.5 pl-1 animate-in fade-in duration-100">
|
||||
{node.children!.map((child) => renderNode(child))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 虚线连接子孙 */}
|
||||
{hasChildren && isExpanded && (
|
||||
<div className="flex flex-col gap-1 mt-1 border-l border-dashed border-subtle/50 ml-2.5 pl-1 animate-in fade-in duration-100">
|
||||
{node.children!.map((child) => renderNestedNode(child))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
);
|
||||
};
|
||||
return renderNode(node);
|
||||
},
|
||||
[expandedNodes, effectiveSelected, toggleExpand]
|
||||
);
|
||||
|
||||
@@ -63,6 +63,21 @@ export const SOURCE_THEME: Record<
|
||||
label: 'TESS',
|
||||
dot: 'bg-violet-500',
|
||||
},
|
||||
jwst: {
|
||||
badge: 'bg-pink-100 text-pink-700 dark:bg-pink-500/15 dark:text-pink-300',
|
||||
label: 'JWST',
|
||||
dot: 'bg-pink-500',
|
||||
},
|
||||
xmm: {
|
||||
badge: 'bg-teal-100 text-teal-700 dark:bg-teal-500/15 dark:text-teal-300',
|
||||
label: 'XMM-Newton',
|
||||
dot: 'bg-teal-500',
|
||||
},
|
||||
chandra: {
|
||||
badge: 'bg-red-100 text-red-700 dark:bg-red-500/15 dark:text-red-300',
|
||||
label: 'Chandra',
|
||||
dot: 'bg-red-500',
|
||||
},
|
||||
};
|
||||
|
||||
// ── 产品类型中文标签(product 轴)──
|
||||
|
||||
@@ -813,6 +813,9 @@ function LibraryView({
|
||||
{ value: 'gaia', label: 'Gaia' },
|
||||
{ value: 'sdss', label: 'SDSS' },
|
||||
{ value: 'desi', label: 'DESI' },
|
||||
{ value: 'jwst', label: 'JWST' },
|
||||
{ value: 'xmm', label: 'XMM-Newton' },
|
||||
{ value: 'chandra', label: 'Chandra' },
|
||||
]}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
Reference in New Issue
Block a user