feat: 支持智能体文档附件与轻量读取

- 建立文档上传、异步读取、对象存储、补偿与聊天绑定闭环

- 按智能体 20K 上下文预算选择文档片段并保留稳定引用

- 统一聊天文件卡片、类型图标、草稿恢复与可靠下载
This commit is contained in:
2026-07-29 01:32:19 +08:00
parent fceedd02cd
commit d45c67a317
72 changed files with 5876 additions and 237 deletions

View File

@@ -4,6 +4,7 @@ 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.ContentDisposition;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
@@ -16,6 +17,7 @@ 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;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import tech.easyflow.admin.controller.ai.support.AiResourceCreatorNameSupport;
import tech.easyflow.agent.entity.Agent;
import tech.easyflow.agent.entity.AgentKnowledgeBinding;
@@ -27,6 +29,9 @@ 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.document.AgentDocumentResource;
import tech.easyflow.agent.runtime.document.AgentDocumentService;
import tech.easyflow.agent.runtime.document.AgentDocumentUploadView;
import tech.easyflow.agent.runtime.media.AgentMediaService;
import tech.easyflow.agent.runtime.media.AgentMediaUploadView;
import com.easyagents.agent.runtime.media.AgentMediaResource;
@@ -50,6 +55,7 @@ import tech.easyflow.system.service.ResourceAccessService;
import javax.annotation.Resource;
import java.io.Serializable;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
@@ -82,6 +88,8 @@ public class AgentController extends BaseCurdController<AgentService, Agent> {
@Resource
private AgentMediaService agentMediaService;
@Resource
private AgentDocumentService agentDocumentService;
@Resource
private AgentComposerDraftService agentComposerDraftService;
/**
@@ -226,6 +234,91 @@ public class AgentController extends BaseCurdController<AgentService, Agent> {
.body(resource.bytes());
}
/**
* 上传一份 Agent 聊天文档并异步触发轻量读取。
*
* @param file 文档文件
* @param mode 聊天模式
* @param agentId Agent ID
* @param sessionId 会话 ID
* @param uploadId 客户端生成的幂等上传 ID
* @return 上传与读取状态
*/
@PostMapping(value = "/media/document/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public Result<AgentDocumentUploadView> uploadDocument(@RequestParam("file") MultipartFile file,
@RequestParam("mode") String mode,
@RequestParam("agentId") String agentId,
@RequestParam("sessionId") String sessionId,
@RequestParam(value = "uploadId", required = false)
String uploadId) {
return Result.ok(agentDocumentService.upload(
file, mode, agentId, sessionId, uploadId, SaTokenUtil.getLoginAccount()));
}
/**
* 查询当前账号一个上传文档的读取状态。
*
* @param uploadId 上传 ID
* @return 最新状态
*/
@GetMapping("/media/document/status")
public Result<AgentDocumentUploadView> documentStatus(@RequestParam("uploadId") String uploadId) {
return Result.ok(agentDocumentService.status(uploadId, SaTokenUtil.getLoginAccount()));
}
/**
* 重试一次明确失败的文档读取。
*
* @param uploadId 上传 ID
* @return 重试后的状态
*/
@PostMapping("/media/document/retry")
public Result<AgentDocumentUploadView> retryDocument(
@JsonBody(value = "uploadId", required = true) String uploadId) {
return Result.ok(agentDocumentService.retry(uploadId, SaTokenUtil.getLoginAccount()));
}
/**
* 删除当前账号尚未发送的临时文档。
*
* @param uploadId 上传 ID
* @return 操作结果
*/
@PostMapping("/media/document/delete")
public Result<Void> deleteDocument(
@JsonBody(value = "uploadId", required = true) String uploadId) {
agentDocumentService.deleteUpload(uploadId, SaTokenUtil.getLoginAccount());
return Result.ok();
}
/**
* 通过鉴权代理流式下载 Agent 私有聊天文档。
*
* @param reference 稳定文档引用
* @return 文档流
*/
@GetMapping("/media/document/content")
@LogReporterDisabled
public ResponseEntity<StreamingResponseBody> documentContent(
@RequestParam("reference") String reference) {
AgentDocumentResource resource = agentDocumentService.load(
reference, SaTokenUtil.getLoginAccount());
StreamingResponseBody body = output -> {
try (var input = resource.inputStream()) {
input.transferTo(output);
}
};
ContentDisposition disposition = ContentDisposition.attachment()
.filename(resource.name(), StandardCharsets.UTF_8)
.build();
return ResponseEntity.ok()
.header(HttpHeaders.CACHE_CONTROL, "private, no-store")
.header(HttpHeaders.CONTENT_DISPOSITION, disposition.toString())
.contentType(MediaType.parseMediaType(resource.mimeType()))
.contentLength(resource.size())
.body(body);
}
/**
* 为输入框预分配稳定会话 ID。
*
@@ -272,7 +365,8 @@ public class AgentController extends BaseCurdController<AgentService, Agent> {
* @param agentId Agent ID
* @param sessionId 会话 ID
* @param imageUploadIds 调用方仍持有的上传 ID
* @param deleteUploads 是否同时删除临时图片
* @param documentUploadIds 调用方仍持有的文档上传 ID
* @param deleteUploads 是否同时删除临时附件
* @return 操作结果
*/
@PostMapping("/composer/draft/delete")
@@ -280,8 +374,9 @@ public class AgentController extends BaseCurdController<AgentService, Agent> {
@JsonBody(value = "agentId", required = true) String agentId,
@JsonBody(value = "sessionId", required = true) String sessionId,
@JsonBody(value = "imageUploadIds") List<String> imageUploadIds,
@JsonBody(value = "documentUploadIds") List<String> documentUploadIds,
@JsonBody(value = "deleteUploads") Boolean deleteUploads) {
agentComposerDraftService.delete(mode, agentId, sessionId, imageUploadIds,
agentComposerDraftService.delete(mode, agentId, sessionId, imageUploadIds, documentUploadIds,
!Boolean.FALSE.equals(deleteUploads), SaTokenUtil.getLoginAccount());
return Result.ok();
}

View File

@@ -7,6 +7,7 @@ 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.document.AgentDocumentService;
import tech.easyflow.agent.runtime.media.AgentMediaService;
import tech.easyflow.agent.service.AgentService;
import tech.easyflow.ai.entity.DocumentCollection;
@@ -24,6 +25,7 @@ import tech.easyflow.system.enums.CategoryResourceType;
import tech.easyflow.system.enums.ResourceAction;
import tech.easyflow.system.service.ResourceAccessService;
import javax.annotation.Resource;
import java.math.BigInteger;
import java.util.*;
@@ -44,6 +46,8 @@ public class AgentSessionService {
private final AgentMediaService agentMediaService;
private final AgentComposerDraftService agentComposerDraftService;
private final ChatJsonSupport chatJsonSupport;
@Resource
private AgentDocumentService agentDocumentService;
/**
* 创建 Agent 管理端会话服务。
@@ -201,6 +205,7 @@ public class AgentSessionService {
// 上一次删除可能已写入删除标记但媒体清理失败,重试时继续清理当前用户目录。
deleteComposerDraft(summary, account, sessionId);
agentMediaService.deleteFormalSession(sessionId.toString(), account);
deleteFormalDocuments(sessionId, account);
return;
}
requireUserAgentSession(account, summary);
@@ -208,6 +213,19 @@ public class AgentSessionService {
chatSessionCommandService.deleteSession(sessionId, account.getId(), account.getId());
deleteComposerDraft(summary, account, sessionId);
agentMediaService.deleteFormalSession(sessionId.toString(), account);
deleteFormalDocuments(sessionId, account);
}
/**
* 幂等清理正式会话绑定的文档对象与快照。
*
* @param sessionId 会话 ID
* @param account 当前账号
*/
private void deleteFormalDocuments(BigInteger sessionId, LoginAccount account) {
if (agentDocumentService != null) {
agentDocumentService.deleteFormalSession(sessionId.toString(), account);
}
}
/**