payload = new LinkedHashMap<>();
+ payload.put("statusKey", "agent-cancelled");
+ payload.put("status", "cancelled");
+ payload.put("label", "已取消");
+ payload.put("message", reason);
+ sendEnvelope(chatSseEmitter, ChatDomain.BUSINESS, ChatType.STATUS, payload);
+ sendDone(chatSseEmitter);
+ }
+
+ /**
+ * 保存 Agent 取消前已经生成的 assistant 内容。
+ *
+ * 用户主动中止或连接断开时,运行不会进入正常 COMPLETED 分支,但前端历史恢复仍需要看到
+ * 中止前已经流出的模型输出。没有正文、reasoning 或工具链内容时不写空 assistant 消息。
+ *
+ * @param context 聊天运行上下文
+ * @param answer 已累计的 assistant 正文
+ * @param assistantAccumulator assistant 结构化累计器
+ * @param reason 取消原因
+ */
+ private void recordPartialAssistantIfPresent(ChatRuntimeContext context,
+ StringBuilder answer,
+ ChatAssistantAccumulator assistantAccumulator,
+ String reason) {
+ String partialAnswer = answer == null ? "" : answer.toString();
+ if (partialAnswer.isBlank() && !hasAssistantPayload(assistantAccumulator)) {
+ return;
+ }
+ chatRuntimeManager.recordAssistantCompleted(context,
+ buildAssistantRuntimeMessage(context, partialAnswer, assistantAccumulator, List.of()));
+ LOG.info("Agent partial answer persisted after cancellation, sessionId={}, answerLength={}, reason={}",
+ context == null ? null : context.getSessionId(), partialAnswer.length(), reason);
+ }
+
+ /**
+ * 判断 assistant 累计器中是否存在可恢复的展示内容。
+ *
+ * @param assistantAccumulator assistant 结构化累计器
+ * @return true 表示存在正文、思考过程或工具链内容
+ */
+ private boolean hasAssistantPayload(ChatAssistantAccumulator assistantAccumulator) {
+ if (assistantAccumulator == null) {
+ return false;
+ }
+ Map payload = assistantAccumulator.buildPayload("");
+ if (payload == null || payload.isEmpty()) {
+ return false;
+ }
+ return payload.values().stream().anyMatch(value -> {
+ if (value instanceof List> list) {
+ return !list.isEmpty();
+ }
+ if (value instanceof Map, ?> map) {
+ return !map.isEmpty();
+ }
+ return value != null && !String.valueOf(value).isBlank();
+ });
+ }
+
+ private ChatRuntimeContext buildChatRuntimeContext(Agent agent, BigInteger sessionId, String prompt, LoginAccount account) {
+ return buildChatRuntimeContext(agent, sessionId, prompt, account, ASSISTANT_CODE);
+ }
+
+ /**
+ * 为正式 Agent 聊天设置会话默认标题。
+ *
+ * 正式聊天只在新会话首轮使用用户输入作为默认标题,后续轮次不再自动覆盖,避免用户已重命名的标题被后续消息改写。
+ *
+ * @param context 聊天运行上下文
+ * @param prompt 当前用户输入
+ * @param existingSession 已存在的会话摘要
+ */
+ private void applyFormalSessionTitle(ChatRuntimeContext context, String prompt, ChatSessionSummary existingSession) {
+ if (context == null) {
+ return;
+ }
+ if (existingSession != null) {
+ context.setSessionTitle(null);
+ return;
+ }
+ context.setSessionTitle(toSessionTitle(prompt));
+ }
+
+ private ChatRuntimeContext buildChatRuntimeContext(Agent agent,
+ BigInteger sessionId,
+ String prompt,
+ LoginAccount account,
+ String assistantCode) {
+ ChatRuntimeContext context = new ChatRuntimeContext();
+ context.setChannel(ChatChannel.ADMIN);
+ context.setSessionId(sessionId);
+ context.setTenantId(account.getTenantId());
+ context.setDeptId(account.getDeptId());
+ context.setUserId(account.getId());
+ context.setUserAccount(account.getLoginName());
+ context.setUserName(account.getNickname() == null || account.getNickname().isBlank() ? account.getLoginName() : account.getNickname());
+ context.setAssistantId(agent.getId());
+ context.setAssistantCode(assistantCode);
+ context.setAssistantName(agent.getName());
+ context.setSessionTitle(toSessionTitle(prompt));
+ return context;
+ }
+
+ /**
+ * 将用户输入裁剪为会话标题。
+ *
+ * @param prompt 用户输入内容
+ * @return 最长 200 字符的会话标题
+ */
+ private String toSessionTitle(String prompt) {
+ if (prompt == null) {
+ return null;
+ }
+ return prompt.length() > 200 ? prompt.substring(0, 200) : prompt;
+ }
+
+ private AgentRuntimeContext buildAgentRuntimeContext(ChatRuntimeContext chatContext, String traceId, String sessionId) {
+ AgentRuntimeContext context = new AgentRuntimeContext();
+ context.setTenantId(chatContext.getTenantId() == null ? null : chatContext.getTenantId().toString());
+ context.setUserId(chatContext.getUserId() == null ? null : chatContext.getUserId().toString());
+ context.setUserName(chatContext.getUserName());
+ context.setSessionId(sessionId);
+ context.setTraceId(traceId);
+ return context;
+ }
+
+ private ChatRuntimeMessage buildUserRuntimeMessage(ChatRuntimeContext context, String prompt) {
+ ChatRuntimeMessage message = new ChatRuntimeMessage();
+ message.setRole("user");
+ message.setContentType("TEXT");
+ message.setContentText(prompt);
+ message.setCreatedAt(new Date());
+ message.setSenderId(context.getUserId());
+ message.setSenderName(context.getUserName());
+ return message;
+ }
+
+ private ChatRuntimeMessage buildAssistantRuntimeMessage(ChatRuntimeContext context, String content) {
+ return buildAssistantRuntimeMessage(context, content, new ChatAssistantAccumulator(), List.of());
+ }
+
+ private ChatRuntimeMessage buildAssistantRuntimeMessage(ChatRuntimeContext context,
+ String content,
+ ChatAssistantAccumulator assistantAccumulator,
+ List