feat: 完善智能体图片聊天与会话恢复
- 增加私有图片上传、绑定、历史回显与生命周期清理 - 支持输入草稿恢复、图片交互和模型图片能力约束 - 修复旧脏会话幂等删除与前端会话恢复
This commit is contained in:
@@ -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 草稿试运行会话。
|
||||
*
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user