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,161 @@
/**
* Deep Research V2.0 主页面 — 瀑布流布局
*
* Phase 0: Landing全屏居中搜索
* Phase 1+: 配置 → 策略 → 执行 → 结果,依次累积展示
*/
import { useState, useCallback, useRef, useEffect } from 'react';
import { message } from 'antd';
import { aslApi } from '../api';
import { useDeepResearchTask } from '../hooks/useDeepResearchTask';
import LandingView from '../components/deep-research/LandingView';
import SetupPanel from '../components/deep-research/SetupPanel';
import StrategyConfirm from '../components/deep-research/StrategyConfirm';
import AgentTerminal from '../components/deep-research/AgentTerminal';
import ResultsView from '../components/deep-research/ResultsView';
import type { IntentSummary, GenerateRequirementResponse } from '../types/deepResearch';
type Phase = 0 | 1 | 2 | 3 | 4;
const DeepResearchPage = () => {
const [phase, setPhase] = useState<Phase>(0);
const [query, setQuery] = useState('');
const [taskId, setTaskId] = useState<string | null>(null);
const [generatedRequirement, setGeneratedRequirement] = useState('');
const [intentSummary, setIntentSummary] = useState<IntentSummary | null>(null);
const [generating, setGenerating] = useState(false);
const strategyRef = useRef<HTMLDivElement>(null);
const terminalRef = useRef<HTMLDivElement>(null);
const resultsRef = useRef<HTMLDivElement>(null);
const { task, isRunning, isCompleted, isFailed } = useDeepResearchTask({
taskId,
enabled: phase >= 3,
});
useEffect(() => {
if (isCompleted && phase === 3) {
setPhase(4);
setTimeout(() => resultsRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }), 150);
}
}, [isCompleted, phase]);
const scrollTo = (ref: React.RefObject<HTMLDivElement | null>) => {
setTimeout(() => ref.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }), 150);
};
const handleLandingSubmit = useCallback((q: string) => {
setQuery(q);
setPhase(1);
setGeneratedRequirement('');
setIntentSummary(null);
setTaskId(null);
}, []);
const handleSetupSubmit = useCallback(async (
originalQuery: string,
selectedSources: string[],
filters: { yearRange?: string; targetCount?: string }
) => {
setGenerating(true);
try {
const res = await aslApi.generateRequirement({
originalQuery,
targetSources: selectedSources,
filters,
});
const data = res.data as GenerateRequirementResponse;
setTaskId(data.taskId);
setGeneratedRequirement(data.generatedRequirement);
setIntentSummary(data.intentSummary);
setPhase(2);
scrollTo(strategyRef);
} catch (err: any) {
message.error(err.message || '需求扩写失败');
} finally {
setGenerating(false);
}
}, []);
const handleStrategyConfirm = useCallback(async (confirmedReq: string) => {
if (!taskId) return;
try {
await aslApi.executeDeepResearchTask(taskId, confirmedReq);
setPhase(3);
scrollTo(terminalRef);
message.success('Deep Research 已启动');
} catch (err: any) {
message.error(err.message || '启动失败');
}
}, [taskId]);
const handleNewSearch = useCallback(() => {
setPhase(0);
setQuery('');
setTaskId(null);
setGeneratedRequirement('');
setIntentSummary(null);
window.scrollTo({ top: 0, behavior: 'smooth' });
}, []);
if (phase === 0) {
return (
<div className="h-full overflow-auto bg-gray-50">
<LandingView onSubmit={handleLandingSubmit} />
</div>
);
}
return (
<div className="h-full overflow-auto bg-gray-50 pb-16">
<div className="max-w-5xl mx-auto px-6 pt-6">
{/* Section 1: Setup */}
<SetupPanel
initialQuery={query}
onSubmit={handleSetupSubmit}
onBack={() => setPhase(0)}
loading={generating}
collapsed={phase >= 2}
onExpand={phase === 2 ? () => setPhase(1) : undefined}
/>
{/* Section 2: Strategy */}
{phase >= 2 && (
<div ref={strategyRef} className="mt-6">
<StrategyConfirm
generatedRequirement={generatedRequirement}
intentSummary={intentSummary}
onConfirm={handleStrategyConfirm}
collapsed={phase >= 3}
/>
</div>
)}
{/* Section 3: Executing */}
{phase >= 3 && (
<div ref={terminalRef} className="mt-6">
<AgentTerminal
task={task}
isRunning={isRunning}
isFailed={isFailed}
/>
</div>
)}
{/* Section 4: Results */}
{phase >= 4 && task && (
<div ref={resultsRef} className="mt-6">
<ResultsView
task={task}
onNewSearch={handleNewSearch}
/>
</div>
)}
</div>
</div>
);
};
export default DeepResearchPage;