山东大学软件学院 项目实训 Shiyo个人博客 2026/5/29

发布时间:2026/6/10 1:48:11

山东大学软件学院 项目实训 Shiyo个人博客 2026/5/29 Dockerfile构建优化在上一次完成任务结果结构化展示和RNA结构可视化后项目中新增了VARNA相关依赖。由于VARNA使用的是本地jar包不是普通的远程Maven依赖所以在Docker镜像构建时需要进行额外处理。原本Dockerfile中是在复制源码后直接执行./mvnw -s .mvn/settings.xml -DskipTests package这样每次重新构建镜像时Maven依赖都可能重新下载构建时间比较长。本次修改后在Dockerfile中加入了BuildKit缓存RUN --mounttypecache,idmaven-cache,target/root/.m2 \ ./mvnw -s .mvn/settings.xml -DskipTests -B dependency:go-offline这样Maven本地仓库可以被缓存后续重新构建镜像时速度会快很多。同时因为项目中有本地VARNA jar包所以需要先将它安装到Maven本地仓库中之后再进行正常依赖下载和项目打包。运行阶段也将lib目录复制到容器中这样后端在容器中运行时也可以正常调用VARNA生成RNA结构图。chat接口支持四类小任务之前chat接口中对任务的判断主要依赖pythonTaskId。也就是说只有Python端返回了异步任务ID时Java后端才会将它保存为PlatformTask。但是后续测试时发现Python端有些任务不会生成异步任务ID而是直接在chat接口中返回结果。这种情况在实际使用中也需要保存否则前端只能看到AI回复无法通过统一任务结果接口查看结构化结果。所以本次对chat接口中的任务保存逻辑进行了调整。原本判断逻辑比较简单if (StringUtils.hasText(pythonTaskId)) { task createChatTask(...); }修改后增加了shouldPersistChatTask方法。只要能够从route_result中识别出任务类型就允许保存为任务。目前支持四种任务类型family motif modification structure这样即使没有pythonTaskId只要是这四类任务也可以作为小任务保存下来。大任务与小任务的区别现在chat中产生的任务可以分成两种大任务Python端返回pythonTaskId需要后续查询任务状态和任务结果 小任务Python端直接返回结果没有pythonTaskId大任务适合运行时间较长的分析任务例如需要异步执行的FASTA任务。小任务适合直接在chat中完成的分析例如某些结构分析或直接返回的预测结果。为了让两种任务都能使用统一的结果接口后端需要解决一个问题Redis缓存key原本依赖pythonTaskId但是小任务没有这个字段。所以本次增加了统一的任务key生成逻辑private String effectiveTaskKeyId(PlatformTask task) { if (task null) return null; if (StringUtils.hasText(task.getPythonTaskId())) return task.getPythonTaskId(); return String.valueOf(task.getId()); }处理方式为优先使用pythonTaskId 如果没有pythonTaskId则使用Java数据库中的taskId这样大任务和小任务都可以生成稳定的Redis key。小任务结果读取逻辑TaskService中原来的任务结果查询逻辑会先校验任务是否存在Python任务ID。但是对于小任务来说没有Python任务ID也是正常情况。所以新增了requireOwnedTaskForResult方法只校验任务是否存在以及是否属于当前用户不再强制要求pythonTaskId存在。在查询任务结果时增加判断if (!StringUtils.hasText(task.getPythonTaskId())) { return loadTaskResultPayload(task); }如果没有pythonTaskId就直接从本地保存的resultJson或者Redis缓存中读取结果。这样小任务也可以使用/api/task/{taskId}/result /api/task/{taskId}/result/overview /api/task/{taskId}/result/sequences/{sequenceId}这些统一接口进行查看。modification任务类型统一之前modification任务类型中存在一个不太统一的名字modification_result.txt这个名称更像文件名不适合作为任务类型。所以本次将它统一为modification修改位置主要包括TaskService TaskResultAdapterFactory ModificationResultAdapter这样任务类型就统一为family motif modification structure后续前端判断任务类型时也会更方便。截断过长的chat原始结果由于Python端返回的结果可能比较大如果完整保存到ChatMessage.rawResult中可能会超过数据库TEXT字段长度。所以本次增加了截断逻辑这样既可以保留大部分调试信息也可以避免字段过长导致写入数据库失败。删除无用文档之前在实现S3文件上传时临时记录了一些配置说明和问题修复文档。包括S3PRESIGNER_FIX.md S3_SETUP_GUIDE.md现在S3相关逻辑已经基本稳定这些文档属于开发过程中的临时说明所以本次删除了它们避免仓库中保留过多无用文件。chat接口支持SSE协议在完成小任务保存后又对chat接口的返回方式进行了修改。之前chat接口是普通HTTP请求前端发送消息后只能等待最终结果返回。但是现在chat过程包含多个阶段路由判断 知识库检索 任务识别 任务创建 结果生成如果只返回最终结果前端无法展示中间过程用户等待时也不知道当前执行到哪一步。所以本次新增了SSE流式返回。在ChatRequest中新增字段JsonProperty(stream) private Boolean stream;当请求中传入{ stream: true }后端就会走流式chat逻辑。ChatController中的处理ChatController中对普通JSON请求和multipart请求都增加了stream判断。如果stream为true则返回chatService.chatStream(request)否则仍然使用原来的普通chat逻辑。这样原接口可以同时兼容普通chat 流式chat 普通文件chat 流式文件chat不需要新增额外的controller路径。PythonClient读取SSE在PythonClient中新增了chatStream方法请求Python端接口/api/chat/stream请求头中设置接收类型headers.setAccept(List.of(MediaType.TEXT_EVENT_STREAM));然后通过BufferedReader逐行读取Python端返回的数据。当前支持解析SSE格式中的event: data: id: retry:如果读到空行就说明一个事件结束将事件封装为StreamEvent交给ChatService处理。同时也兼容一些简单的inline事件格式例如stage ... final ... done ...ChatService中的SSE上下文SSE接口不能只负责转发Python端事件还需要完成原来chat接口中的前置逻辑。例如校验message 校验session_id 校验上传文件 绑定文件到会话 保存用户消息 构造Python请求payload所以本次将这部分逻辑整理成prepareChatContext。它会返回一个ChatStreamContext里面保存后续流式处理需要用到的内容private static final class ChatStreamContext { private final Long userId; private final ChatSession session; private final UserFile uploadedFile; private final ChatRequest request; private final MapString, Object extra; private final ChatMessage userMessage; private final MapString, Object payload; }这样后续收到final事件时可以继续使用这些上下文完成持久化。final事件处理SSE过程中普通stage事件只需要转发给前端。但是当收到final事件时说明Python端已经返回最终结果。这时后端需要完成和普通chat一样的收尾逻辑保存assistant message 提取task_result 创建PlatformTask 缓存任务结果 绑定上传文件 更新session时间 返回ChatResponse为了避免普通chat和SSE chat逻辑不一致本次将最终处理逻辑抽成了finalizeChat(...)普通chat在拿到Python完整返回后调用它。SSE chat在收到final事件后也调用它。这样可以保证两种接口最终保存的数据结构一致。添加WebSocket任务状态更新SSE主要解决的是chat过程中的实时进度问题。但是任务创建之后如果是异步大任务它的后续状态变化不一定还在当前SSE连接中返回。所以本次又新增了WebSocket用于任务状态更新。新增依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-websocket/artifactId /dependency新增WebSocket配置类Configuration EnableWebSocket public class TaskWebSocketConfig implements WebSocketConfigurer { Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(taskWebSocketHandler, /ws/tasks) .setAllowedOriginPatterns(*); } }WebSocket入口为/ws/tasksWebSocket鉴权WebSocket连接时需要携带token。支持的参数名包括accessToken token后端会解析JWT从中获取user_id。如果token无效则关闭连接session.close(CloseStatus.POLICY_VIOLATION.withReason(invalid token));如果连接成功则返回{ type: ws_ready }terminalId区分不同页面同一个用户可能同时打开多个页面。如果任务状态只按用户ID推送那么多个页面都会收到同一条任务状态消息前端处理会比较混乱。所以本次在ChatRequest中增加了terminal_id字段JsonProperty(terminal_id) JsonAlias({terminalId, client_id, clientId}) private String terminalId;WebSocket连接时也可以携带terminalId terminal_id后端保存WebSocket连接时使用两层MapMapLong, MapString, SetWebSocketSession sessions第一层是用户ID第二层是terminalId。这样任务状态可以尽量推送到发起任务的前端页面。TaskNotificationService新增TaskNotificationService用于管理WebSocket连接和推送任务状态。主要功能包括register注册连接 unregister移除连接 notifyTaskStatus构造并发送任务状态 send向指定用户和terminal推送消息推送的数据结构为{ type: task_status, taskId: 1, pythonTaskId: xxx, taskType: family, status: running, mode: large, time: 2026-06-01T16:20:00 }其中mode用于区分任务类型large有pythonTaskId的异步任务 smallchat中直接返回结果的小任务Python任务状态回调为了让Python端能够主动更新任务状态本次在TaskController中新增了回调接口/api/task/callback/status该接口支持GET和POST两种方式。请求参数包括pythonTaskId status后端收到后会根据pythonTaskId查询对应的PlatformTask更新状态字段。之后通过WebSocket将新的状态推送给前端。主要处理逻辑为task.setStatus(status); task.setUpdatedAt(LocalDateTime.now()); taskMapper.updateById(task);然后调用taskNotificationService.notifyTaskStatus(...)这样Python端任务状态变化后前端可以及时收到通知。SSE阶段信息过快问题在测试SSE时发现Python端返回stage事件比较快时前端展示效果并不好。有时候多个阶段几乎同时出现看起来不像是实时进度。所以本次对stage事件增加了一个随机延迟private void sleepRandomStageDelay() throws InterruptedException { Thread.sleep(ThreadLocalRandom.current().nextLong(1000L, 2001L)); }只对stage事件延迟处理if (stage.equalsIgnoreCase(eventName)) { sleepRandomStageDelay(); }final事件不会延迟避免影响最终结果返回。这样前端展示时每个阶段会有一定停顿用户可以更清楚地看到当前执行步骤。

相关新闻