Files
AIclinicalresearch/backend/src/modules/asl/controllers/researchController.ts
HaHafeng 5c5fec52c1 fix(aia,ssa,asl,infra): harden SSE transport and stabilize attachment context
Deliver SSE protocol hardening for SAE/HTTP2 paths, add graceful shutdown health behavior, and improve SSA retry UX for transient stream failures. For AIA, persist attachment extraction results in database with cache read-through fallback, plus production cache safety guard to prevent memory-cache drift in multi-instance deployments; also restore ASL SR page scrolling behavior.

Made-with: Cursor
2026-03-09 18:45:12 +08:00

138 lines
4.0 KiB
TypeScript

/**
* 智能文献检索 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',
'Access-Control-Allow-Origin': '*',
'X-Accel-Buffering': 'no',
});
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 });
}
}