feat(asl): Add DeepSearch smart literature retrieval MVP

Features:
- Integrate unifuncs DeepSearch API (OpenAI compatible protocol)
- SSE real-time streaming for AI thinking process display
- Natural language input, auto-generate PubMed search strategy
- Extract and display PubMed literature links
- Database storage for task records (asl_research_tasks)

Backend:
- researchService.ts - Core business logic with SSE streaming
- researchController.ts - SSE stream endpoint
- researchWorker.ts - Async task worker (backup mode)
- schema.prisma - AslResearchTask model

Frontend:
- ResearchSearch.tsx - Search page with unified content stream
- ResearchSearch.css - Styling (unifuncs-inspired simple design)
- ASLLayout.tsx - Enable menu item
- api/index.ts - Add research API functions

API Endpoints:
- POST /api/v1/asl/research/stream - SSE streaming search
- POST /api/v1/asl/research/tasks - Async task creation
- GET /api/v1/asl/research/tasks/:taskId/status - Task status

Documentation:
- Development record for DeepSearch integration
- Update ASL module status (v1.5)
- Update system status (v3.7)

Known limitations:
- SSE mode, task interrupts when leaving page
- Cost ~0.3 RMB per search (unifuncs API)
This commit is contained in:
2026-01-18 19:15:55 +08:00
parent 57fdc6ef00
commit 1ece9a4ae8
20 changed files with 2052 additions and 16 deletions

View File

@@ -0,0 +1,137 @@
/**
* 智能文献检索 Controller
*
* SSE 流式 + PubMed 链接提取
*/
import { FastifyRequest, FastifyReply } from 'fastify';
import { researchService } from '../services/researchService.js';
import { logger } from '../../../common/logging/index.js';
interface SearchBody {
projectId: string;
query: string;
}
/**
* POST /api/v1/asl/research/stream
* SSE 实时流式检索
*/
export async function streamSearch(
request: FastifyRequest<{ Body: SearchBody }>,
reply: FastifyReply
) {
const { projectId, query } = request.body;
const userId = request.user?.userId;
if (!userId) {
return reply.code(401).send({ success: false, error: '用户未认证' });
}
if (!query?.trim()) {
return reply.code(400).send({ success: false, error: '请输入检索问题' });
}
logger.info('[ResearchController] Starting SSE stream', { userId, queryLength: query.length });
// 设置 SSE 响应头
reply.raw.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Access-Control-Allow-Origin': '*',
});
try {
const task = await researchService.createTaskRecord(projectId, userId, query);
reply.raw.write(`data: ${JSON.stringify({ type: 'task_created', taskId: task.id })}\n\n`);
await researchService.executeStreamSearch(
task.id,
query,
// 思考过程(统一追加)
(reasoning: string) => {
reply.raw.write(`data: ${JSON.stringify({ type: 'reasoning', content: reasoning })}\n\n`);
},
// 结果内容(统一追加)
(content: string) => {
reply.raw.write(`data: ${JSON.stringify({ type: 'content', content })}\n\n`);
},
// 完成(返回链接列表)
(result: { links: string[] }) => {
reply.raw.write(`data: ${JSON.stringify({ type: 'completed', links: result.links })}\n\n`);
reply.raw.end();
},
// 错误
(error: string) => {
reply.raw.write(`data: ${JSON.stringify({ type: 'error', error })}\n\n`);
reply.raw.end();
}
);
} catch (error: any) {
logger.error('[ResearchController] Stream search failed', { error: error.message });
reply.raw.write(`data: ${JSON.stringify({ type: 'error', error: error.message })}\n\n`);
reply.raw.end();
}
}
/**
* POST /api/v1/asl/research/tasks
* 创建检索任务(异步模式)
*/
export async function createTask(
request: FastifyRequest<{ Body: SearchBody }>,
reply: FastifyReply
) {
try {
const { projectId, query } = request.body;
const userId = request.user?.userId;
if (!userId) {
return reply.code(401).send({ success: false, error: '用户未认证' });
}
if (!projectId) {
return reply.code(400).send({ success: false, error: '缺少 projectId' });
}
if (!query?.trim()) {
return reply.code(400).send({ success: false, error: '请输入检索问题' });
}
const task = await researchService.createTask({ projectId, userId, query: query.trim() });
return reply.send({ success: true, data: task });
} catch (error: any) {
logger.error('[ResearchController] Create task failed', { error: error.message });
return reply.code(500).send({ success: false, error: error.message });
}
}
/**
* GET /api/v1/asl/research/tasks/:taskId/status
* 获取任务状态
*/
export async function getTaskStatus(
request: FastifyRequest<{ Params: { taskId: string } }>,
reply: FastifyReply
) {
try {
const { taskId } = request.params;
if (!taskId) {
return reply.code(400).send({ success: false, error: '缺少 taskId' });
}
const status = await researchService.getTaskStatus(taskId);
if (!status) {
return reply.code(404).send({ success: false, error: '任务不存在' });
}
return reply.send({ success: true, data: status });
} catch (error: any) {
logger.error('[ResearchController] Get task status failed', { error: error.message });
return reply.code(500).send({ success: false, error: error.message });
}
}