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:
137
backend/src/modules/asl/controllers/researchController.ts
Normal file
137
backend/src/modules/asl/controllers/researchController.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user