feat(asl): Complete Deep Research V2.0 core development

Backend:
- Add SSE streaming client (unifuncsSseClient) replacing async polling
- Add paragraph-based reasoning parser with mergeConsecutiveThinking
- Add requirement expansion service (DeepSeek-V3 PICOS+MeSH)
- Add Word export service with Pandoc, inline hyperlinks, reference link expansion
- Add deep research V2 worker with 2s log flush and Chinese source prompt
- Add 5 curated data sources config (PubMed/ClinicalTrials/Cochrane/CNKI/MedJournals)
- Add 4 API endpoints (generate-requirement/tasks/task-status/export-word)
- Update Prisma schema with 6 new V2.0 fields on AslResearchTask
- Add DB migration for V2.0 fields
- Simplify ASL_DEEP_RESEARCH_EXPANSION prompt (remove strategy section)

Frontend:
- Add waterfall-flow DeepResearchPage (phase 0-4 progressive reveal)
- Add LandingView, SetupPanel, StrategyConfirm, AgentTerminal, ResultsView
- Add react-markdown + remark-gfm for report rendering
- Add custom link component showing visible URLs after references
- Add useDeepResearchTask polling hook
- Add deep research TypeScript types

Tests:
- Add E2E test, smoke test, and Chinese data source test scripts

Docs:
- Update ASL module status (v2.0 - core features complete)
- Update system status (v6.1 - ASL V2.0 milestone)
- Update Unifuncs DeepSearch API guide (v2.0 - SSE mode + Chinese source results)
- Update module auth specification (test script guidelines)
- Update V2.0 development plan

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-02-23 13:21:52 +08:00
parent b06daecacd
commit 8f06d4f929
39 changed files with 5605 additions and 417 deletions

View File

@@ -0,0 +1,165 @@
/**
* Deep Research V2.0 — Agent Terminal
*
* 暗色终端风格展示 AI 思考与执行过程:
* - 每条日志展示完整思考内容
* - 自动过滤无意义内容(任务 ID 等)
* - 条件 auto-scroll
*/
import { useRef, useEffect, useCallback, useState } from 'react';
import { Spin, Typography, Alert } from 'antd';
import { LoadingOutlined } from '@ant-design/icons';
import type { DeepResearchTask, ExecutionLogEntry } from '../../types/deepResearch';
const { Text } = Typography;
interface AgentTerminalProps {
task: DeepResearchTask | null;
isRunning: boolean;
isFailed: boolean;
}
const LOG_COLORS: Record<string, string> = {
thinking: '#8b949e',
searching: '#58a6ff',
reading: '#d2a8ff',
analyzing: '#7ee787',
summary: '#ffa657',
info: '#79c0ff',
};
const LOG_PREFIXES: Record<string, string> = {
thinking: '🤔 思考',
searching: '🔍 搜索',
reading: '📖 阅读',
analyzing: '🧪 分析',
summary: '📊 总结',
info: ' 信息',
};
const AgentTerminal: React.FC<AgentTerminalProps> = ({ task, isRunning, isFailed }) => {
const containerRef = useRef<HTMLDivElement>(null);
const [userScrolled, setUserScrolled] = useState(false);
const prevLogCountRef = useRef(0);
const logs = (task?.executionLogs || []).filter(
(log: ExecutionLogEntry) => !log.text.includes('Unifuncs 任务 ID')
);
const handleScroll = useCallback(() => {
const el = containerRef.current;
if (!el) return;
const distanceFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight;
setUserScrolled(distanceFromBottom > 50);
}, []);
useEffect(() => {
if (!userScrolled && containerRef.current && logs.length > prevLogCountRef.current) {
containerRef.current.scrollTop = containerRef.current.scrollHeight;
}
prevLogCountRef.current = logs.length;
}, [logs.length, userScrolled]);
return (
<div>
<div className="flex items-center gap-3 mb-4">
{isRunning && <Spin indicator={<LoadingOutlined spin />} size="small" />}
<Text strong className="text-lg">
{isRunning ? 'Deep Research 执行中...' : isFailed ? '执行失败' : '执行完成'}
</Text>
</div>
{isFailed && task?.errorMessage && (
<Alert
type="error"
message="执行失败"
description={task.errorMessage}
className="mb-4"
showIcon
/>
)}
<div
ref={containerRef}
onScroll={handleScroll}
className="rounded-xl overflow-hidden"
style={{
background: '#0d1117',
color: '#c9d1d9',
fontFamily: "'JetBrains Mono', 'Fira Code', 'Consolas', monospace",
fontSize: 13,
lineHeight: 1.8,
padding: '16px 20px',
maxHeight: 500,
minHeight: 200,
overflowY: 'auto',
}}
>
{/* Header */}
<div className="flex items-center gap-2 mb-4 pb-3" style={{ borderBottom: '1px solid #21262d' }}>
<span style={{ width: 12, height: 12, borderRadius: '50%', background: '#ff5f56', display: 'inline-block' }} />
<span style={{ width: 12, height: 12, borderRadius: '50%', background: '#ffbd2e', display: 'inline-block' }} />
<span style={{ width: 12, height: 12, borderRadius: '50%', background: '#27c93f', display: 'inline-block' }} />
<span className="ml-3" style={{ color: '#8b949e' }}>Deep Research Agent</span>
</div>
{/* Log entries */}
{logs.map((log: ExecutionLogEntry, i: number) => (
<div key={i} className="mb-3 pb-2" style={{ borderBottom: '1px solid #161b22' }}>
<div className="flex items-center gap-2 mb-1">
<span style={{ color: LOG_COLORS[log.type] || '#c9d1d9', fontWeight: 600, fontSize: 12 }}>
{LOG_PREFIXES[log.type] || log.title}
</span>
<span style={{ color: '#484f58', fontSize: 11 }}>
{new Date(log.ts).toLocaleTimeString()}
</span>
</div>
<div style={{ color: '#c9d1d9', paddingLeft: 4, whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
{log.text}
</div>
</div>
))}
{/* Blinking cursor */}
{isRunning && (
<span className="inline-block mt-1" style={{
width: 8,
height: 16,
background: '#58a6ff',
animation: 'blink 1s step-end infinite',
}} />
)}
{logs.length === 0 && (
<div style={{ color: '#484f58' }}>...</div>
)}
</div>
{userScrolled && isRunning && (
<div className="text-center mt-2">
<Text
type="secondary"
className="text-xs cursor-pointer hover:text-blue-400 transition-colors"
onClick={() => {
setUserScrolled(false);
if (containerRef.current) {
containerRef.current.scrollTop = containerRef.current.scrollHeight;
}
}}
>
</Text>
</div>
)}
<style>{`
@keyframes blink {
50% { opacity: 0; }
}
`}</style>
</div>
);
};
export default AgentTerminal;