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
138 lines
4.0 KiB
TypeScript
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 });
|
|
}
|
|
}
|