feat: 完善智能体图片聊天与会话恢复

- 增加私有图片上传、绑定、历史回显与生命周期清理

- 支持输入草稿恢复、图片交互和模型图片能力约束

- 修复旧脏会话幂等删除与前端会话恢复
This commit is contained in:
2026-07-17 19:54:26 +08:00
parent 62d763199f
commit 1e6158be77
62 changed files with 5333 additions and 189 deletions

View File

@@ -4,10 +4,15 @@ import cn.dev33.satoken.annotation.SaCheckPermission;
import com.mybatisflex.core.paginate.Page;
import com.mybatisflex.core.query.QueryWrapper;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
@@ -19,6 +24,12 @@ import tech.easyflow.agent.publish.AgentPublishAppService;
import tech.easyflow.agent.runtime.AgentChatRequest;
import tech.easyflow.agent.runtime.AgentDraftChatRequest;
import tech.easyflow.agent.runtime.AgentRunService;
import tech.easyflow.agent.runtime.composer.AgentComposerDraft;
import tech.easyflow.agent.runtime.composer.AgentComposerDraftService;
import tech.easyflow.agent.runtime.composer.AgentComposerSession;
import tech.easyflow.agent.runtime.media.AgentMediaService;
import tech.easyflow.agent.runtime.media.AgentMediaUploadView;
import com.easyagents.agent.runtime.media.AgentMediaResource;
import tech.easyflow.agent.service.AgentApprovalStateService;
import tech.easyflow.agent.service.AgentKnowledgeBindingService;
import tech.easyflow.agent.service.AgentService;
@@ -28,6 +39,8 @@ import tech.easyflow.approval.entity.vo.ApprovalActionResult;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.web.controller.BaseCurdController;
import tech.easyflow.common.web.jsonbody.JsonBody;
import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.log.annotation.LogReporterDisabled;
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
import tech.easyflow.system.enums.CategoryResourceType;
import tech.easyflow.system.enums.ResourceAction;
@@ -66,6 +79,10 @@ public class AgentController extends BaseCurdController<AgentService, Agent> {
private AgentApprovalStateService agentApprovalStateService;
@Resource
private AiResourceCreatorNameSupport aiResourceCreatorNameSupport;
@Resource
private AgentMediaService agentMediaService;
@Resource
private AgentComposerDraftService agentComposerDraftService;
/**
* 创建 Agent 控制器。
@@ -162,6 +179,113 @@ public class AgentController extends BaseCurdController<AgentService, Agent> {
return agentRunService.chatDraft(request);
}
/**
* 上传一张 Agent 聊天临时图片。
*
* @param file 图片文件
* @param mode 聊天模式
* @param agentId Agent ID
* @param sessionId 会话 ID
* @return 上传结果
*/
@PostMapping(value = "/media/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public Result<AgentMediaUploadView> uploadMedia(@RequestParam("file") MultipartFile file,
@RequestParam("mode") String mode,
@RequestParam("agentId") String agentId,
@RequestParam("sessionId") String sessionId) {
return Result.ok(agentMediaService.upload(file, mode, agentId, sessionId, SaTokenUtil.getLoginAccount()));
}
/**
* 删除当前账号尚未发送的临时图片。
*
* @param uploadId 上传 ID
* @return 操作结果
*/
@PostMapping("/media/delete")
public Result<Void> deleteMedia(@JsonBody(value = "uploadId", required = true) String uploadId) {
agentMediaService.deleteUpload(uploadId, SaTokenUtil.getLoginAccount());
return Result.ok();
}
/**
* 通过鉴权代理读取 Agent 私有聊天图片。
*
* @param reference 稳定图片引用
* @return 图片响应
*/
@GetMapping("/media/content")
@LogReporterDisabled
public ResponseEntity<byte[]> mediaContent(@RequestParam("reference") String reference) {
AgentMediaResource resource = agentMediaService.load(reference, SaTokenUtil.getLoginAccount());
return ResponseEntity.ok()
.header(HttpHeaders.CACHE_CONTROL, "private, no-store")
.header(HttpHeaders.CONTENT_DISPOSITION, "inline")
.contentType(MediaType.parseMediaType(resource.mimeType()))
.contentLength(resource.bytes().length)
.body(resource.bytes());
}
/**
* 为输入框预分配稳定会话 ID。
*
* @param mode 聊天模式
* @return 会话信息
*/
@PostMapping("/composer/session")
public Result<AgentComposerSession> allocateComposerSession(
@JsonBody(value = "mode", required = true) String mode) {
return Result.ok(agentComposerDraftService.allocateSession(mode));
}
/**
* 保存 Agent 输入草稿。
*
* @param draft 输入草稿
* @return 保存后的草稿
*/
@PostMapping("/composer/draft/persist")
public Result<AgentComposerDraft> saveComposerDraft(@JsonBody AgentComposerDraft draft) {
return Result.ok(agentComposerDraftService.save(draft, SaTokenUtil.getLoginAccount()));
}
/**
* 获取当前会话或最近未发送会话的输入草稿。
*
* @param mode 聊天模式
* @param agentId Agent ID
* @param sessionId 会话 ID可为空
* @return 输入草稿
*/
@GetMapping("/composer/draft")
public Result<AgentComposerDraft> getComposerDraft(@RequestParam("mode") String mode,
@RequestParam("agentId") String agentId,
@RequestParam(value = "sessionId", required = false) String sessionId) {
return Result.ok(agentComposerDraftService.get(mode, agentId, sessionId, SaTokenUtil.getLoginAccount())
.orElse(null));
}
/**
* 删除已发送或主动清空的输入草稿。
*
* @param mode 聊天模式
* @param agentId Agent ID
* @param sessionId 会话 ID
* @param imageUploadIds 调用方仍持有的上传 ID
* @param deleteUploads 是否同时删除临时图片
* @return 操作结果
*/
@PostMapping("/composer/draft/delete")
public Result<Void> deleteComposerDraft(@JsonBody(value = "mode", required = true) String mode,
@JsonBody(value = "agentId", required = true) String agentId,
@JsonBody(value = "sessionId", required = true) String sessionId,
@JsonBody(value = "imageUploadIds") List<String> imageUploadIds,
@JsonBody(value = "deleteUploads") Boolean deleteUploads) {
agentComposerDraftService.delete(mode, agentId, sessionId, imageUploadIds,
!Boolean.FALSE.equals(deleteUploads), SaTokenUtil.getLoginAccount());
return Result.ok();
}
/**
* 清理 Agent 草稿试运行会话。
*

View File

@@ -6,6 +6,8 @@ import org.springframework.util.StringUtils;
import tech.easyflow.admin.dto.chatworkspace.*;
import tech.easyflow.agent.entity.Agent;
import tech.easyflow.agent.runtime.AgentRuntimeStateCleanupService;
import tech.easyflow.agent.runtime.composer.AgentComposerDraftService;
import tech.easyflow.agent.runtime.media.AgentMediaService;
import tech.easyflow.agent.service.AgentService;
import tech.easyflow.ai.entity.DocumentCollection;
import tech.easyflow.ai.enums.PublishStatus;
@@ -39,6 +41,8 @@ public class AgentSessionService {
private final DocumentCollectionService documentCollectionService;
private final ResourceAccessService resourceAccessService;
private final AgentRuntimeStateCleanupService agentRuntimeStateCleanupService;
private final AgentMediaService agentMediaService;
private final AgentComposerDraftService agentComposerDraftService;
private final ChatJsonSupport chatJsonSupport;
/**
@@ -50,6 +54,8 @@ public class AgentSessionService {
* @param documentCollectionService 知识库服务
* @param resourceAccessService 资源访问服务
* @param agentRuntimeStateCleanupService Agent 运行态清理服务
* @param agentMediaService Agent 媒体服务
* @param agentComposerDraftService Agent 输入草稿服务
* @param chatJsonSupport 聊天 JSON 工具
*/
public AgentSessionService(ChatSessionQueryService chatSessionQueryService,
@@ -58,6 +64,8 @@ public class AgentSessionService {
DocumentCollectionService documentCollectionService,
ResourceAccessService resourceAccessService,
AgentRuntimeStateCleanupService agentRuntimeStateCleanupService,
AgentMediaService agentMediaService,
AgentComposerDraftService agentComposerDraftService,
ChatJsonSupport chatJsonSupport) {
this.chatSessionQueryService = chatSessionQueryService;
this.chatSessionCommandService = chatSessionCommandService;
@@ -65,6 +73,8 @@ public class AgentSessionService {
this.documentCollectionService = documentCollectionService;
this.resourceAccessService = resourceAccessService;
this.agentRuntimeStateCleanupService = agentRuntimeStateCleanupService;
this.agentMediaService = agentMediaService;
this.agentComposerDraftService = agentComposerDraftService;
this.chatJsonSupport = chatJsonSupport;
}
@@ -186,21 +196,58 @@ public class AgentSessionService {
* @param sessionId 会话 ID
*/
public void deleteCurrentUserSession(LoginAccount account, BigInteger sessionId) {
requireUserAgentSession(account, sessionId);
ChatSessionSummary summary = chatSessionQueryService.getSessionSummary(sessionId);
if (summary == null || Integer.valueOf(1).equals(summary.getIsDeleted())) {
// 上一次删除可能已写入删除标记但媒体清理失败,重试时继续清理当前用户目录。
deleteComposerDraft(summary, account, sessionId);
agentMediaService.deleteFormalSession(sessionId.toString(), account);
return;
}
requireUserAgentSession(account, summary);
agentRuntimeStateCleanupService.clearChatSession(sessionId, account.getId());
chatSessionCommandService.deleteSession(sessionId, account.getId(), account.getId());
deleteComposerDraft(summary, account, sessionId);
agentMediaService.deleteFormalSession(sessionId.toString(), account);
}
/**
* 删除会话对应的未发送草稿和临时图片。
*
* @param summary 会话摘要
* @param account 当前登录账号
* @param sessionId 会话 ID
*/
private void deleteComposerDraft(ChatSessionSummary summary, LoginAccount account, BigInteger sessionId) {
if (summary == null || summary.getAssistantId() == null) {
return;
}
agentComposerDraftService.delete(AgentMediaService.MODE_FORMAL,
summary.getAssistantId().toString(), sessionId.toString(), account);
}
private ChatSessionSummary requireUserAgentSession(LoginAccount account, BigInteger sessionId) {
ChatSessionSummary summary = chatSessionQueryService.getSessionSummary(sessionId);
if (summary == null || Integer.valueOf(1).equals(summary.getIsDeleted())
|| !ASSISTANT_CODE.equals(summary.getAssistantCode())) {
if (summary == null || Integer.valueOf(1).equals(summary.getIsDeleted())) {
throw new BusinessException("Agent 会话不存在");
}
requireUserAgentSession(account, summary);
return summary;
}
/**
* 校验会话属于当前用户且类型为 Agent。
*
* @param account 当前登录账号
* @param summary 会话摘要
* @throws BusinessException 会话类型不匹配或不属于当前用户时抛出
*/
private void requireUserAgentSession(LoginAccount account, ChatSessionSummary summary) {
if (!ASSISTANT_CODE.equals(summary.getAssistantCode())) {
throw new BusinessException("Agent 会话不存在");
}
if (!Objects.equals(summary.getUserId(), account.getId())) {
throw new BusinessException("无权访问该 Agent 会话");
}
return summary;
}
private Map<BigInteger, AgentAvailability> resolveAgentAvailability(List<ChatSessionSummary> sessions) {