Compare commits
7 Commits
6da90e2296
...
2689adfa40
| Author | SHA1 | Date | |
|---|---|---|---|
| 2689adfa40 | |||
| a41b50959e | |||
| 855e93ecbf | |||
| ae10383f17 | |||
| 31a755a8bc | |||
| 8cfe5400fe | |||
| 47655a728b |
@@ -36,5 +36,17 @@
|
||||
<groupId>tech.easyflow</groupId>
|
||||
<artifactId>easyflow-common-captcha</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>${junit.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<version>5.12.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
||||
@@ -16,6 +16,7 @@ import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
import tech.easyflow.admin.controller.ai.support.AiResourceCreatorNameSupport;
|
||||
import tech.easyflow.ai.easyagents.listener.PromptChoreChatStreamListener;
|
||||
import tech.easyflow.ai.entity.*;
|
||||
import tech.easyflow.ai.publish.BotPublishAppService;
|
||||
@@ -73,6 +74,8 @@ public class BotController extends BaseCurdController<BotService, Bot> {
|
||||
private BotPublishAppService botPublishAppService;
|
||||
@Resource
|
||||
private AiResourceApprovalStateService aiResourceApprovalStateService;
|
||||
@Resource
|
||||
private AiResourceCreatorNameSupport aiResourceCreatorNameSupport;
|
||||
|
||||
public BotController(BotService service, ModelService modelService, BotWorkflowService botWorkflowService,
|
||||
BotDocumentCollectionService botDocumentCollectionService, BotMessageService botMessageService) {
|
||||
@@ -305,6 +308,7 @@ public class BotController extends BaseCurdController<BotService, Bot> {
|
||||
applyCategoryPermission(queryWrapper);
|
||||
Page<Bot> result = super.queryPage(page, queryWrapper);
|
||||
aiResourceApprovalStateService.fillBotApprovalState(result.getRecords());
|
||||
aiResourceCreatorNameSupport.fillBotCreatorNames(result.getRecords());
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import org.springframework.web.bind.annotation.PostMapping;
|
||||
import tech.easyflow.ai.entity.Plugin;
|
||||
import tech.easyflow.ai.entity.BotPlugin;
|
||||
import tech.easyflow.ai.entity.PluginItem;
|
||||
import tech.easyflow.ai.enums.PluginType;
|
||||
import tech.easyflow.common.annotation.UsePermission;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.tree.Tree;
|
||||
@@ -58,7 +59,14 @@ public class BotPluginController extends BaseCurdController<BotPluginService, Bo
|
||||
List<BotPlugin> visibleList = new ArrayList<>();
|
||||
for (BotPlugin relation : botPlugins) {
|
||||
Plugin plugin = relation.getAiPlugin();
|
||||
if (plugin == null || pluginVisibilityService.canAccessPlugin(plugin.getCreatedBy(), plugin.getId())) {
|
||||
if (plugin == null) {
|
||||
visibleList.add(relation);
|
||||
continue;
|
||||
}
|
||||
if (PluginType.isWorkflow(plugin.getType())) {
|
||||
continue;
|
||||
}
|
||||
if (pluginVisibilityService.canAccessPlugin(plugin.getCreatedBy(), plugin.getId())) {
|
||||
visibleList.add(relation);
|
||||
}
|
||||
}
|
||||
@@ -73,7 +81,13 @@ public class BotPluginController extends BaseCurdController<BotPluginService, Bo
|
||||
List<Plugin> plugins = botPluginService.getList(botId);
|
||||
List<Plugin> visibleList = new ArrayList<>();
|
||||
for (Plugin plugin : plugins) {
|
||||
if (plugin == null || pluginVisibilityService.canAccessPlugin(plugin.getCreatedBy(), plugin.getId())) {
|
||||
if (plugin == null) {
|
||||
continue;
|
||||
}
|
||||
if (PluginType.isWorkflow(plugin.getType())) {
|
||||
continue;
|
||||
}
|
||||
if (pluginVisibilityService.canAccessPlugin(plugin.getCreatedBy(), plugin.getId())) {
|
||||
visibleList.add(plugin);
|
||||
}
|
||||
}
|
||||
@@ -105,6 +119,9 @@ public class BotPluginController extends BaseCurdController<BotPluginService, Bo
|
||||
if (pluginItem.getPluginId() != null) {
|
||||
Plugin plugin = pluginService.getById(pluginItem.getPluginId());
|
||||
if (plugin != null) {
|
||||
if (PluginType.isWorkflow(plugin.getType())) {
|
||||
throw new tech.easyflow.common.web.exceptions.BusinessException("当前版本暂不支持聊天助手绑定工作流插件");
|
||||
}
|
||||
pluginVisibilityService.assertPluginVisible(plugin.getCreatedBy(), plugin.getId(), "无权限绑定插件");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import tech.easyflow.admin.controller.ai.support.AiResourceCreatorNameSupport;
|
||||
import tech.easyflow.ai.permission.KnowledgeVisibilityQueryHelper;
|
||||
import tech.easyflow.ai.documentimport.DocumentImportDtos;
|
||||
import tech.easyflow.ai.dto.KnowledgeSearchResultItem;
|
||||
@@ -76,6 +77,8 @@ public class DocumentCollectionController extends BaseCurdController<DocumentCol
|
||||
private KnowledgePublishAppService knowledgePublishAppService;
|
||||
@Resource
|
||||
private AiResourceApprovalStateService aiResourceApprovalStateService;
|
||||
@Resource
|
||||
private AiResourceCreatorNameSupport aiResourceCreatorNameSupport;
|
||||
|
||||
public DocumentCollectionController(DocumentCollectionService service, DocumentChunkService chunkService, ModelService llmService) {
|
||||
super(service);
|
||||
@@ -143,10 +146,14 @@ public class DocumentCollectionController extends BaseCurdController<DocumentCol
|
||||
)
|
||||
public Result<List<KnowledgeSearchResultItem>> search(@RequestParam BigInteger knowledgeId,
|
||||
@RequestParam String keyword,
|
||||
@RequestParam(required = false) String retrievalMode) {
|
||||
@RequestParam(required = false) String retrievalMode,
|
||||
@RequestParam(required = false) Integer docRecallMaxNum,
|
||||
@RequestParam(required = false) Double simThreshold) {
|
||||
KnowledgeRetrievalRequest request = new KnowledgeRetrievalRequest();
|
||||
request.setKnowledgeId(knowledgeId);
|
||||
request.setQuery(keyword);
|
||||
request.setLimit(docRecallMaxNum);
|
||||
request.setMinSimilarity(simThreshold);
|
||||
request.setRetrievalMode(KnowledgeRetrievalModes.parse(retrievalMode));
|
||||
request.setCallerType("API");
|
||||
request.setCallerId(String.valueOf(knowledgeId));
|
||||
@@ -310,6 +317,7 @@ public class DocumentCollectionController extends BaseCurdController<DocumentCol
|
||||
applyPublishedOnlyFilter(queryWrapper);
|
||||
Page<DocumentCollection> result = super.queryPage(page, queryWrapper);
|
||||
aiResourceApprovalStateService.fillKnowledgeApprovalState(result.getRecords());
|
||||
aiResourceCreatorNameSupport.fillDocumentCollectionCreatorNames(result.getRecords());
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,9 +8,12 @@ import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
import tech.easyflow.ai.documentimport.DocumentImportDtos;
|
||||
import tech.easyflow.ai.documentimport.task.DocumentImportTaskStatusStreamService;
|
||||
import tech.easyflow.ai.entity.Document;
|
||||
import tech.easyflow.ai.entity.DocumentCollection;
|
||||
import tech.easyflow.ai.entity.DocumentCollectionSplitParams;
|
||||
@@ -77,6 +80,9 @@ public class DocumentController extends BaseCurdController<DocumentService, Docu
|
||||
@Autowired
|
||||
private ResourceAccessService resourceAccessService;
|
||||
|
||||
@Autowired
|
||||
private DocumentImportTaskStatusStreamService documentImportTaskStatusStreamService;
|
||||
|
||||
@Value("${easyflow.storage.local.root:}")
|
||||
private String fileUploadPath;
|
||||
|
||||
@@ -233,6 +239,79 @@ public class DocumentController extends BaseCurdController<DocumentService, Docu
|
||||
return documentService.commitImport(request);
|
||||
}
|
||||
|
||||
@PostMapping("import/task/create")
|
||||
@SaCheckPermission("/api/v1/documentCollection/save")
|
||||
public Result<DocumentImportDtos.TaskCreateResponse> createImportTask(@JsonBody DocumentImportDtos.TaskCreateRequest request) {
|
||||
if (request.getKnowledgeId() == null) {
|
||||
throw new BusinessException("知识库id不能为空");
|
||||
}
|
||||
getDocumentCollection(request.getKnowledgeId().toString(), ResourceAction.MANAGE, "无权限管理知识库");
|
||||
return documentService.createImportTask(request);
|
||||
}
|
||||
|
||||
@GetMapping("import/task/detail")
|
||||
@SaCheckPermission("/api/v1/documentCollection/query")
|
||||
public Result<DocumentImportDtos.TaskDetailResponse> getImportTaskDetail(@RequestParam BigInteger taskId) {
|
||||
Result<DocumentImportDtos.TaskDetailResponse> result = documentService.getImportTaskDetail(taskId);
|
||||
if (result.getData() != null && result.getData().getKnowledgeId() != null) {
|
||||
getDocumentCollection(result.getData().getKnowledgeId().toString(), ResourceAction.READ, "无权限访问知识库");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 订阅知识库文档任务状态流。
|
||||
*
|
||||
* @param knowledgeId 知识库 ID
|
||||
* @return SSE 推送连接
|
||||
*/
|
||||
@PostMapping(value = "import/task/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||
@SaCheckPermission("/api/v1/documentCollection/query")
|
||||
public SseEmitter streamImportTask(@JsonBody(value = "knowledgeId", required = true) BigInteger knowledgeId) {
|
||||
getDocumentCollection(knowledgeId.toString(), ResourceAction.READ, "无权限访问知识库");
|
||||
return documentImportTaskStatusStreamService.subscribe(knowledgeId);
|
||||
}
|
||||
|
||||
@PostMapping("import/task/preview")
|
||||
@SaCheckPermission("/api/v1/documentCollection/save")
|
||||
public Result<DocumentImportDtos.PreviewResponse> previewImportTask(@JsonBody DocumentImportDtos.PreviewRequest request) {
|
||||
if (request.getKnowledgeId() == null) {
|
||||
throw new BusinessException("知识库id不能为空");
|
||||
}
|
||||
getDocumentCollection(request.getKnowledgeId().toString(), ResourceAction.MANAGE, "无权限管理知识库");
|
||||
return documentService.previewImportTask(request);
|
||||
}
|
||||
|
||||
@PostMapping("import/task/startIndex")
|
||||
@SaCheckPermission("/api/v1/documentCollection/save")
|
||||
public Result<DocumentImportDtos.TaskStartIndexResponse> startIndexTask(@JsonBody DocumentImportDtos.TaskStartIndexRequest request) {
|
||||
if (request.getKnowledgeId() == null) {
|
||||
throw new BusinessException("知识库id不能为空");
|
||||
}
|
||||
getDocumentCollection(request.getKnowledgeId().toString(), ResourceAction.MANAGE, "无权限管理知识库");
|
||||
return documentService.startIndexTask(request);
|
||||
}
|
||||
|
||||
@PostMapping("import/task/retryParse")
|
||||
@SaCheckPermission("/api/v1/documentCollection/save")
|
||||
public Result<DocumentImportDtos.TaskStartIndexResponse> retryParseTask(@JsonBody DocumentImportDtos.TaskRetryRequest request) {
|
||||
if (request.getKnowledgeId() == null) {
|
||||
throw new BusinessException("知识库id不能为空");
|
||||
}
|
||||
getDocumentCollection(request.getKnowledgeId().toString(), ResourceAction.MANAGE, "无权限管理知识库");
|
||||
return documentService.retryParseTask(request);
|
||||
}
|
||||
|
||||
@PostMapping("import/task/retryIndex")
|
||||
@SaCheckPermission("/api/v1/documentCollection/save")
|
||||
public Result<DocumentImportDtos.TaskStartIndexResponse> retryIndexTask(@JsonBody DocumentImportDtos.TaskRetryRequest request) {
|
||||
if (request.getKnowledgeId() == null) {
|
||||
throw new BusinessException("知识库id不能为空");
|
||||
}
|
||||
getDocumentCollection(request.getKnowledgeId().toString(), ResourceAction.MANAGE, "无权限管理知识库");
|
||||
return documentService.retryIndexTask(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 entity
|
||||
*
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
package tech.easyflow.admin.controller.ai;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import tech.easyflow.ai.dto.KnowledgeShareApiGrantRequest;
|
||||
import tech.easyflow.ai.entity.DocumentCollection;
|
||||
import tech.easyflow.ai.enums.KnowledgeShareActionScope;
|
||||
import tech.easyflow.ai.service.DocumentCollectionService;
|
||||
import tech.easyflow.ai.service.KnowledgeShareAuditService;
|
||||
import tech.easyflow.ai.service.KnowledgeSharePermissionService;
|
||||
import tech.easyflow.ai.service.KnowledgeShareService;
|
||||
import tech.easyflow.ai.vo.KnowledgeShareUrlCreateResult;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.util.RequestUtil;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||
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.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 知识库分享管理接口。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/knowledgeShare")
|
||||
public class KnowledgeShareController {
|
||||
|
||||
@Resource
|
||||
private KnowledgeShareService knowledgeShareService;
|
||||
@Resource
|
||||
private KnowledgeSharePermissionService knowledgeSharePermissionService;
|
||||
@Resource
|
||||
private KnowledgeShareAuditService knowledgeShareAuditService;
|
||||
@Resource
|
||||
private ResourceAccessService resourceAccessService;
|
||||
@Resource
|
||||
private DocumentCollectionService documentCollectionService;
|
||||
|
||||
/**
|
||||
* 创建 URL 分享。
|
||||
*
|
||||
* @param request HTTP 请求
|
||||
* @param knowledgeId 知识库 ID
|
||||
* @return 创建结果
|
||||
*/
|
||||
@PostMapping("/url/create")
|
||||
@SaCheckPermission("/api/v1/documentCollection/save")
|
||||
public Result<KnowledgeShareUrlCreateResult> createUrlShare(HttpServletRequest request, @JsonBody("knowledgeId") BigInteger knowledgeId) {
|
||||
assertManagePermission(knowledgeId);
|
||||
LoginAccount loginAccount = SaTokenUtil.getLoginAccount();
|
||||
KnowledgeShareUrlCreateResult result = knowledgeShareService.createUrlShare(
|
||||
knowledgeId,
|
||||
loginAccount.getTenantId(),
|
||||
loginAccount.getDeptId(),
|
||||
loginAccount.getId(),
|
||||
buildShareBaseUrl(request),
|
||||
KnowledgeShareActionScope.defaultUrlScopes()
|
||||
);
|
||||
knowledgeShareAuditService.log(
|
||||
loginAccount.getId(),
|
||||
"创建知识库 URL 分享",
|
||||
"KNOWLEDGE_SHARE_CREATE",
|
||||
request.getRequestURI(),
|
||||
Map.of("knowledgeId", knowledgeId, "shareId", result.getId())
|
||||
);
|
||||
return Result.ok(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 为系统访问令牌配置知识库 API 分享授权。
|
||||
*
|
||||
* @param request 授权请求
|
||||
* @return 结果
|
||||
*/
|
||||
@PostMapping("/api/grant")
|
||||
@SaCheckPermission("/api/v1/documentCollection/save")
|
||||
public Result<Void> grantApiShare(@JsonBody KnowledgeShareApiGrantRequest request) {
|
||||
assertManagePermission(request.getKnowledgeId());
|
||||
knowledgeSharePermissionService.grantApiShare(
|
||||
request.getApiKeyId(),
|
||||
request.getKnowledgeId(),
|
||||
request.getActionScopes()
|
||||
);
|
||||
LoginAccount loginAccount = SaTokenUtil.getLoginAccount();
|
||||
knowledgeShareAuditService.log(
|
||||
loginAccount.getId(),
|
||||
"配置知识库 API 分享授权",
|
||||
"KNOWLEDGE_API_SHARE_GRANT",
|
||||
"/api/v1/knowledgeShare/api/grant",
|
||||
Map.of(
|
||||
"knowledgeId", request.getKnowledgeId(),
|
||||
"apiKeyId", request.getApiKeyId(),
|
||||
"actionScopes", request.getActionScopes()
|
||||
)
|
||||
);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
private void assertManagePermission(BigInteger knowledgeId) {
|
||||
DocumentCollection knowledge = documentCollectionService.getById(knowledgeId);
|
||||
if (knowledge == null) {
|
||||
throw new BusinessException("知识库不存在");
|
||||
}
|
||||
resourceAccessService.assertAccess(
|
||||
CategoryResourceType.KNOWLEDGE,
|
||||
knowledge,
|
||||
ResourceAction.MANAGE,
|
||||
"无权限管理知识库"
|
||||
);
|
||||
}
|
||||
|
||||
private String buildShareBaseUrl(HttpServletRequest request) {
|
||||
String referer = RequestUtil.getReferer(request);
|
||||
String refererBaseUrl = extractFrontendBaseUrl(referer);
|
||||
if (refererBaseUrl != null) {
|
||||
return refererBaseUrl + "/share/knowledge";
|
||||
}
|
||||
|
||||
String forwardedOrigin = buildForwardedOrigin(request);
|
||||
if (forwardedOrigin != null) {
|
||||
return forwardedOrigin + normalizeBasePath(firstHeaderValue(request.getHeader("X-Forwarded-Prefix"))) + "/share/knowledge";
|
||||
}
|
||||
|
||||
String origin = normalizeOrigin(request.getHeader("Origin"));
|
||||
if (origin != null) {
|
||||
return origin + normalizeBasePath(request.getContextPath()) + "/share/knowledge";
|
||||
}
|
||||
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append(request.getScheme()).append("://").append(request.getServerName());
|
||||
if (request.getServerPort() != 80 && request.getServerPort() != 443) {
|
||||
builder.append(':').append(request.getServerPort());
|
||||
}
|
||||
builder.append(normalizeBasePath(request.getContextPath())).append("/share/knowledge");
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private String extractFrontendBaseUrl(String url) {
|
||||
if (url == null || url.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
URI uri = new URI(url.trim());
|
||||
if (uri.getScheme() == null || uri.getHost() == null) {
|
||||
return null;
|
||||
}
|
||||
String origin = extractOrigin(url);
|
||||
if (origin == null) {
|
||||
return null;
|
||||
}
|
||||
return origin + inferFrontendBasePath(uri.getPath());
|
||||
} catch (URISyntaxException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private String inferFrontendBasePath(String path) {
|
||||
if (path == null || path.isBlank() || "/".equals(path)) {
|
||||
return "";
|
||||
}
|
||||
for (String marker : new String[]{"/ai/", "/auth/", "/share/"}) {
|
||||
int markerIndex = path.indexOf(marker);
|
||||
if (markerIndex > 0) {
|
||||
return normalizeBasePath(path.substring(0, markerIndex));
|
||||
}
|
||||
if (markerIndex == 0) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private String normalizeBasePath(String basePath) {
|
||||
if (basePath == null || basePath.isBlank() || "/".equals(basePath.trim())) {
|
||||
return "";
|
||||
}
|
||||
String normalized = basePath.trim();
|
||||
if (!normalized.startsWith("/")) {
|
||||
normalized = "/" + normalized;
|
||||
}
|
||||
while (normalized.endsWith("/") && normalized.length() > 1) {
|
||||
normalized = normalized.substring(0, normalized.length() - 1);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private String buildForwardedOrigin(HttpServletRequest request) {
|
||||
String proto = firstHeaderValue(request.getHeader("X-Forwarded-Proto"));
|
||||
String host = firstHeaderValue(request.getHeader("X-Forwarded-Host"));
|
||||
if (proto == null || host == null) {
|
||||
return null;
|
||||
}
|
||||
return normalizeOrigin(proto + "://" + host);
|
||||
}
|
||||
|
||||
private String extractOrigin(String url) {
|
||||
if (url == null || url.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
URI uri = new URI(url.trim());
|
||||
if (uri.getScheme() == null || uri.getHost() == null) {
|
||||
return null;
|
||||
}
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append(uri.getScheme()).append("://").append(uri.getHost());
|
||||
if (uri.getPort() != -1 && uri.getPort() != 80 && uri.getPort() != 443) {
|
||||
builder.append(':').append(uri.getPort());
|
||||
}
|
||||
return builder.toString();
|
||||
} catch (URISyntaxException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private String normalizeOrigin(String origin) {
|
||||
return extractOrigin(origin);
|
||||
}
|
||||
|
||||
private String firstHeaderValue(String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
int commaIndex = value.indexOf(',');
|
||||
String normalized = commaIndex >= 0 ? value.substring(0, commaIndex) : value;
|
||||
normalized = normalized.trim();
|
||||
return normalized.isEmpty() ? null : normalized;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
package tech.easyflow.admin.controller.ai;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import tech.easyflow.ai.entity.PluginCategory;
|
||||
import tech.easyflow.ai.entity.PluginCategoryMapping;
|
||||
import tech.easyflow.ai.service.PluginCategoryMappingService;
|
||||
import tech.easyflow.common.annotation.UsePermission;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||
@@ -21,6 +23,7 @@ import java.util.List;
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/pluginCategoryMapping")
|
||||
@UsePermission(moduleName = "/api/v1/plugin")
|
||||
public class PluginCategoryMappingController extends BaseCurdController<PluginCategoryMappingService, PluginCategoryMapping> {
|
||||
public PluginCategoryMappingController(PluginCategoryMappingService service) {
|
||||
super(service);
|
||||
@@ -30,6 +33,7 @@ public class PluginCategoryMappingController extends BaseCurdController<PluginCa
|
||||
private PluginCategoryMappingService relationService;
|
||||
|
||||
@PostMapping("/updateRelation")
|
||||
@SaCheckPermission("/api/v1/plugin/save")
|
||||
public Result<Boolean> updateRelation(
|
||||
@JsonBody(value="pluginId") BigInteger pluginId,
|
||||
@JsonBody(value="categoryIds") ArrayList<BigInteger> categoryIds
|
||||
|
||||
@@ -6,21 +6,33 @@ import com.mybatisflex.core.query.QueryWrapper;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import tech.easyflow.admin.controller.ai.support.AiResourceCreatorNameSupport;
|
||||
import tech.easyflow.ai.entity.Model;
|
||||
import tech.easyflow.ai.entity.Plugin;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
import tech.easyflow.ai.plugin.workflow.snapshot.WorkflowPluginSnapshotResolver;
|
||||
import tech.easyflow.ai.service.ModelService;
|
||||
import tech.easyflow.ai.service.PluginVisibilityService;
|
||||
import tech.easyflow.ai.permission.WorkflowVisibilityQueryHelper;
|
||||
import tech.easyflow.ai.service.WorkflowService;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||
import tech.easyflow.ai.service.PluginService;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceAction;
|
||||
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
|
||||
import tech.easyflow.system.service.CategoryPermissionService;
|
||||
import tech.easyflow.system.service.ResourceAccessService;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
@@ -47,6 +59,16 @@ public class PluginController extends BaseCurdController<PluginService, Plugin>
|
||||
private PluginVisibilityService pluginVisibilityService;
|
||||
@Resource
|
||||
private ModelService modelService;
|
||||
@Resource
|
||||
private WorkflowVisibilityQueryHelper workflowVisibilityQueryHelper;
|
||||
@Resource
|
||||
private WorkflowService workflowService;
|
||||
@Resource
|
||||
private ResourceAccessService resourceAccessService;
|
||||
@Resource
|
||||
private WorkflowPluginSnapshotResolver workflowPluginSnapshotResolver;
|
||||
@Resource
|
||||
private AiResourceCreatorNameSupport aiResourceCreatorNameSupport;
|
||||
|
||||
@Override
|
||||
protected Result<?> onSaveOrUpdateBefore(Plugin entity, boolean isSave) {
|
||||
@@ -79,7 +101,8 @@ public class PluginController extends BaseCurdController<PluginService, Plugin>
|
||||
public Result<List<Plugin>> getList(){
|
||||
QueryWrapper queryWrapper = QueryWrapper.create().select();
|
||||
applyCategoryPermission(queryWrapper);
|
||||
return Result.ok(service.getMapper().selectListByQuery(queryWrapper));
|
||||
List<Plugin> plugins = service.getMapper().selectListWithRelationsByQuery(queryWrapper);
|
||||
return Result.ok(pluginService.preparePluginsForCurrentUser(plugins, true, false));
|
||||
}
|
||||
|
||||
@GetMapping("/pageByCategory")
|
||||
@@ -97,8 +120,29 @@ public class PluginController extends BaseCurdController<PluginService, Plugin>
|
||||
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||
return Result.ok(queryPage(new Page<>(pageNumber, pageSize), queryWrapper));
|
||||
} else {
|
||||
return pluginService.pageByCategory(pageNumber, pageSize, category);
|
||||
Result<Page<Plugin>> result = pluginService.pageByCategory(pageNumber, pageSize, category);
|
||||
if (result != null && result.getData() != null) {
|
||||
aiResourceCreatorNameSupport.fillPluginCreatorNames(result.getData().getRecords());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/workflowCandidates")
|
||||
@SaCheckPermission("/api/v1/plugin/query")
|
||||
public Result<List<Workflow>> workflowCandidates(String keyword) {
|
||||
QueryWrapper queryWrapper = QueryWrapper.create();
|
||||
workflowVisibilityQueryHelper.applyReadableAccess(queryWrapper);
|
||||
queryWrapper.eq("publish_status", tech.easyflow.ai.enums.PublishStatus.PUBLISHED.getCode());
|
||||
if (keyword != null && !keyword.isBlank()) {
|
||||
queryWrapper.like("title", keyword.trim());
|
||||
}
|
||||
queryWrapper.orderBy("modified desc");
|
||||
LoginAccount loginAccount = SaTokenUtil.getLoginAccount();
|
||||
List<Workflow> workflows = workflowService.list(queryWrapper).stream()
|
||||
.filter(workflow -> canBindWorkflowCandidate(workflow, loginAccount))
|
||||
.collect(java.util.stream.Collectors.toCollection(ArrayList::new));
|
||||
return Result.ok(workflows);
|
||||
}
|
||||
|
||||
@GetMapping("/modelList")
|
||||
@@ -110,14 +154,25 @@ public class PluginController extends BaseCurdController<PluginService, Plugin>
|
||||
@Override
|
||||
protected Page<Plugin> queryPage(Page<Plugin> page, QueryWrapper queryWrapper) {
|
||||
applyCategoryPermission(queryWrapper);
|
||||
return service.getMapper().paginateWithRelations(page, queryWrapper);
|
||||
List<Plugin> totalList = service.getMapper().selectListWithRelationsByQuery(queryWrapper);
|
||||
boolean availableOnly = isAvailableOnly();
|
||||
List<Plugin> prepared = pluginService.preparePluginsForCurrentUser(totalList, !availableOnly, availableOnly);
|
||||
aiResourceCreatorNameSupport.fillPluginCreatorNames(prepared);
|
||||
long total = prepared.size();
|
||||
int fromIndex = Math.max(0, Math.toIntExact((page.getPageNumber() - 1) * page.getPageSize()));
|
||||
if (fromIndex >= prepared.size()) {
|
||||
return new Page<>(Collections.emptyList(), page.getPageNumber(), page.getPageSize(), total);
|
||||
}
|
||||
int toIndex = Math.min(prepared.size(), Math.toIntExact(fromIndex + page.getPageSize()));
|
||||
return new Page<>(prepared.subList(fromIndex, toIndex), page.getPageNumber(), page.getPageSize(), total);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Plugin> detail(String id) {
|
||||
Plugin plugin = service.getById(id);
|
||||
Plugin plugin = service.getMapper().selectOneWithRelationsById(id);
|
||||
if (plugin != null) {
|
||||
pluginVisibilityService.assertPluginVisible(plugin.getCreatedBy(), plugin.getId(), "无权限访问插件");
|
||||
pluginService.preparePluginForCurrentUser(plugin);
|
||||
}
|
||||
return Result.ok(plugin);
|
||||
}
|
||||
@@ -138,4 +193,30 @@ public class PluginController extends BaseCurdController<PluginService, Plugin>
|
||||
}
|
||||
queryWrapper.and(PLUGIN.CREATED_BY.eq(access.getAccountIdAsLong()).or(PLUGIN.ID.in(pluginIds)));
|
||||
}
|
||||
|
||||
private boolean isAvailableOnly() {
|
||||
HttpServletRequest request = currentHttpRequest();
|
||||
return request != null && "true".equalsIgnoreCase(request.getParameter("availableOnly"));
|
||||
}
|
||||
|
||||
private HttpServletRequest currentHttpRequest() {
|
||||
org.springframework.web.context.request.ServletRequestAttributes attributes =
|
||||
(org.springframework.web.context.request.ServletRequestAttributes)
|
||||
org.springframework.web.context.request.RequestContextHolder.getRequestAttributes();
|
||||
return attributes == null ? null : attributes.getRequest();
|
||||
}
|
||||
|
||||
private boolean canBindWorkflowCandidate(Workflow workflow, LoginAccount loginAccount) {
|
||||
if (workflow == null || loginAccount == null || loginAccount.getId() == null) {
|
||||
return false;
|
||||
}
|
||||
if (workflow.getPublishedSnapshotJson() == null || workflow.getPublishedSnapshotJson().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
Workflow publishedWorkflow = workflowService.toPublishedView(workflow);
|
||||
if (!workflowPluginSnapshotResolver.isSupportedForWorkflowPlugin(publishedWorkflow)) {
|
||||
return false;
|
||||
}
|
||||
return resourceAccessService.canAccess(loginAccount, CategoryResourceType.WORKFLOW, workflow, ResourceAction.USE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +1,49 @@
|
||||
package tech.easyflow.admin.controller.ai;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.easyagents.flow.core.chain.runtime.ChainExecutor;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
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 tech.easyflow.ai.easyagentsflow.entity.ChainInfo;
|
||||
import tech.easyflow.ai.easyagentsflow.entity.NodeInfo;
|
||||
import tech.easyflow.ai.easyagentsflow.entity.WorkflowCheckStage;
|
||||
import tech.easyflow.ai.easyagentsflow.service.TinyFlowService;
|
||||
import tech.easyflow.ai.easyagentsflow.service.WorkflowCheckService;
|
||||
import tech.easyflow.ai.easyagentsflow.support.PublishedWorkflowDefinitionIds;
|
||||
import tech.easyflow.ai.entity.BotPlugin;
|
||||
import tech.easyflow.ai.entity.Plugin;
|
||||
import tech.easyflow.ai.entity.PluginItem;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
import tech.easyflow.ai.enums.PluginType;
|
||||
import tech.easyflow.ai.plugin.workflow.snapshot.WorkflowPluginSnapshotResolver;
|
||||
import tech.easyflow.ai.service.BotPluginService;
|
||||
import tech.easyflow.ai.service.PluginService;
|
||||
import tech.easyflow.ai.service.PluginItemService;
|
||||
import tech.easyflow.ai.service.WorkflowService;
|
||||
import tech.easyflow.common.constant.Constants;
|
||||
import tech.easyflow.common.annotation.UsePermission;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 控制层。
|
||||
@@ -45,6 +64,18 @@ public class PluginItemController extends BaseCurdController<PluginItemService,
|
||||
|
||||
@Resource
|
||||
private BotPluginService botPluginService;
|
||||
@Resource
|
||||
private PluginService pluginService;
|
||||
@Resource
|
||||
private WorkflowPluginSnapshotResolver workflowPluginSnapshotResolver;
|
||||
@Resource
|
||||
private WorkflowService workflowService;
|
||||
@Resource
|
||||
private ChainExecutor chainExecutor;
|
||||
@Resource
|
||||
private TinyFlowService tinyFlowService;
|
||||
@Resource
|
||||
private WorkflowCheckService workflowCheckService;
|
||||
|
||||
@PostMapping("/tool/save")
|
||||
@SaCheckPermission("/api/v1/plugin/save")
|
||||
@@ -87,8 +118,18 @@ public class PluginItemController extends BaseCurdController<PluginItemService,
|
||||
if (record == null) {
|
||||
return Result.ok(nodeData);
|
||||
}
|
||||
Plugin plugin = pluginService.getById(record.getPluginId());
|
||||
nodeData.put("pluginId", record.getId().toString());
|
||||
nodeData.put("pluginName", record.getName());
|
||||
if (plugin != null) {
|
||||
plugin = pluginService.preparePluginForCurrentUser(plugin);
|
||||
nodeData.put("pluginType", plugin.getType());
|
||||
nodeData.put("workflowId", plugin.getWorkflowId());
|
||||
nodeData.put("workflowTitle", plugin.getWorkflowTitle());
|
||||
nodeData.put("available", plugin.getAvailable());
|
||||
nodeData.put("reasonCode", plugin.getReasonCode());
|
||||
nodeData.put("reasonMessage", plugin.getReasonMessage());
|
||||
}
|
||||
|
||||
JSONArray parameters = new JSONArray();
|
||||
JSONArray outputDefs = new JSONArray();
|
||||
@@ -104,6 +145,7 @@ public class PluginItemController extends BaseCurdController<PluginItemService,
|
||||
handleArray(array);
|
||||
outputDefs = array;
|
||||
}
|
||||
nodeData.put("schemaHash", resolveSchemaHash(record, plugin));
|
||||
nodeData.put("parameters", parameters);
|
||||
nodeData.put("outputDefs", outputDefs);
|
||||
return Result.ok(nodeData);
|
||||
@@ -119,6 +161,71 @@ public class PluginItemController extends BaseCurdController<PluginItemService,
|
||||
return pluginItemService.pluginToolTest(inputData, pluginToolId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 工作流插件异步试运行。
|
||||
*
|
||||
* @param inputData 输入参数 JSON
|
||||
* @param pluginToolId 插件工具 ID
|
||||
* @return 执行 ID
|
||||
*/
|
||||
@PostMapping("/testAsync")
|
||||
@SaCheckPermission("/api/v1/plugin/query")
|
||||
public Result<String> pluginToolTestAsync(@JsonBody(value = "inputData", required = true) String inputData,
|
||||
@JsonBody(value = "pluginToolId", required = true) BigInteger pluginToolId) {
|
||||
PluginItem pluginItem = pluginItemService.getById(pluginToolId);
|
||||
Plugin plugin = requireWorkflowPlugin(pluginItem);
|
||||
Plugin preparedPlugin = pluginService.preparePluginForCurrentUser(plugin);
|
||||
if (Boolean.FALSE.equals(preparedPlugin.getAvailable())) {
|
||||
throw new BusinessException(preparedPlugin.getReasonMessage());
|
||||
}
|
||||
Workflow workflow = workflowService.getPublishedById(preparedPlugin.getWorkflowId());
|
||||
if (workflow == null) {
|
||||
throw new BusinessException("未找到已发布工作流");
|
||||
}
|
||||
workflowCheckService.checkOrThrow(workflow.getContent(), WorkflowCheckStage.PRE_EXECUTE, workflow.getId());
|
||||
Map<String, Object> variables = JSON.parseObject(inputData, Map.class);
|
||||
if (variables == null) {
|
||||
variables = new HashMap<>();
|
||||
}
|
||||
if (StpUtil.isLogin()) {
|
||||
variables.put(Constants.LOGIN_USER_KEY, SaTokenUtil.getLoginAccount());
|
||||
}
|
||||
String executeId = chainExecutor.executeAsync(
|
||||
PublishedWorkflowDefinitionIds.published(String.valueOf(workflow.getId())),
|
||||
variables
|
||||
);
|
||||
return Result.ok(executeId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取工作流插件试运行状态。
|
||||
*
|
||||
* @param executeId 执行 ID
|
||||
* @param nodes 节点列表
|
||||
* @return 链路状态
|
||||
*/
|
||||
@PostMapping("/testChainStatus")
|
||||
@SaCheckPermission("/api/v1/plugin/query")
|
||||
public Result<ChainInfo> pluginToolTestChainStatus(@JsonBody(value = "executeId", required = true) String executeId,
|
||||
@JsonBody("nodes") List<NodeInfo> nodes) {
|
||||
return Result.ok(tinyFlowService.getChainStatus(executeId, nodes));
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复工作流插件试运行。
|
||||
*
|
||||
* @param executeId 执行 ID
|
||||
* @param confirmParams 恢复参数
|
||||
* @return 空结果
|
||||
*/
|
||||
@PostMapping("/testResume")
|
||||
@SaCheckPermission("/api/v1/plugin/query")
|
||||
public Result<Void> pluginToolTestResume(@JsonBody(value = "executeId", required = true) String executeId,
|
||||
@JsonBody("confirmParams") Map<String, Object> confirmParams) {
|
||||
chainExecutor.resumeAsync(executeId, confirmParams);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
private void handleArray(JSONArray array) {
|
||||
for (Object o : array) {
|
||||
JSONObject obj = (JSONObject) o;
|
||||
@@ -134,6 +241,40 @@ public class PluginItemController extends BaseCurdController<PluginItemService,
|
||||
}
|
||||
}
|
||||
|
||||
private String resolveSchemaHash(PluginItem record, Plugin plugin) {
|
||||
if (record == null) {
|
||||
return null;
|
||||
}
|
||||
if (StrUtil.isNotBlank(record.getSchemaHash())) {
|
||||
return record.getSchemaHash();
|
||||
}
|
||||
if (plugin == null || !PluginType.isWorkflow(plugin.getType()) || plugin.getWorkflowId() == null) {
|
||||
return null;
|
||||
}
|
||||
tech.easyflow.ai.entity.Workflow workflow = workflowService.getPublishedById(plugin.getWorkflowId());
|
||||
if (workflow == null) {
|
||||
return null;
|
||||
}
|
||||
return workflowPluginSnapshotResolver.resolveSchemaHash(workflow);
|
||||
}
|
||||
|
||||
private Plugin requireWorkflowPlugin(PluginItem pluginItem) {
|
||||
if (pluginItem == null) {
|
||||
throw new BusinessException("插件工具不存在");
|
||||
}
|
||||
Plugin plugin = pluginService.getById(pluginItem.getPluginId());
|
||||
if (plugin == null) {
|
||||
throw new BusinessException("插件不存在");
|
||||
}
|
||||
if (!PluginType.isWorkflow(plugin.getType())) {
|
||||
throw new BusinessException("当前工具不是工作流插件");
|
||||
}
|
||||
if (plugin.getWorkflowId() == null) {
|
||||
throw new BusinessException("插件未绑定工作流");
|
||||
}
|
||||
return plugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Result<?> onRemoveBefore(Collection<Serializable> ids) {
|
||||
|
||||
@@ -144,6 +285,15 @@ public class PluginItemController extends BaseCurdController<PluginItemService,
|
||||
if (exists){
|
||||
return Result.fail(1, "此工具还关联着bot,请先取消关联!");
|
||||
}
|
||||
if (ids.size() == 1) {
|
||||
PluginItem pluginItem = pluginItemService.getById(ids.iterator().next());
|
||||
if (pluginItem != null) {
|
||||
Plugin plugin = pluginService.getById(pluginItem.getPluginId());
|
||||
if (plugin != null && PluginType.isWorkflow(plugin.getType())) {
|
||||
return Result.fail(1, "工作流插件工具由系统自动维护,不支持删除");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,961 @@
|
||||
package tech.easyflow.admin.controller.ai;
|
||||
|
||||
import cn.hutool.core.io.IoUtil;
|
||||
import com.easyagents.core.model.embedding.EmbeddingModel;
|
||||
import com.easyagents.core.store.DocumentStore;
|
||||
import com.easyagents.core.store.StoreOptions;
|
||||
import com.easyagents.core.store.StoreResult;
|
||||
import com.mybatisflex.core.paginate.Page;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.util.StringUtils;
|
||||
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.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import tech.easyflow.ai.documentimport.DocumentImportDtos;
|
||||
import tech.easyflow.ai.documentimport.task.DocumentImportTaskStatusStreamService;
|
||||
import tech.easyflow.ai.dto.KnowledgeShareLimitedConfigRequest;
|
||||
import tech.easyflow.ai.dto.KnowledgeSearchResultItem;
|
||||
import tech.easyflow.ai.entity.Document;
|
||||
import tech.easyflow.ai.entity.DocumentChunk;
|
||||
import tech.easyflow.ai.entity.DocumentCollection;
|
||||
import tech.easyflow.ai.entity.DocumentCollectionSplitParams;
|
||||
import tech.easyflow.ai.entity.FaqCategory;
|
||||
import tech.easyflow.ai.entity.FaqItem;
|
||||
import tech.easyflow.ai.entity.Model;
|
||||
import tech.easyflow.ai.enums.KnowledgeShareActionScope;
|
||||
import tech.easyflow.ai.rag.KnowledgeRetrievalModes;
|
||||
import tech.easyflow.ai.rag.KnowledgeRetrievalRequest;
|
||||
import tech.easyflow.ai.service.DocumentChunkService;
|
||||
import tech.easyflow.ai.service.DocumentCollectionService;
|
||||
import tech.easyflow.ai.service.DocumentService;
|
||||
import tech.easyflow.ai.service.FaqCategoryService;
|
||||
import tech.easyflow.ai.service.FaqItemService;
|
||||
import tech.easyflow.ai.service.KnowledgeEmbeddingService;
|
||||
import tech.easyflow.ai.service.KnowledgeShareAuditService;
|
||||
import tech.easyflow.ai.service.KnowledgeShareService;
|
||||
import tech.easyflow.ai.service.ModelService;
|
||||
import tech.easyflow.ai.vo.FaqImportResultVo;
|
||||
import tech.easyflow.ai.vo.KnowledgeShareAuthContext;
|
||||
import tech.easyflow.ai.vo.KnowledgeShareViewDetail;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.filestorage.FileStorageService;
|
||||
import tech.easyflow.common.vo.UploadResVo;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.io.InputStream;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 内部员工登录态下的知识库分享访问接口。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/share/knowledge")
|
||||
public class ShareKnowledgeController {
|
||||
|
||||
private static final long MAX_IMAGE_SIZE_BYTES = 5L * 1024L * 1024L;
|
||||
private static final Set<String> ALLOWED_IMAGE_TYPES = new HashSet<>(Arrays.asList(
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"image/webp",
|
||||
"image/gif"
|
||||
));
|
||||
|
||||
@Resource
|
||||
private KnowledgeShareService knowledgeShareService;
|
||||
@Resource
|
||||
private KnowledgeShareAuditService knowledgeShareAuditService;
|
||||
@Resource
|
||||
private DocumentCollectionService documentCollectionService;
|
||||
@Resource
|
||||
private DocumentService documentService;
|
||||
@Resource
|
||||
private DocumentChunkService documentChunkService;
|
||||
@Resource
|
||||
private ModelService modelService;
|
||||
@Resource
|
||||
private FaqItemService faqItemService;
|
||||
@Resource
|
||||
private FaqCategoryService faqCategoryService;
|
||||
@Resource
|
||||
private KnowledgeEmbeddingService knowledgeEmbeddingService;
|
||||
@Resource(name = "default")
|
||||
private FileStorageService fileStorageService;
|
||||
@Resource
|
||||
private DocumentImportTaskStatusStreamService documentImportTaskStatusStreamService;
|
||||
|
||||
/**
|
||||
* 获取知识库详情。
|
||||
*
|
||||
* @param shareKey 分享访问密钥
|
||||
* @return 知识库详情
|
||||
*/
|
||||
@GetMapping("/documentCollection/detail")
|
||||
public Result<KnowledgeShareViewDetail> detail(@RequestParam String shareKey) {
|
||||
KnowledgeShareAuthContext context = knowledgeShareService.assertUrlShareAccess(
|
||||
shareKey,
|
||||
null,
|
||||
KnowledgeShareActionScope.VIEW.name()
|
||||
);
|
||||
audit(context, "访问知识库分享页", "KNOWLEDGE_SHARE_URL_ACCESS", false, auditDetail("knowledgeId", context.getKnowledge().getId()));
|
||||
KnowledgeShareViewDetail detail = new KnowledgeShareViewDetail();
|
||||
detail.setKnowledge(context.getKnowledge());
|
||||
detail.setPermissionScopes(new java.util.ArrayList<String>(context.getShare().getPermissionScopeSet()));
|
||||
return Result.ok(detail);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模型列表。
|
||||
*
|
||||
* @param shareKey 分享访问密钥
|
||||
* @param modelType 模型类型
|
||||
* @return 模型列表
|
||||
*/
|
||||
@GetMapping("/documentCollection/modelList")
|
||||
public Result<List<Model>> modelList(
|
||||
@RequestParam String shareKey,
|
||||
@RequestParam String modelType
|
||||
) {
|
||||
knowledgeShareService.assertUrlShareAccess(shareKey, null, KnowledgeShareActionScope.CONFIG_UPDATE.name());
|
||||
Model entity = new Model();
|
||||
entity.setModelType(modelType);
|
||||
return Result.ok(modelService.listSelectableModels(entity, false, null, null));
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新受限配置。
|
||||
*
|
||||
* @param request 更新请求
|
||||
* @param shareKey 分享访问密钥
|
||||
* @return 结果
|
||||
*/
|
||||
@PostMapping("/documentCollection/shareConfigUpdate")
|
||||
public Result<Void> updateConfig(
|
||||
@JsonBody KnowledgeShareLimitedConfigRequest request,
|
||||
@RequestParam String shareKey
|
||||
) {
|
||||
KnowledgeShareAuthContext context = knowledgeShareService.assertUrlShareAccess(
|
||||
shareKey,
|
||||
request == null ? null : request.getKnowledgeId(),
|
||||
KnowledgeShareActionScope.CONFIG_UPDATE.name()
|
||||
);
|
||||
BigInteger knowledgeId = resolveKnowledgeId(context, request == null ? null : request.getKnowledgeId());
|
||||
DocumentCollection current = context.getKnowledge();
|
||||
DocumentCollection update = new DocumentCollection();
|
||||
update.setId(current.getId());
|
||||
Map<String, Object> options = current.getOptions() == null
|
||||
? new HashMap<>()
|
||||
: new HashMap<>(current.getOptions());
|
||||
if (request.getVectorEmbedModelId() != null) {
|
||||
update.setVectorEmbedModelId(request.getVectorEmbedModelId());
|
||||
options.put(DocumentCollection.KEY_CAN_UPDATE_EMBEDDING_MODEL, true);
|
||||
Model model = modelService.getModelInstance(request.getVectorEmbedModelId());
|
||||
if (model != null) {
|
||||
update.setDimensionOfVectorModel(Model.getEmbeddingDimension(model.toEmbeddingModel()));
|
||||
}
|
||||
}
|
||||
if (request.getRerankModelId() != null || current.getRerankModelId() != null) {
|
||||
update.setRerankModelId(request.getRerankModelId());
|
||||
}
|
||||
if (request.getRerankEnable() != null) {
|
||||
options.put(DocumentCollection.KEY_RERANK_ENABLE, request.getRerankEnable());
|
||||
}
|
||||
if (request.getDocRecallMaxNum() != null) {
|
||||
options.put(DocumentCollection.KEY_DOC_RECALL_MAX_NUM, request.getDocRecallMaxNum());
|
||||
}
|
||||
if (request.getSimThreshold() != null) {
|
||||
options.put(DocumentCollection.KEY_SIMILARITY_THRESHOLD, BigDecimal.valueOf(request.getSimThreshold()));
|
||||
}
|
||||
update.setOptions(options);
|
||||
documentCollectionService.updateById(update);
|
||||
if (Boolean.TRUE.equals(request.getRebuildVectors())) {
|
||||
knowledgeEmbeddingService.rebuildKnowledgeVectors(knowledgeId);
|
||||
}
|
||||
audit(context, "更新知识库分享受限配置", "KNOWLEDGE_SHARE_URL_WRITE", true, auditDetail("knowledgeId", knowledgeId));
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检索知识库。
|
||||
*
|
||||
* @param shareKey 分享访问密钥
|
||||
* @param keyword 检索关键词
|
||||
* @param retrievalMode 检索模式
|
||||
* @return 检索结果
|
||||
*/
|
||||
@GetMapping("/documentCollection/search")
|
||||
public Result<List<KnowledgeSearchResultItem>> search(
|
||||
@RequestParam String shareKey,
|
||||
@RequestParam String keyword,
|
||||
@RequestParam(required = false) String retrievalMode
|
||||
) {
|
||||
KnowledgeShareAuthContext context = knowledgeShareService.assertUrlShareAccess(
|
||||
shareKey,
|
||||
null,
|
||||
KnowledgeShareActionScope.SEARCH.name()
|
||||
);
|
||||
KnowledgeRetrievalRequest request = new KnowledgeRetrievalRequest();
|
||||
request.setKnowledgeId(context.getKnowledge().getId());
|
||||
request.setQuery(keyword);
|
||||
request.setRetrievalMode(KnowledgeRetrievalModes.parse(retrievalMode));
|
||||
request.setCallerType("SHARE_URL");
|
||||
request.setCallerId(String.valueOf(context.getShare().getId()));
|
||||
return Result.ok(toKnowledgeSearchResult(documentCollectionService.search(request)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 文档分页。
|
||||
*/
|
||||
@GetMapping("/document/documentList")
|
||||
public Result<Page<Document>> documentPage(
|
||||
@RequestParam String shareKey,
|
||||
@RequestParam(required = false) String title,
|
||||
@RequestParam(defaultValue = "10") int pageSize,
|
||||
@RequestParam(defaultValue = "1") int pageNumber
|
||||
) {
|
||||
KnowledgeShareAuthContext context = knowledgeShareService.assertUrlShareAccess(
|
||||
shareKey,
|
||||
null,
|
||||
KnowledgeShareActionScope.VIEW.name()
|
||||
);
|
||||
return Result.ok(documentService.getDocumentList(context.getKnowledge().getId().toString(), pageSize, pageNumber, title));
|
||||
}
|
||||
|
||||
/**
|
||||
* 订阅分享知识库的文档任务状态流。
|
||||
*
|
||||
* @param shareKey 分享访问密钥
|
||||
* @param knowledgeId 知识库 ID
|
||||
* @return SSE 推送连接
|
||||
*/
|
||||
@PostMapping(value = "/document/import/task/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||
public SseEmitter streamDocumentTask(
|
||||
@RequestParam String shareKey,
|
||||
@JsonBody(value = "knowledgeId", required = true) BigInteger knowledgeId
|
||||
) {
|
||||
KnowledgeShareAuthContext context = knowledgeShareService.assertUrlShareAccess(
|
||||
shareKey,
|
||||
knowledgeId,
|
||||
KnowledgeShareActionScope.VIEW.name()
|
||||
);
|
||||
return documentImportTaskStatusStreamService.subscribe(context.getKnowledge().getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载文档。
|
||||
*/
|
||||
@GetMapping("/document/download")
|
||||
public void documentDownload(
|
||||
@RequestParam String shareKey,
|
||||
@RequestParam BigInteger documentId,
|
||||
HttpServletResponse response
|
||||
) throws Exception {
|
||||
KnowledgeShareAuthContext context = knowledgeShareService.assertUrlShareAccess(
|
||||
shareKey,
|
||||
null,
|
||||
KnowledgeShareActionScope.VIEW.name()
|
||||
);
|
||||
Document document = documentService.getById(documentId);
|
||||
if (document == null || document.getCollectionId() == null
|
||||
|| document.getCollectionId().compareTo(context.getKnowledge().getId()) != 0) {
|
||||
throw new BusinessException("文档不存在");
|
||||
}
|
||||
response.setContentType("application/octet-stream");
|
||||
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
|
||||
String fileName = URLEncoder.encode(document.getTitle(), StandardCharsets.UTF_8).replaceAll("\\+", "%20");
|
||||
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName);
|
||||
try (InputStream inputStream = fileStorageService.readStream(document.getDocumentPath())) {
|
||||
IoUtil.copy(inputStream, response.getOutputStream());
|
||||
response.flushBuffer();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文档。
|
||||
*/
|
||||
@PostMapping("/document/removeDoc")
|
||||
public Result<?> removeDocument(
|
||||
@RequestParam String shareKey,
|
||||
@JsonBody("id") String id
|
||||
) {
|
||||
KnowledgeShareAuthContext context = knowledgeShareService.assertUrlShareAccess(
|
||||
shareKey,
|
||||
null,
|
||||
KnowledgeShareActionScope.CONTENT_DELETE.name()
|
||||
);
|
||||
Document document = documentService.getById(new BigInteger(id));
|
||||
if (document == null || document.getCollectionId() == null
|
||||
|| document.getCollectionId().compareTo(context.getKnowledge().getId()) != 0) {
|
||||
throw new BusinessException("文档不存在");
|
||||
}
|
||||
boolean success = documentService.removeDoc(id);
|
||||
audit(context, "删除分享文档", "KNOWLEDGE_SHARE_URL_WRITE", true,
|
||||
auditDetail("knowledgeId", context.getKnowledge().getId(), "documentId", id));
|
||||
return Result.ok(success);
|
||||
}
|
||||
|
||||
/**
|
||||
* 文档导入分析。
|
||||
*/
|
||||
@PostMapping("/document/import/analyze")
|
||||
public Result<DocumentImportDtos.AnalyzeResponse> analyzeImport(
|
||||
@RequestParam String shareKey,
|
||||
@JsonBody DocumentImportDtos.AnalyzeRequest request
|
||||
) {
|
||||
KnowledgeShareAuthContext context = knowledgeShareService.assertUrlShareAccess(
|
||||
shareKey,
|
||||
request == null ? null : request.getKnowledgeId(),
|
||||
KnowledgeShareActionScope.CONTENT_CREATE.name()
|
||||
);
|
||||
BigInteger knowledgeId = resolveKnowledgeId(context, request == null ? null : request.getKnowledgeId());
|
||||
request.setKnowledgeId(knowledgeId);
|
||||
audit(context, "分析分享文档导入", "KNOWLEDGE_SHARE_URL_WRITE", true, auditDetail("knowledgeId", knowledgeId));
|
||||
return documentService.analyzeImport(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 文档导入预览。
|
||||
*/
|
||||
@PostMapping("/document/import/preview")
|
||||
public Result<DocumentImportDtos.PreviewResponse> previewImport(
|
||||
@RequestParam String shareKey,
|
||||
@JsonBody DocumentImportDtos.PreviewRequest request
|
||||
) {
|
||||
KnowledgeShareAuthContext context = knowledgeShareService.assertUrlShareAccess(
|
||||
shareKey,
|
||||
request == null ? null : request.getKnowledgeId(),
|
||||
KnowledgeShareActionScope.CONTENT_CREATE.name()
|
||||
);
|
||||
BigInteger knowledgeId = resolveKnowledgeId(context, request == null ? null : request.getKnowledgeId());
|
||||
request.setKnowledgeId(knowledgeId);
|
||||
audit(context, "预览分享文档导入", "KNOWLEDGE_SHARE_URL_WRITE", true, auditDetail("knowledgeId", knowledgeId));
|
||||
return documentService.previewImport(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交文档导入。
|
||||
*/
|
||||
@PostMapping("/document/import/commit")
|
||||
public Result<DocumentImportDtos.CommitResponse> commitImport(
|
||||
@RequestParam String shareKey,
|
||||
@JsonBody DocumentImportDtos.CommitRequest request
|
||||
) {
|
||||
KnowledgeShareAuthContext context = knowledgeShareService.assertUrlShareAccess(
|
||||
shareKey,
|
||||
request == null ? null : request.getKnowledgeId(),
|
||||
KnowledgeShareActionScope.CONTENT_CREATE.name()
|
||||
);
|
||||
BigInteger knowledgeId = resolveKnowledgeId(context, request == null ? null : request.getKnowledgeId());
|
||||
request.setKnowledgeId(knowledgeId);
|
||||
audit(context, "提交分享文档导入", "KNOWLEDGE_SHARE_URL_WRITE", true, auditDetail("knowledgeId", knowledgeId));
|
||||
return documentService.commitImport(request);
|
||||
}
|
||||
|
||||
@PostMapping("/document/import/task/create")
|
||||
public Result<DocumentImportDtos.TaskCreateResponse> createImportTask(
|
||||
@RequestParam String shareKey,
|
||||
@JsonBody DocumentImportDtos.TaskCreateRequest request
|
||||
) {
|
||||
KnowledgeShareAuthContext context = knowledgeShareService.assertUrlShareAccess(
|
||||
shareKey,
|
||||
request == null ? null : request.getKnowledgeId(),
|
||||
KnowledgeShareActionScope.CONTENT_CREATE.name()
|
||||
);
|
||||
BigInteger knowledgeId = resolveKnowledgeId(context, request == null ? null : request.getKnowledgeId());
|
||||
request.setKnowledgeId(knowledgeId);
|
||||
audit(context, "创建分享文档导入任务", "KNOWLEDGE_SHARE_URL_WRITE", true, auditDetail("knowledgeId", knowledgeId));
|
||||
return documentService.createImportTask(request);
|
||||
}
|
||||
|
||||
@GetMapping("/document/import/task/detail")
|
||||
public Result<DocumentImportDtos.TaskDetailResponse> getImportTaskDetail(
|
||||
@RequestParam String shareKey,
|
||||
@RequestParam BigInteger taskId
|
||||
) {
|
||||
KnowledgeShareAuthContext context = knowledgeShareService.assertUrlShareAccess(
|
||||
shareKey,
|
||||
null,
|
||||
KnowledgeShareActionScope.VIEW.name()
|
||||
);
|
||||
Result<DocumentImportDtos.TaskDetailResponse> result = documentService.getImportTaskDetail(taskId);
|
||||
BigInteger knowledgeId = result.getData() == null ? null : result.getData().getKnowledgeId();
|
||||
if (knowledgeId == null || knowledgeId.compareTo(context.getKnowledge().getId()) != 0) {
|
||||
throw new BusinessException("任务不存在");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@PostMapping("/document/import/task/preview")
|
||||
public Result<DocumentImportDtos.PreviewResponse> previewImportTask(
|
||||
@RequestParam String shareKey,
|
||||
@JsonBody DocumentImportDtos.PreviewRequest request
|
||||
) {
|
||||
KnowledgeShareAuthContext context = knowledgeShareService.assertUrlShareAccess(
|
||||
shareKey,
|
||||
request == null ? null : request.getKnowledgeId(),
|
||||
KnowledgeShareActionScope.CONTENT_CREATE.name()
|
||||
);
|
||||
BigInteger knowledgeId = resolveKnowledgeId(context, request == null ? null : request.getKnowledgeId());
|
||||
request.setKnowledgeId(knowledgeId);
|
||||
audit(context, "预览分享文档分块", "KNOWLEDGE_SHARE_URL_WRITE", true, auditDetail("knowledgeId", knowledgeId));
|
||||
return documentService.previewImportTask(request);
|
||||
}
|
||||
|
||||
@PostMapping("/document/import/task/startIndex")
|
||||
public Result<DocumentImportDtos.TaskStartIndexResponse> startIndexTask(
|
||||
@RequestParam String shareKey,
|
||||
@JsonBody DocumentImportDtos.TaskStartIndexRequest request
|
||||
) {
|
||||
KnowledgeShareAuthContext context = knowledgeShareService.assertUrlShareAccess(
|
||||
shareKey,
|
||||
request == null ? null : request.getKnowledgeId(),
|
||||
KnowledgeShareActionScope.CONTENT_CREATE.name()
|
||||
);
|
||||
BigInteger knowledgeId = resolveKnowledgeId(context, request == null ? null : request.getKnowledgeId());
|
||||
request.setKnowledgeId(knowledgeId);
|
||||
audit(context, "启动分享文档向量化", "KNOWLEDGE_SHARE_URL_WRITE", true, auditDetail("knowledgeId", knowledgeId));
|
||||
return documentService.startIndexTask(request);
|
||||
}
|
||||
|
||||
@PostMapping("/document/import/task/retryParse")
|
||||
public Result<DocumentImportDtos.TaskStartIndexResponse> retryParseTask(
|
||||
@RequestParam String shareKey,
|
||||
@JsonBody DocumentImportDtos.TaskRetryRequest request
|
||||
) {
|
||||
KnowledgeShareAuthContext context = knowledgeShareService.assertUrlShareAccess(
|
||||
shareKey,
|
||||
request == null ? null : request.getKnowledgeId(),
|
||||
KnowledgeShareActionScope.CONTENT_CREATE.name()
|
||||
);
|
||||
BigInteger knowledgeId = resolveKnowledgeId(context, request == null ? null : request.getKnowledgeId());
|
||||
request.setKnowledgeId(knowledgeId);
|
||||
audit(context, "重试分享文档解析", "KNOWLEDGE_SHARE_URL_WRITE", true, auditDetail("knowledgeId", knowledgeId));
|
||||
return documentService.retryParseTask(request);
|
||||
}
|
||||
|
||||
@PostMapping("/document/import/task/retryIndex")
|
||||
public Result<DocumentImportDtos.TaskStartIndexResponse> retryIndexTask(
|
||||
@RequestParam String shareKey,
|
||||
@JsonBody DocumentImportDtos.TaskRetryRequest request
|
||||
) {
|
||||
KnowledgeShareAuthContext context = knowledgeShareService.assertUrlShareAccess(
|
||||
shareKey,
|
||||
request == null ? null : request.getKnowledgeId(),
|
||||
KnowledgeShareActionScope.CONTENT_CREATE.name()
|
||||
);
|
||||
BigInteger knowledgeId = resolveKnowledgeId(context, request == null ? null : request.getKnowledgeId());
|
||||
request.setKnowledgeId(knowledgeId);
|
||||
audit(context, "重试分享文档向量化", "KNOWLEDGE_SHARE_URL_WRITE", true, auditDetail("knowledgeId", knowledgeId));
|
||||
return documentService.retryIndexTask(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Chunk 分页。
|
||||
*/
|
||||
@GetMapping("/documentChunk/page")
|
||||
public Result<Page<DocumentChunk>> documentChunkPage(
|
||||
@RequestParam String shareKey,
|
||||
@RequestParam BigInteger documentId,
|
||||
@RequestParam(defaultValue = "1") long pageNumber,
|
||||
@RequestParam(defaultValue = "10") long pageSize
|
||||
) {
|
||||
KnowledgeShareAuthContext context = knowledgeShareService.assertUrlShareAccess(
|
||||
shareKey,
|
||||
null,
|
||||
KnowledgeShareActionScope.VIEW.name()
|
||||
);
|
||||
Document document = documentService.getById(documentId);
|
||||
if (document == null || document.getCollectionId() == null
|
||||
|| document.getCollectionId().compareTo(context.getKnowledge().getId()) != 0) {
|
||||
throw new BusinessException("文档不存在");
|
||||
}
|
||||
QueryWrapper wrapper = QueryWrapper.create()
|
||||
.eq(DocumentChunk::getDocumentId, documentId)
|
||||
.orderBy("sorting asc");
|
||||
return Result.ok(documentChunkService.page(new Page<>(pageNumber, pageSize), wrapper));
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 Chunk。
|
||||
*/
|
||||
@PostMapping("/documentChunk/update")
|
||||
public Result<?> updateDocumentChunk(
|
||||
@RequestParam String shareKey,
|
||||
@JsonBody DocumentChunk documentChunk
|
||||
) {
|
||||
KnowledgeShareAuthContext context = knowledgeShareService.assertUrlShareAccess(
|
||||
shareKey,
|
||||
null,
|
||||
KnowledgeShareActionScope.CONTENT_UPDATE.name()
|
||||
);
|
||||
DocumentChunk current = documentChunkService.getById(documentChunk.getId());
|
||||
if (current == null || current.getDocumentCollectionId() == null
|
||||
|| current.getDocumentCollectionId().compareTo(context.getKnowledge().getId()) != 0) {
|
||||
throw new BusinessException("记录不存在");
|
||||
}
|
||||
boolean success = documentChunkService.updateById(documentChunk);
|
||||
if (success) {
|
||||
DocumentStore documentStore = context.getKnowledge().toDocumentStore();
|
||||
if (documentStore == null) {
|
||||
return Result.fail(2, "知识库没有配置向量库");
|
||||
}
|
||||
Model model = modelService.getModelInstance(context.getKnowledge().getVectorEmbedModelId());
|
||||
if (model == null) {
|
||||
return Result.fail(3, "知识库没有配置向量模型");
|
||||
}
|
||||
EmbeddingModel embeddingModel = model.toEmbeddingModel();
|
||||
documentStore.setEmbeddingModel(embeddingModel);
|
||||
StoreOptions options = StoreOptions.ofCollectionName(context.getKnowledge().getVectorStoreCollection());
|
||||
com.easyagents.core.document.Document doc = com.easyagents.core.document.Document.of(documentChunk.getContent());
|
||||
doc.setId(documentChunk.getId());
|
||||
StoreResult result = documentStore.update(doc, options);
|
||||
audit(context, "更新分享文档 Chunk", "KNOWLEDGE_SHARE_URL_WRITE", true,
|
||||
auditDetail("knowledgeId", context.getKnowledge().getId(), "chunkId", documentChunk.getId()));
|
||||
return Result.ok(result);
|
||||
}
|
||||
return Result.ok(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除 Chunk。
|
||||
*/
|
||||
@PostMapping("/documentChunk/removeChunk")
|
||||
public Result<?> removeDocumentChunk(
|
||||
@RequestParam String shareKey,
|
||||
@JsonBody("id") BigInteger chunkId
|
||||
) {
|
||||
KnowledgeShareAuthContext context = knowledgeShareService.assertUrlShareAccess(
|
||||
shareKey,
|
||||
null,
|
||||
KnowledgeShareActionScope.CONTENT_DELETE.name()
|
||||
);
|
||||
DocumentChunk current = documentChunkService.getById(chunkId);
|
||||
if (current == null || current.getDocumentCollectionId() == null
|
||||
|| current.getDocumentCollectionId().compareTo(context.getKnowledge().getId()) != 0) {
|
||||
return Result.fail(1, "记录不存在");
|
||||
}
|
||||
DocumentStore documentStore = context.getKnowledge().toDocumentStore();
|
||||
if (documentStore == null) {
|
||||
return Result.fail(2, "知识库没有配置向量库");
|
||||
}
|
||||
Model model = modelService.getModelInstance(context.getKnowledge().getVectorEmbedModelId());
|
||||
if (model == null) {
|
||||
return Result.fail(3, "知识库没有配置向量模型");
|
||||
}
|
||||
documentStore.setEmbeddingModel(model.toEmbeddingModel());
|
||||
StoreOptions options = StoreOptions.ofCollectionName(context.getKnowledge().getVectorStoreCollection());
|
||||
documentStore.delete(Collections.singletonList(chunkId), options);
|
||||
documentChunkService.removeById(chunkId);
|
||||
audit(context, "删除分享文档 Chunk", "KNOWLEDGE_SHARE_URL_WRITE", true,
|
||||
auditDetail("knowledgeId", context.getKnowledge().getId(), "chunkId", chunkId));
|
||||
return Result.ok(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* FAQ 分类列表。
|
||||
*/
|
||||
@GetMapping("/faqCategory/list")
|
||||
public Result<List<FaqCategory>> faqCategoryList(
|
||||
@RequestParam String shareKey,
|
||||
@RequestParam(required = false) Boolean asTree
|
||||
) {
|
||||
KnowledgeShareAuthContext context = knowledgeShareService.assertUrlShareAccess(
|
||||
shareKey,
|
||||
null,
|
||||
KnowledgeShareActionScope.VIEW.name()
|
||||
);
|
||||
return Result.ok(faqCategoryService.listByCollection(context.getKnowledge().getId(), asTree));
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存 FAQ 分类。
|
||||
*/
|
||||
@PostMapping("/faqCategory/save")
|
||||
public Result<?> saveFaqCategory(
|
||||
@RequestParam String shareKey,
|
||||
@JsonBody FaqCategory entity
|
||||
) {
|
||||
KnowledgeShareAuthContext context = knowledgeShareService.assertUrlShareAccess(
|
||||
shareKey,
|
||||
entity.getCollectionId(),
|
||||
KnowledgeShareActionScope.CONTENT_CREATE.name()
|
||||
);
|
||||
audit(context, "新增 FAQ 分类", "KNOWLEDGE_SHARE_URL_WRITE", true, auditDetail("knowledgeId", entity.getCollectionId()));
|
||||
return Result.ok(faqCategoryService.saveCategory(entity));
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 FAQ 分类。
|
||||
*/
|
||||
@PostMapping("/faqCategory/update")
|
||||
public Result<?> updateFaqCategory(
|
||||
@RequestParam String shareKey,
|
||||
@JsonBody FaqCategory entity
|
||||
) {
|
||||
KnowledgeShareAuthContext context = knowledgeShareService.assertUrlShareAccess(
|
||||
shareKey,
|
||||
null,
|
||||
KnowledgeShareActionScope.CONTENT_UPDATE.name()
|
||||
);
|
||||
audit(context, "更新 FAQ 分类", "KNOWLEDGE_SHARE_URL_WRITE", true,
|
||||
auditDetail("knowledgeId", context.getKnowledge().getId(), "categoryId", entity.getId()));
|
||||
return Result.ok(faqCategoryService.updateCategory(entity));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除 FAQ 分类。
|
||||
*/
|
||||
@PostMapping("/faqCategory/remove")
|
||||
public Result<?> removeFaqCategory(
|
||||
@RequestParam String shareKey,
|
||||
@JsonBody("id") BigInteger id
|
||||
) {
|
||||
KnowledgeShareAuthContext context = knowledgeShareService.assertUrlShareAccess(
|
||||
shareKey,
|
||||
null,
|
||||
KnowledgeShareActionScope.CONTENT_DELETE.name()
|
||||
);
|
||||
audit(context, "删除 FAQ 分类", "KNOWLEDGE_SHARE_URL_WRITE", true,
|
||||
auditDetail("knowledgeId", context.getKnowledge().getId(), "categoryId", id));
|
||||
return Result.ok(faqCategoryService.removeCategory(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* FAQ 分页。
|
||||
*/
|
||||
@GetMapping("/faqItem/page")
|
||||
public Result<Page<FaqItem>> faqPage(
|
||||
@RequestParam String shareKey,
|
||||
HttpServletRequest request,
|
||||
@RequestParam(defaultValue = "1") long pageNumber,
|
||||
@RequestParam(defaultValue = "10") long pageSize
|
||||
) {
|
||||
KnowledgeShareAuthContext context = knowledgeShareService.assertUrlShareAccess(
|
||||
shareKey,
|
||||
null,
|
||||
KnowledgeShareActionScope.VIEW.name()
|
||||
);
|
||||
faqCategoryService.ensureDefaultCategory(context.getKnowledge().getId());
|
||||
QueryWrapper queryWrapper = QueryWrapper.create()
|
||||
.eq(FaqItem::getCollectionId, context.getKnowledge().getId());
|
||||
String question = request.getParameter("question");
|
||||
if (StringUtils.hasText(question)) {
|
||||
queryWrapper.like(FaqItem::getQuestion, question.trim());
|
||||
}
|
||||
String categoryId = request.getParameter("categoryId");
|
||||
if (StringUtils.hasText(categoryId)) {
|
||||
List<BigInteger> descendantIds =
|
||||
faqCategoryService.findDescendantIds(context.getKnowledge().getId(), new BigInteger(categoryId));
|
||||
if (descendantIds.isEmpty()) {
|
||||
queryWrapper.eq(FaqItem::getId, BigInteger.ZERO);
|
||||
} else {
|
||||
queryWrapper.in(FaqItem::getCategoryId, descendantIds);
|
||||
}
|
||||
}
|
||||
queryWrapper.orderBy("order_no asc");
|
||||
Page<FaqItem> page = faqItemService.page(new Page<>(pageNumber, pageSize), queryWrapper);
|
||||
Map<BigInteger, String> pathMap = faqCategoryService.buildPathMap(context.getKnowledge().getId());
|
||||
if (page.getRecords() != null) {
|
||||
for (FaqItem record : page.getRecords()) {
|
||||
record.setCategoryPath(pathMap.get(record.getCategoryId()));
|
||||
}
|
||||
}
|
||||
return Result.ok(page);
|
||||
}
|
||||
|
||||
/**
|
||||
* FAQ 详情。
|
||||
*/
|
||||
@GetMapping("/faqItem/detail")
|
||||
public Result<FaqItem> faqDetail(
|
||||
@RequestParam String shareKey,
|
||||
@RequestParam String id
|
||||
) {
|
||||
KnowledgeShareAuthContext context = knowledgeShareService.assertUrlShareAccess(
|
||||
shareKey,
|
||||
null,
|
||||
KnowledgeShareActionScope.VIEW.name()
|
||||
);
|
||||
FaqItem item = faqItemService.getById(id);
|
||||
if (item == null || item.getCollectionId() == null
|
||||
|| item.getCollectionId().compareTo(context.getKnowledge().getId()) != 0) {
|
||||
throw new BusinessException("FAQ不存在");
|
||||
}
|
||||
return Result.ok(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存 FAQ。
|
||||
*/
|
||||
@PostMapping("/faqItem/save")
|
||||
public Result<?> saveFaq(
|
||||
@RequestParam String shareKey,
|
||||
@JsonBody FaqItem entity
|
||||
) {
|
||||
KnowledgeShareAuthContext context = knowledgeShareService.assertUrlShareAccess(
|
||||
shareKey,
|
||||
entity.getCollectionId(),
|
||||
KnowledgeShareActionScope.CONTENT_CREATE.name()
|
||||
);
|
||||
audit(context, "新增 FAQ", "KNOWLEDGE_SHARE_URL_WRITE", true, auditDetail("knowledgeId", entity.getCollectionId()));
|
||||
return Result.ok(faqItemService.saveFaqItem(entity));
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 FAQ。
|
||||
*/
|
||||
@PostMapping("/faqItem/update")
|
||||
public Result<?> updateFaq(
|
||||
@RequestParam String shareKey,
|
||||
@JsonBody FaqItem entity
|
||||
) {
|
||||
KnowledgeShareAuthContext context = knowledgeShareService.assertUrlShareAccess(
|
||||
shareKey,
|
||||
null,
|
||||
KnowledgeShareActionScope.CONTENT_UPDATE.name()
|
||||
);
|
||||
audit(context, "更新 FAQ", "KNOWLEDGE_SHARE_URL_WRITE", true,
|
||||
auditDetail("knowledgeId", context.getKnowledge().getId(), "faqId", entity.getId()));
|
||||
return Result.ok(faqItemService.updateFaqItem(entity));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除 FAQ。
|
||||
*/
|
||||
@PostMapping("/faqItem/remove")
|
||||
public Result<?> removeFaq(
|
||||
@RequestParam String shareKey,
|
||||
@JsonBody("id") BigInteger id
|
||||
) {
|
||||
KnowledgeShareAuthContext context = knowledgeShareService.assertUrlShareAccess(
|
||||
shareKey,
|
||||
null,
|
||||
KnowledgeShareActionScope.CONTENT_DELETE.name()
|
||||
);
|
||||
audit(context, "删除 FAQ", "KNOWLEDGE_SHARE_URL_WRITE", true,
|
||||
auditDetail("knowledgeId", context.getKnowledge().getId(), "faqId", id));
|
||||
return Result.ok(faqItemService.removeFaqItem(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传 FAQ 图片。
|
||||
*/
|
||||
@PostMapping(value = "/faqItem/uploadImage", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public Result<UploadResVo> uploadFaqImage(
|
||||
@RequestParam String shareKey,
|
||||
MultipartFile file,
|
||||
BigInteger collectionId
|
||||
) {
|
||||
KnowledgeShareAuthContext context = knowledgeShareService.assertUrlShareAccess(
|
||||
shareKey,
|
||||
collectionId,
|
||||
KnowledgeShareActionScope.CONTENT_UPDATE.name()
|
||||
);
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new BusinessException("图片不能为空");
|
||||
}
|
||||
if (file.getSize() > MAX_IMAGE_SIZE_BYTES) {
|
||||
throw new BusinessException("图片大小不能超过5MB");
|
||||
}
|
||||
if (!isAllowedImageType(file)) {
|
||||
throw new BusinessException("仅支持 JPG/PNG/WEBP/GIF 图片");
|
||||
}
|
||||
String path = fileStorageService.save(file, "faq/" + collectionId);
|
||||
UploadResVo result = new UploadResVo();
|
||||
result.setPath(path);
|
||||
audit(context, "上传 FAQ 图片", "KNOWLEDGE_SHARE_URL_WRITE", true, auditDetail("knowledgeId", collectionId));
|
||||
return Result.ok(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入 FAQ Excel。
|
||||
*/
|
||||
@PostMapping(value = "/faqItem/importExcel", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
public Result<FaqImportResultVo> importFaqExcel(
|
||||
@RequestParam String shareKey,
|
||||
MultipartFile file,
|
||||
BigInteger collectionId
|
||||
) {
|
||||
KnowledgeShareAuthContext context = knowledgeShareService.assertUrlShareAccess(
|
||||
shareKey,
|
||||
collectionId,
|
||||
KnowledgeShareActionScope.IMPORT_EXPORT.name()
|
||||
);
|
||||
audit(context, "导入 FAQ Excel", "KNOWLEDGE_SHARE_URL_WRITE", true, auditDetail("knowledgeId", collectionId));
|
||||
return Result.ok(faqItemService.importFromExcel(collectionId, file));
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载 FAQ 导入模板。
|
||||
*/
|
||||
@GetMapping("/faqItem/downloadImportTemplate")
|
||||
public void downloadFaqImportTemplate(
|
||||
@RequestParam String shareKey,
|
||||
@RequestParam BigInteger collectionId,
|
||||
HttpServletResponse response
|
||||
) throws Exception {
|
||||
KnowledgeShareAuthContext context = knowledgeShareService.assertUrlShareAccess(
|
||||
shareKey,
|
||||
collectionId,
|
||||
KnowledgeShareActionScope.IMPORT_EXPORT.name()
|
||||
);
|
||||
response.setContentType("application/octet-stream");
|
||||
response.setHeader(
|
||||
"Content-disposition",
|
||||
"attachment;filename*=utf-8''" + URLEncoder.encode("faq_import_template.xlsx", StandardCharsets.UTF_8)
|
||||
);
|
||||
faqItemService.writeImportTemplate(response.getOutputStream());
|
||||
response.flushBuffer();
|
||||
audit(context, "下载 FAQ 导入模板", "KNOWLEDGE_SHARE_URL_ACCESS", false, auditDetail("knowledgeId", collectionId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出 FAQ Excel。
|
||||
*/
|
||||
@GetMapping("/faqItem/exportExcel")
|
||||
public void exportFaqExcel(
|
||||
@RequestParam String shareKey,
|
||||
@RequestParam BigInteger collectionId,
|
||||
HttpServletResponse response
|
||||
) throws Exception {
|
||||
KnowledgeShareAuthContext context = knowledgeShareService.assertUrlShareAccess(
|
||||
shareKey,
|
||||
collectionId,
|
||||
KnowledgeShareActionScope.IMPORT_EXPORT.name()
|
||||
);
|
||||
String fileName = "faq_export_" + new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()) + ".xlsx";
|
||||
response.setContentType("application/octet-stream");
|
||||
response.setHeader(
|
||||
"Content-disposition",
|
||||
"attachment;filename*=utf-8''" + URLEncoder.encode(fileName, StandardCharsets.UTF_8)
|
||||
);
|
||||
faqItemService.exportToExcel(collectionId, response.getOutputStream());
|
||||
response.flushBuffer();
|
||||
audit(context, "导出 FAQ Excel", "KNOWLEDGE_SHARE_URL_ACCESS", false, auditDetail("knowledgeId", collectionId));
|
||||
}
|
||||
|
||||
private boolean isAllowedImageType(MultipartFile file) {
|
||||
String contentType = file.getContentType();
|
||||
return contentType != null && ALLOWED_IMAGE_TYPES.contains(contentType.toLowerCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验分享请求中的知识库标识。
|
||||
*
|
||||
* @param knowledgeId 知识库 ID
|
||||
* @return 已校验的知识库 ID
|
||||
*/
|
||||
private BigInteger resolveKnowledgeId(KnowledgeShareAuthContext context, BigInteger requestedKnowledgeId) {
|
||||
if (requestedKnowledgeId != null) {
|
||||
return requestedKnowledgeId;
|
||||
}
|
||||
if (context != null && context.getKnowledge() != null && context.getKnowledge().getId() != null) {
|
||||
return context.getKnowledge().getId();
|
||||
}
|
||||
if (context == null || context.getKnowledge() == null) {
|
||||
throw new BusinessException("知识库不能为空");
|
||||
}
|
||||
throw new BusinessException("知识库不存在");
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造允许空值的审计详情。
|
||||
*
|
||||
* @param keyValues 键值对
|
||||
* @return 审计详情
|
||||
*/
|
||||
private Map<String, Object> auditDetail(Object... keyValues) {
|
||||
if (keyValues == null || keyValues.length == 0) {
|
||||
return new HashMap<>();
|
||||
}
|
||||
if ((keyValues.length & 1) != 0) {
|
||||
throw new IllegalArgumentException("审计详情参数必须成对出现");
|
||||
}
|
||||
Map<String, Object> detail = new HashMap<>(keyValues.length / 2);
|
||||
for (int index = 0; index < keyValues.length; index += 2) {
|
||||
Object key = keyValues[index];
|
||||
if (!(key instanceof String detailKey) || !StringUtils.hasText(detailKey)) {
|
||||
throw new IllegalArgumentException("审计详情 key 必须为非空字符串");
|
||||
}
|
||||
detail.put(detailKey, keyValues[index + 1]);
|
||||
}
|
||||
return detail;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录分享访问审计。
|
||||
*
|
||||
* @param context 分享鉴权上下文
|
||||
* @param actionName 动作名称
|
||||
* @param actionType 动作类型
|
||||
* @param writeOperation 是否写操作
|
||||
* @param detail 审计详情
|
||||
*/
|
||||
private void audit(KnowledgeShareAuthContext context, String actionName, String actionType, boolean writeOperation, Map<String, Object> detail) {
|
||||
Map<String, Object> payload = new HashMap<>(detail);
|
||||
payload.put("shareId", context.getShare().getId());
|
||||
payload.put("writeOperation", writeOperation);
|
||||
knowledgeShareAuditService.log(null, actionName, actionType, "/api/v1/share/knowledge", payload);
|
||||
}
|
||||
|
||||
private List<KnowledgeSearchResultItem> toKnowledgeSearchResult(List<com.easyagents.core.document.Document> documents) {
|
||||
List<KnowledgeSearchResultItem> result = new java.util.ArrayList<>();
|
||||
if (documents == null) {
|
||||
return result;
|
||||
}
|
||||
for (int index = 0; index < documents.size(); index++) {
|
||||
com.easyagents.core.document.Document document = documents.get(index);
|
||||
if (document == null) {
|
||||
continue;
|
||||
}
|
||||
KnowledgeSearchResultItem item = new KnowledgeSearchResultItem();
|
||||
item.setSorting(index + 1);
|
||||
item.setContent(document.getContent());
|
||||
item.setScore(document.getScore() == null ? null : document.getScore().doubleValue());
|
||||
Object hitSource = document.getMetadata("hitSource");
|
||||
item.setHitSource(hitSource == null ? null : String.valueOf(hitSource));
|
||||
item.setVectorScore(asDouble(document.getMetadata("vectorScore")));
|
||||
item.setKeywordScore(asDouble(document.getMetadata("keywordScore")));
|
||||
result.add(item);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Double asDouble(Object value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value instanceof Number number) {
|
||||
return number.doubleValue();
|
||||
}
|
||||
if (value instanceof String text && StringUtils.hasText(text)) {
|
||||
try {
|
||||
return Double.parseDouble(text);
|
||||
} catch (NumberFormatException ignore) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import tech.easyflow.admin.controller.ai.support.AiResourceCreatorNameSupport;
|
||||
import tech.easyflow.ai.permission.WorkflowVisibilityQueryHelper;
|
||||
import tech.easyflow.ai.easyagentsflow.entity.ChainInfo;
|
||||
import tech.easyflow.ai.easyagentsflow.entity.NodeInfo;
|
||||
@@ -93,6 +94,8 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
||||
private WorkflowPublishAppService workflowPublishAppService;
|
||||
@Resource
|
||||
private AiResourceApprovalStateService aiResourceApprovalStateService;
|
||||
@Resource
|
||||
private AiResourceCreatorNameSupport aiResourceCreatorNameSupport;
|
||||
|
||||
public WorkflowController(WorkflowService service, ModelService modelService) {
|
||||
super(service);
|
||||
@@ -120,6 +123,12 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
||||
if (workflow == null) {
|
||||
return Result.fail(1, "工作流不存在");
|
||||
}
|
||||
if (variables == null) {
|
||||
variables = new HashMap<>();
|
||||
}
|
||||
if (StpUtil.isLogin()) {
|
||||
variables.put(Constants.LOGIN_USER_KEY, SaTokenUtil.getLoginAccount());
|
||||
}
|
||||
Map<String, Object> res = chainExecutor.executeNode(workflowId.toString(), nodeId, variables);
|
||||
return Result.ok(res);
|
||||
}
|
||||
@@ -428,6 +437,7 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
||||
applyPublishedOnlyFilter(queryWrapper);
|
||||
Page<Workflow> result = super.queryPage(page, queryWrapper);
|
||||
aiResourceApprovalStateService.fillWorkflowApprovalState(result.getRecords());
|
||||
aiResourceCreatorNameSupport.fillWorkflowCreatorNames(result.getRecords());
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
package tech.easyflow.admin.controller.ai.support;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
import tech.easyflow.ai.entity.Bot;
|
||||
import tech.easyflow.ai.entity.DocumentCollection;
|
||||
import tech.easyflow.ai.entity.Plugin;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
import tech.easyflow.system.service.SysAccountService;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* 为 AI 资源批量补充创建人展示名称。
|
||||
*
|
||||
* <p>该组件只做展示字段填充,不参与权限或查询逻辑。</p>
|
||||
*
|
||||
* @author Codex
|
||||
* @since 2026-04-12
|
||||
*/
|
||||
@Component
|
||||
public class AiResourceCreatorNameSupport {
|
||||
|
||||
@Resource
|
||||
private SysAccountService sysAccountService;
|
||||
|
||||
/**
|
||||
* 批量填充工作流创建人名称。
|
||||
*
|
||||
* @param workflows 工作流集合
|
||||
*/
|
||||
public void fillWorkflowCreatorNames(Collection<Workflow> workflows) {
|
||||
fillCreatorNames(workflows, Workflow::getCreatedBy, Workflow::setCreatedByName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量填充聊天助手创建人名称。
|
||||
*
|
||||
* @param bots 聊天助手集合
|
||||
*/
|
||||
public void fillBotCreatorNames(Collection<Bot> bots) {
|
||||
fillCreatorNames(bots, Bot::getCreatedBy, Bot::setCreatedByName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量填充知识库创建人名称。
|
||||
*
|
||||
* @param collections 知识库集合
|
||||
*/
|
||||
public void fillDocumentCollectionCreatorNames(Collection<DocumentCollection> collections) {
|
||||
fillCreatorNames(collections, DocumentCollection::getCreatedBy, DocumentCollection::setCreatedByName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量填充插件创建人名称。
|
||||
*
|
||||
* @param plugins 插件集合
|
||||
*/
|
||||
public void fillPluginCreatorNames(Collection<Plugin> plugins) {
|
||||
fillCreatorNames(plugins, Plugin::getCreatedBy, Plugin::setCreatedByName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用的创建人名称填充逻辑。
|
||||
*
|
||||
* @param resources 资源集合
|
||||
* @param createdByGetter 创建人 ID 提取函数
|
||||
* @param createdByNameSetter 创建人名称回填函数
|
||||
* @param <T> 资源类型
|
||||
*/
|
||||
private <T> void fillCreatorNames(
|
||||
Collection<T> resources,
|
||||
Function<T, Number> createdByGetter,
|
||||
BiConsumer<T, String> createdByNameSetter
|
||||
) {
|
||||
if (resources == null || resources.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
LinkedHashSet<BigInteger> creatorIds = resources.stream()
|
||||
.map(createdByGetter)
|
||||
.map(this::toBigInteger)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new));
|
||||
if (creatorIds.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Map<BigInteger, String> displayNameMap = sysAccountService.resolveDisplayNameMap(creatorIds);
|
||||
for (T resource : resources) {
|
||||
BigInteger creatorId = toBigInteger(createdByGetter.apply(resource));
|
||||
if (creatorId == null) {
|
||||
continue;
|
||||
}
|
||||
createdByNameSetter.accept(resource, displayNameMap.getOrDefault(creatorId, creatorId.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一把不同数值类型转换为 {@link BigInteger}。
|
||||
*
|
||||
* @param value 原始数值
|
||||
* @return 归一化后的 {@link BigInteger}
|
||||
*/
|
||||
private BigInteger toBigInteger(Number value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value instanceof BigInteger) {
|
||||
return (BigInteger) value;
|
||||
}
|
||||
return BigInteger.valueOf(value.longValue());
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ 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 tech.easyflow.ai.service.KnowledgeSharePermissionService;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
@@ -43,6 +44,8 @@ public class SysApiKeyController extends BaseCurdController<SysApiKeyService, Sy
|
||||
|
||||
@Resource
|
||||
private SysApiKeyResourceMappingService sysApiKeyResourceMappingService;
|
||||
@Resource
|
||||
private KnowledgeSharePermissionService knowledgeSharePermissionService;
|
||||
/**
|
||||
* 添加(保存)数据
|
||||
*
|
||||
@@ -79,10 +82,20 @@ public class SysApiKeyController extends BaseCurdController<SysApiKeyService, Sy
|
||||
|
||||
@Override
|
||||
protected void onSaveOrUpdateAfter(SysApiKey entity, boolean isSave) {
|
||||
if (!isSave && entity.getPermissionIds() != null && !entity.getPermissionIds().isEmpty()) {
|
||||
// 修改的时候绑定授权接口
|
||||
if (entity.getPermissionIds() != null) {
|
||||
sysApiKeyResourceMappingService.authInterface(entity);
|
||||
}
|
||||
if (entity.getKnowledgeShareEnabled() != null) {
|
||||
knowledgeSharePermissionService.replaceApiShareEnabled(entity.getId(), entity.getKnowledgeShareEnabled());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@GetMapping("/detail")
|
||||
public Result<SysApiKey> detail(String id) {
|
||||
Result<SysApiKey> result = super.detail(id);
|
||||
fillApiKeyPermissions(result.getData());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -91,11 +104,30 @@ public class SysApiKeyController extends BaseCurdController<SysApiKeyService, Sy
|
||||
Result<Page<SysApiKey>> pageResult = (Result<Page<SysApiKey>>) super.page(request, sortKey, sortType, pageNumber, pageSize);
|
||||
Page<SysApiKey> data = pageResult.getData();
|
||||
List<SysApiKey> records = data.getRecords();
|
||||
records.forEach(record -> {
|
||||
QueryWrapper queryWrapper = QueryWrapper.create().select(SysApiKeyResourceMapping::getApiKeyResourceId).eq(SysApiKeyResourceMapping::getApiKeyId, record.getId());
|
||||
List<BigInteger> resourceIds = sysApiKeyResourceMappingService.listAs(queryWrapper, BigInteger.class);
|
||||
record.setPermissionIds(resourceIds);
|
||||
});
|
||||
records.forEach(this::fillApiKeyPermissions);
|
||||
return pageResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* 回填访问令牌的接口与知识库授权。
|
||||
*
|
||||
* @param entity 访问令牌
|
||||
*/
|
||||
private void fillApiKeyPermissions(SysApiKey entity) {
|
||||
if (entity == null || entity.getId() == null) {
|
||||
return;
|
||||
}
|
||||
QueryWrapper interfaceWrapper = QueryWrapper.create()
|
||||
.select(SysApiKeyResourceMapping::getApiKeyResourceId)
|
||||
.eq(SysApiKeyResourceMapping::getApiKeyId, entity.getId())
|
||||
.isNull(SysApiKeyResourceMapping::getResourceType);
|
||||
List<BigInteger> resourceIds = sysApiKeyResourceMappingService.listAs(interfaceWrapper, BigInteger.class);
|
||||
entity.setPermissionIds(resourceIds);
|
||||
|
||||
QueryWrapper knowledgeWrapper = QueryWrapper.create()
|
||||
.select(SysApiKeyResourceMapping::getId)
|
||||
.eq(SysApiKeyResourceMapping::getApiKeyId, entity.getId())
|
||||
.eq(SysApiKeyResourceMapping::getResourceType, "KNOWLEDGE");
|
||||
entity.setKnowledgeShareEnabled(sysApiKeyResourceMappingService.count(knowledgeWrapper) > 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package tech.easyflow.admin.controller.ai;
|
||||
|
||||
import com.mybatisflex.core.paginate.Page;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.testng.Assert;
|
||||
import org.testng.annotations.BeforeMethod;
|
||||
import org.testng.annotations.Test;
|
||||
import tech.easyflow.admin.controller.ai.support.AiResourceCreatorNameSupport;
|
||||
import tech.easyflow.ai.entity.Bot;
|
||||
import tech.easyflow.ai.service.AiResourceApprovalStateService;
|
||||
import tech.easyflow.ai.service.BotDocumentCollectionService;
|
||||
import tech.easyflow.ai.service.BotMessageService;
|
||||
import tech.easyflow.ai.service.BotService;
|
||||
import tech.easyflow.ai.service.BotWorkflowService;
|
||||
import tech.easyflow.ai.service.ModelService;
|
||||
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
|
||||
import tech.easyflow.system.service.CategoryPermissionService;
|
||||
import tech.easyflow.system.service.SysAccountService;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doNothing;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* {@link BotController} 测试。
|
||||
*/
|
||||
public class BotControllerTest {
|
||||
|
||||
private BotService botService;
|
||||
private ModelService modelService;
|
||||
private BotWorkflowService botWorkflowService;
|
||||
private BotDocumentCollectionService botDocumentCollectionService;
|
||||
private BotMessageService botMessageService;
|
||||
private CategoryPermissionService categoryPermissionService;
|
||||
private AiResourceApprovalStateService aiResourceApprovalStateService;
|
||||
private SysAccountService sysAccountService;
|
||||
|
||||
/**
|
||||
* 初始化测试 mock。
|
||||
*/
|
||||
@BeforeMethod
|
||||
public void setUp() {
|
||||
botService = mock(BotService.class);
|
||||
modelService = mock(ModelService.class);
|
||||
botWorkflowService = mock(BotWorkflowService.class);
|
||||
botDocumentCollectionService = mock(BotDocumentCollectionService.class);
|
||||
botMessageService = mock(BotMessageService.class);
|
||||
categoryPermissionService = mock(CategoryPermissionService.class);
|
||||
aiResourceApprovalStateService = mock(AiResourceApprovalStateService.class);
|
||||
sysAccountService = mock(SysAccountService.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证分页结果会补充创建人展示名称,且只做一次批量账号查询。
|
||||
*/
|
||||
@Test
|
||||
public void shouldFillCreatedByNameForPageRecords() {
|
||||
TestBotController controller = new TestBotController(
|
||||
botService,
|
||||
modelService,
|
||||
botWorkflowService,
|
||||
botDocumentCollectionService,
|
||||
botMessageService
|
||||
);
|
||||
AiResourceCreatorNameSupport creatorNameSupport = new AiResourceCreatorNameSupport();
|
||||
setField(creatorNameSupport, "sysAccountService", sysAccountService);
|
||||
setField(controller, "categoryPermissionService", categoryPermissionService);
|
||||
setField(controller, "aiResourceApprovalStateService", aiResourceApprovalStateService);
|
||||
setField(controller, "aiResourceCreatorNameSupport", creatorNameSupport);
|
||||
|
||||
Bot bot = new Bot();
|
||||
bot.setId(BigInteger.valueOf(101));
|
||||
bot.setCreatedBy(BigInteger.valueOf(7));
|
||||
Page<Bot> page = new Page<>(Collections.singletonList(bot), 1, 10, 1);
|
||||
|
||||
when(categoryPermissionService.getCurrentAccess("BOT"))
|
||||
.thenReturn(new RoleCategoryAccessSnapshot("BOT", BigInteger.ONE, false, true, Collections.emptySet()));
|
||||
when(botService.page(any(Page.class), any(QueryWrapper.class))).thenReturn(page);
|
||||
when(sysAccountService.resolveDisplayNameMap(Collections.singleton(BigInteger.valueOf(7))))
|
||||
.thenReturn(Map.of(BigInteger.valueOf(7), "管理员"));
|
||||
doNothing().when(aiResourceApprovalStateService).fillBotApprovalState(page.getRecords());
|
||||
|
||||
Page<Bot> result = controller.invokeQueryPage(new Page<>(1, 10), QueryWrapper.create());
|
||||
|
||||
Assert.assertEquals(result.getRecords().get(0).getCreatedByName(), "管理员");
|
||||
verify(sysAccountService).resolveDisplayNameMap(Collections.singleton(BigInteger.valueOf(7)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过反射设置字段值。
|
||||
*
|
||||
* @param target 目标对象
|
||||
* @param fieldName 字段名
|
||||
* @param value 字段值
|
||||
*/
|
||||
private static void setField(Object target, String fieldName, Object value) {
|
||||
Class<?> current = target.getClass();
|
||||
while (current != null) {
|
||||
try {
|
||||
java.lang.reflect.Field field = current.getDeclaredField(fieldName);
|
||||
field.setAccessible(true);
|
||||
field.set(target, value);
|
||||
return;
|
||||
} catch (NoSuchFieldException ignored) {
|
||||
current = current.getSuperclass();
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new IllegalStateException("设置测试字段失败: " + fieldName, e);
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("未找到字段: " + fieldName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 暴露受保护分页方法的测试控制器。
|
||||
*/
|
||||
private static class TestBotController extends BotController {
|
||||
|
||||
TestBotController(
|
||||
BotService service,
|
||||
ModelService modelService,
|
||||
BotWorkflowService botWorkflowService,
|
||||
BotDocumentCollectionService botDocumentCollectionService,
|
||||
BotMessageService botMessageService
|
||||
) {
|
||||
super(service, modelService, botWorkflowService, botDocumentCollectionService, botMessageService);
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用受保护的分页查询方法。
|
||||
*
|
||||
* @param page 分页对象
|
||||
* @param queryWrapper 查询条件
|
||||
* @return 查询结果
|
||||
*/
|
||||
Page<Bot> invokeQueryPage(Page<Bot> page, QueryWrapper queryWrapper) {
|
||||
return super.queryPage(page, queryWrapper);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,660 @@
|
||||
package tech.easyflow.publicapi.controller;
|
||||
|
||||
import cn.hutool.core.io.IoUtil;
|
||||
import com.easyagents.core.model.embedding.EmbeddingModel;
|
||||
import com.easyagents.core.store.DocumentStore;
|
||||
import com.easyagents.core.store.StoreOptions;
|
||||
import com.easyagents.core.store.StoreResult;
|
||||
import com.mybatisflex.core.paginate.Page;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import tech.easyflow.ai.documentimport.DocumentImportDtos;
|
||||
import tech.easyflow.ai.dto.KnowledgeSearchResultItem;
|
||||
import tech.easyflow.ai.entity.Document;
|
||||
import tech.easyflow.ai.entity.DocumentChunk;
|
||||
import tech.easyflow.ai.entity.DocumentCollection;
|
||||
import tech.easyflow.ai.entity.FaqItem;
|
||||
import tech.easyflow.ai.entity.Model;
|
||||
import tech.easyflow.ai.enums.KnowledgeShareActionScope;
|
||||
import tech.easyflow.ai.rag.KnowledgeRetrievalModes;
|
||||
import tech.easyflow.ai.rag.KnowledgeRetrievalRequest;
|
||||
import tech.easyflow.ai.service.DocumentChunkService;
|
||||
import tech.easyflow.ai.service.DocumentCollectionService;
|
||||
import tech.easyflow.ai.service.DocumentService;
|
||||
import tech.easyflow.ai.service.FaqCategoryService;
|
||||
import tech.easyflow.ai.service.FaqItemService;
|
||||
import tech.easyflow.ai.service.KnowledgeShareAuditService;
|
||||
import tech.easyflow.ai.service.KnowledgeSharePermissionService;
|
||||
import tech.easyflow.ai.service.ModelService;
|
||||
import tech.easyflow.ai.service.impl.KnowledgeSharePermissionServiceImpl;
|
||||
import tech.easyflow.ai.vo.FaqImportResultVo;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.filestorage.FileStorageService;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||
import tech.easyflow.system.entity.SysApiKey;
|
||||
import tech.easyflow.system.service.SysApiKeyService;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.io.InputStream;
|
||||
import java.math.BigInteger;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 知识库 API 分享接口。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping(value = "/public-api/knowledge-share", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public class PublicKnowledgeShareController {
|
||||
|
||||
@Resource
|
||||
private SysApiKeyService sysApiKeyService;
|
||||
@Resource
|
||||
private KnowledgeSharePermissionService knowledgeSharePermissionService;
|
||||
@Resource
|
||||
private KnowledgeShareAuditService knowledgeShareAuditService;
|
||||
@Resource
|
||||
private DocumentCollectionService documentCollectionService;
|
||||
@Resource
|
||||
private DocumentService documentService;
|
||||
@Resource
|
||||
private DocumentChunkService documentChunkService;
|
||||
@Resource
|
||||
private FaqItemService faqItemService;
|
||||
@Resource
|
||||
private FaqCategoryService faqCategoryService;
|
||||
@Resource
|
||||
private ModelService modelService;
|
||||
@Resource(name = "default")
|
||||
private FileStorageService fileStorageService;
|
||||
|
||||
/**
|
||||
* 获取知识库详情。
|
||||
*/
|
||||
@GetMapping("/detail")
|
||||
public Result<DocumentCollection> detail(
|
||||
@RequestHeader("ApiKey") String apiKey,
|
||||
@RequestParam BigInteger knowledgeId,
|
||||
HttpServletRequest request
|
||||
) {
|
||||
assertApiShare(apiKey, request.getRequestURI(), knowledgeId, KnowledgeShareActionScope.VIEW.name());
|
||||
audit(apiKey, "API读取知识库详情", "KNOWLEDGE_API_SHARE_ACCESS", request.getRequestURI(), Map.of("knowledgeId", knowledgeId));
|
||||
return Result.ok(documentCollectionService.getDetail(knowledgeId.toString()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 检索知识库。
|
||||
*/
|
||||
@GetMapping("/search")
|
||||
public Result<List<KnowledgeSearchResultItem>> search(
|
||||
@RequestHeader("ApiKey") String apiKey,
|
||||
@RequestParam BigInteger knowledgeId,
|
||||
@RequestParam String keyword,
|
||||
@RequestParam(required = false) String retrievalMode,
|
||||
HttpServletRequest request
|
||||
) {
|
||||
assertApiShare(apiKey, request.getRequestURI(), knowledgeId, KnowledgeShareActionScope.SEARCH.name());
|
||||
KnowledgeRetrievalRequest retrievalRequest = new KnowledgeRetrievalRequest();
|
||||
retrievalRequest.setKnowledgeId(knowledgeId);
|
||||
retrievalRequest.setQuery(keyword);
|
||||
retrievalRequest.setRetrievalMode(KnowledgeRetrievalModes.parse(retrievalMode));
|
||||
retrievalRequest.setCallerType("PUBLIC_API");
|
||||
retrievalRequest.setCallerId(String.valueOf(knowledgeId));
|
||||
audit(apiKey, "API检索知识库", "KNOWLEDGE_API_SHARE_ACCESS", request.getRequestURI(), Map.of("knowledgeId", knowledgeId));
|
||||
return Result.ok(toKnowledgeSearchResult(documentCollectionService.search(retrievalRequest)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 文档分页。
|
||||
*/
|
||||
@GetMapping("/document/page")
|
||||
public Result<Page<Document>> documentPage(
|
||||
@RequestHeader("ApiKey") String apiKey,
|
||||
@RequestParam BigInteger knowledgeId,
|
||||
@RequestParam(required = false) String title,
|
||||
@RequestParam(defaultValue = "10") int pageSize,
|
||||
@RequestParam(defaultValue = "1") int pageNumber,
|
||||
HttpServletRequest request
|
||||
) {
|
||||
assertApiShare(apiKey, request.getRequestURI(), knowledgeId, KnowledgeShareActionScope.VIEW.name());
|
||||
requireDocumentKnowledge(knowledgeId);
|
||||
return Result.ok(documentService.getDocumentList(knowledgeId.toString(), pageSize, pageNumber, title));
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载文档。
|
||||
*/
|
||||
@GetMapping(value = "/document/download", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
|
||||
public void documentDownload(
|
||||
@RequestHeader("ApiKey") String apiKey,
|
||||
@RequestParam BigInteger knowledgeId,
|
||||
@RequestParam BigInteger documentId,
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response
|
||||
) throws Exception {
|
||||
assertApiShare(apiKey, request.getRequestURI(), knowledgeId, KnowledgeShareActionScope.VIEW.name());
|
||||
requireDocumentKnowledge(knowledgeId);
|
||||
Document document = requireDocument(documentId, knowledgeId);
|
||||
response.setContentType("application/octet-stream");
|
||||
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
|
||||
String fileName = URLEncoder.encode(document.getTitle(), StandardCharsets.UTF_8).replaceAll("\\+", "%20");
|
||||
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName);
|
||||
try (InputStream inputStream = fileStorageService.readStream(document.getDocumentPath())) {
|
||||
IoUtil.copy(inputStream, response.getOutputStream());
|
||||
response.flushBuffer();
|
||||
}
|
||||
audit(apiKey, "API下载文档", "KNOWLEDGE_API_SHARE_ACCESS", request.getRequestURI(), Map.of("knowledgeId", knowledgeId, "documentId", documentId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文档。
|
||||
*/
|
||||
@PostMapping("/document/remove")
|
||||
public Result<?> removeDocument(
|
||||
@RequestHeader("ApiKey") String apiKey,
|
||||
@RequestParam BigInteger knowledgeId,
|
||||
@JsonBody("id") String id,
|
||||
HttpServletRequest request
|
||||
) {
|
||||
assertApiShare(apiKey, request.getRequestURI(), knowledgeId, KnowledgeShareActionScope.CONTENT_DELETE.name());
|
||||
requireDocumentKnowledge(knowledgeId);
|
||||
requireDocument(new BigInteger(id), knowledgeId);
|
||||
audit(apiKey, "API删除文档", "KNOWLEDGE_API_SHARE_WRITE", request.getRequestURI(), Map.of("knowledgeId", knowledgeId, "documentId", id));
|
||||
return Result.ok(documentService.removeDoc(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 文档导入分析。
|
||||
*/
|
||||
@PostMapping("/document/import/analyze")
|
||||
public Result<DocumentImportDtos.AnalyzeResponse> analyzeImport(
|
||||
@RequestHeader("ApiKey") String apiKey,
|
||||
@JsonBody DocumentImportDtos.AnalyzeRequest request,
|
||||
HttpServletRequest servletRequest
|
||||
) {
|
||||
assertApiShare(apiKey, servletRequest.getRequestURI(), request.getKnowledgeId(), KnowledgeShareActionScope.CONTENT_CREATE.name());
|
||||
requireDocumentKnowledge(request.getKnowledgeId());
|
||||
audit(apiKey, "API分析文档导入", "KNOWLEDGE_API_SHARE_WRITE", servletRequest.getRequestURI(), Map.of("knowledgeId", request.getKnowledgeId()));
|
||||
return documentService.analyzeImport(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 文档导入预览。
|
||||
*/
|
||||
@PostMapping("/document/import/preview")
|
||||
public Result<DocumentImportDtos.PreviewResponse> previewImport(
|
||||
@RequestHeader("ApiKey") String apiKey,
|
||||
@JsonBody DocumentImportDtos.PreviewRequest request,
|
||||
HttpServletRequest servletRequest
|
||||
) {
|
||||
assertApiShare(apiKey, servletRequest.getRequestURI(), request.getKnowledgeId(), KnowledgeShareActionScope.CONTENT_CREATE.name());
|
||||
requireDocumentKnowledge(request.getKnowledgeId());
|
||||
audit(apiKey, "API预览文档导入", "KNOWLEDGE_API_SHARE_WRITE", servletRequest.getRequestURI(), Map.of("knowledgeId", request.getKnowledgeId()));
|
||||
return documentService.previewImport(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 文档导入提交。
|
||||
*/
|
||||
@PostMapping("/document/import/commit")
|
||||
public Result<DocumentImportDtos.CommitResponse> commitImport(
|
||||
@RequestHeader("ApiKey") String apiKey,
|
||||
@JsonBody DocumentImportDtos.CommitRequest request,
|
||||
HttpServletRequest servletRequest
|
||||
) {
|
||||
assertApiShare(apiKey, servletRequest.getRequestURI(), request.getKnowledgeId(), KnowledgeShareActionScope.CONTENT_CREATE.name());
|
||||
requireDocumentKnowledge(request.getKnowledgeId());
|
||||
audit(apiKey, "API提交文档导入", "KNOWLEDGE_API_SHARE_WRITE", servletRequest.getRequestURI(), Map.of("knowledgeId", request.getKnowledgeId()));
|
||||
return documentService.commitImport(request);
|
||||
}
|
||||
|
||||
@PostMapping("/document/import/task/create")
|
||||
public Result<DocumentImportDtos.TaskCreateResponse> createImportTask(
|
||||
@RequestHeader("ApiKey") String apiKey,
|
||||
@JsonBody DocumentImportDtos.TaskCreateRequest request,
|
||||
HttpServletRequest servletRequest
|
||||
) {
|
||||
assertApiShare(apiKey, servletRequest.getRequestURI(), request.getKnowledgeId(), KnowledgeShareActionScope.CONTENT_CREATE.name());
|
||||
requireDocumentKnowledge(request.getKnowledgeId());
|
||||
audit(apiKey, "API创建文档导入任务", "KNOWLEDGE_API_SHARE_WRITE", servletRequest.getRequestURI(), Map.of("knowledgeId", request.getKnowledgeId()));
|
||||
return documentService.createImportTask(request);
|
||||
}
|
||||
|
||||
@GetMapping("/document/import/task/detail")
|
||||
public Result<DocumentImportDtos.TaskDetailResponse> getImportTaskDetail(
|
||||
@RequestHeader("ApiKey") String apiKey,
|
||||
@RequestParam BigInteger knowledgeId,
|
||||
@RequestParam BigInteger taskId,
|
||||
HttpServletRequest request
|
||||
) {
|
||||
assertApiShare(apiKey, request.getRequestURI(), knowledgeId, KnowledgeShareActionScope.VIEW.name());
|
||||
requireDocumentKnowledge(knowledgeId);
|
||||
Result<DocumentImportDtos.TaskDetailResponse> result = documentService.getImportTaskDetail(taskId);
|
||||
if (result.getData() == null || result.getData().getKnowledgeId() == null
|
||||
|| result.getData().getKnowledgeId().compareTo(knowledgeId) != 0) {
|
||||
throw new BusinessException("任务不存在");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@PostMapping("/document/import/task/preview")
|
||||
public Result<DocumentImportDtos.PreviewResponse> previewImportTask(
|
||||
@RequestHeader("ApiKey") String apiKey,
|
||||
@JsonBody DocumentImportDtos.PreviewRequest request,
|
||||
HttpServletRequest servletRequest
|
||||
) {
|
||||
assertApiShare(apiKey, servletRequest.getRequestURI(), request.getKnowledgeId(), KnowledgeShareActionScope.CONTENT_CREATE.name());
|
||||
requireDocumentKnowledge(request.getKnowledgeId());
|
||||
audit(apiKey, "API预览文档分块", "KNOWLEDGE_API_SHARE_WRITE", servletRequest.getRequestURI(), Map.of("knowledgeId", request.getKnowledgeId()));
|
||||
return documentService.previewImportTask(request);
|
||||
}
|
||||
|
||||
@PostMapping("/document/import/task/startIndex")
|
||||
public Result<DocumentImportDtos.TaskStartIndexResponse> startIndexTask(
|
||||
@RequestHeader("ApiKey") String apiKey,
|
||||
@JsonBody DocumentImportDtos.TaskStartIndexRequest request,
|
||||
HttpServletRequest servletRequest
|
||||
) {
|
||||
assertApiShare(apiKey, servletRequest.getRequestURI(), request.getKnowledgeId(), KnowledgeShareActionScope.CONTENT_CREATE.name());
|
||||
requireDocumentKnowledge(request.getKnowledgeId());
|
||||
audit(apiKey, "API启动文档向量化", "KNOWLEDGE_API_SHARE_WRITE", servletRequest.getRequestURI(), Map.of("knowledgeId", request.getKnowledgeId()));
|
||||
return documentService.startIndexTask(request);
|
||||
}
|
||||
|
||||
@PostMapping("/document/import/task/retryParse")
|
||||
public Result<DocumentImportDtos.TaskStartIndexResponse> retryParseTask(
|
||||
@RequestHeader("ApiKey") String apiKey,
|
||||
@JsonBody DocumentImportDtos.TaskRetryRequest request,
|
||||
HttpServletRequest servletRequest
|
||||
) {
|
||||
assertApiShare(apiKey, servletRequest.getRequestURI(), request.getKnowledgeId(), KnowledgeShareActionScope.CONTENT_CREATE.name());
|
||||
requireDocumentKnowledge(request.getKnowledgeId());
|
||||
audit(apiKey, "API重试文档解析", "KNOWLEDGE_API_SHARE_WRITE", servletRequest.getRequestURI(), Map.of("knowledgeId", request.getKnowledgeId()));
|
||||
return documentService.retryParseTask(request);
|
||||
}
|
||||
|
||||
@PostMapping("/document/import/task/retryIndex")
|
||||
public Result<DocumentImportDtos.TaskStartIndexResponse> retryIndexTask(
|
||||
@RequestHeader("ApiKey") String apiKey,
|
||||
@JsonBody DocumentImportDtos.TaskRetryRequest request,
|
||||
HttpServletRequest servletRequest
|
||||
) {
|
||||
assertApiShare(apiKey, servletRequest.getRequestURI(), request.getKnowledgeId(), KnowledgeShareActionScope.CONTENT_CREATE.name());
|
||||
requireDocumentKnowledge(request.getKnowledgeId());
|
||||
audit(apiKey, "API重试文档向量化", "KNOWLEDGE_API_SHARE_WRITE", servletRequest.getRequestURI(), Map.of("knowledgeId", request.getKnowledgeId()));
|
||||
return documentService.retryIndexTask(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Chunk 分页。
|
||||
*/
|
||||
@GetMapping("/documentChunk/page")
|
||||
public Result<Page<DocumentChunk>> documentChunkPage(
|
||||
@RequestHeader("ApiKey") String apiKey,
|
||||
@RequestParam BigInteger knowledgeId,
|
||||
@RequestParam BigInteger documentId,
|
||||
@RequestParam(defaultValue = "1") long pageNumber,
|
||||
@RequestParam(defaultValue = "10") long pageSize,
|
||||
HttpServletRequest request
|
||||
) {
|
||||
assertApiShare(apiKey, request.getRequestURI(), knowledgeId, KnowledgeShareActionScope.VIEW.name());
|
||||
requireDocumentKnowledge(knowledgeId);
|
||||
requireDocument(documentId, knowledgeId);
|
||||
QueryWrapper wrapper = QueryWrapper.create()
|
||||
.eq(DocumentChunk::getDocumentId, documentId)
|
||||
.orderBy("sorting asc");
|
||||
return Result.ok(documentChunkService.page(new Page<>(pageNumber, pageSize), wrapper));
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 Chunk。
|
||||
*/
|
||||
@PostMapping("/documentChunk/update")
|
||||
public Result<?> updateDocumentChunk(
|
||||
@RequestHeader("ApiKey") String apiKey,
|
||||
@RequestParam BigInteger knowledgeId,
|
||||
@JsonBody DocumentChunk documentChunk,
|
||||
HttpServletRequest request
|
||||
) {
|
||||
assertApiShare(apiKey, request.getRequestURI(), knowledgeId, KnowledgeShareActionScope.CONTENT_UPDATE.name());
|
||||
requireDocumentKnowledge(knowledgeId);
|
||||
DocumentChunk current = requireDocumentChunk(documentChunk.getId(), knowledgeId);
|
||||
boolean success = documentChunkService.updateById(documentChunk);
|
||||
if (success) {
|
||||
DocumentCollection knowledge = documentCollectionService.getById(knowledgeId);
|
||||
DocumentStore documentStore = knowledge.toDocumentStore();
|
||||
if (documentStore == null) {
|
||||
return Result.fail(2, "知识库没有配置向量库");
|
||||
}
|
||||
Model model = modelService.getModelInstance(knowledge.getVectorEmbedModelId());
|
||||
if (model == null) {
|
||||
return Result.fail(3, "知识库没有配置向量模型");
|
||||
}
|
||||
EmbeddingModel embeddingModel = model.toEmbeddingModel();
|
||||
documentStore.setEmbeddingModel(embeddingModel);
|
||||
StoreOptions options = StoreOptions.ofCollectionName(knowledge.getVectorStoreCollection());
|
||||
com.easyagents.core.document.Document doc = com.easyagents.core.document.Document.of(documentChunk.getContent());
|
||||
doc.setId(current.getId());
|
||||
StoreResult result = documentStore.update(doc, options);
|
||||
audit(apiKey, "API更新文档 Chunk", "KNOWLEDGE_API_SHARE_WRITE", request.getRequestURI(), Map.of("knowledgeId", knowledgeId, "chunkId", documentChunk.getId()));
|
||||
return Result.ok(result);
|
||||
}
|
||||
return Result.ok(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除 Chunk。
|
||||
*/
|
||||
@PostMapping("/documentChunk/remove")
|
||||
public Result<?> removeDocumentChunk(
|
||||
@RequestHeader("ApiKey") String apiKey,
|
||||
@RequestParam BigInteger knowledgeId,
|
||||
@JsonBody("id") BigInteger chunkId,
|
||||
HttpServletRequest request
|
||||
) {
|
||||
assertApiShare(apiKey, request.getRequestURI(), knowledgeId, KnowledgeShareActionScope.CONTENT_DELETE.name());
|
||||
requireDocumentKnowledge(knowledgeId);
|
||||
requireDocumentChunk(chunkId, knowledgeId);
|
||||
DocumentCollection knowledge = documentCollectionService.getById(knowledgeId);
|
||||
DocumentStore documentStore = knowledge.toDocumentStore();
|
||||
if (documentStore == null) {
|
||||
return Result.fail(2, "知识库没有配置向量库");
|
||||
}
|
||||
Model model = modelService.getModelInstance(knowledge.getVectorEmbedModelId());
|
||||
if (model == null) {
|
||||
return Result.fail(3, "知识库没有配置向量模型");
|
||||
}
|
||||
documentStore.setEmbeddingModel(model.toEmbeddingModel());
|
||||
StoreOptions options = StoreOptions.ofCollectionName(knowledge.getVectorStoreCollection());
|
||||
documentStore.delete(Collections.singletonList(chunkId), options);
|
||||
documentChunkService.removeById(chunkId);
|
||||
audit(apiKey, "API删除文档 Chunk", "KNOWLEDGE_API_SHARE_WRITE", request.getRequestURI(), Map.of("knowledgeId", knowledgeId, "chunkId", chunkId));
|
||||
return Result.ok(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* FAQ 分页。
|
||||
*/
|
||||
@GetMapping("/faq/page")
|
||||
public Result<Page<FaqItem>> faqPage(
|
||||
@RequestHeader("ApiKey") String apiKey,
|
||||
@RequestParam BigInteger knowledgeId,
|
||||
@RequestParam(required = false) String question,
|
||||
@RequestParam(required = false) String categoryId,
|
||||
@RequestParam(defaultValue = "1") long pageNumber,
|
||||
@RequestParam(defaultValue = "10") long pageSize,
|
||||
HttpServletRequest request
|
||||
) {
|
||||
assertApiShare(apiKey, request.getRequestURI(), knowledgeId, KnowledgeShareActionScope.VIEW.name());
|
||||
requireFaqKnowledge(knowledgeId);
|
||||
faqCategoryService.ensureDefaultCategory(knowledgeId);
|
||||
QueryWrapper queryWrapper = QueryWrapper.create()
|
||||
.eq(FaqItem::getCollectionId, knowledgeId);
|
||||
if (question != null && !question.isBlank()) {
|
||||
queryWrapper.like(FaqItem::getQuestion, question.trim());
|
||||
}
|
||||
if (categoryId != null && !categoryId.isBlank()) {
|
||||
List<BigInteger> descendantIds = faqCategoryService.findDescendantIds(knowledgeId, new BigInteger(categoryId));
|
||||
if (descendantIds.isEmpty()) {
|
||||
queryWrapper.eq(FaqItem::getId, BigInteger.ZERO);
|
||||
} else {
|
||||
queryWrapper.in(FaqItem::getCategoryId, descendantIds);
|
||||
}
|
||||
}
|
||||
queryWrapper.orderBy("order_no asc");
|
||||
Page<FaqItem> page = faqItemService.page(new Page<>(pageNumber, pageSize), queryWrapper);
|
||||
Map<BigInteger, String> pathMap = faqCategoryService.buildPathMap(knowledgeId);
|
||||
if (page.getRecords() != null) {
|
||||
for (FaqItem record : page.getRecords()) {
|
||||
record.setCategoryPath(pathMap.get(record.getCategoryId()));
|
||||
}
|
||||
}
|
||||
return Result.ok(page);
|
||||
}
|
||||
|
||||
/**
|
||||
* FAQ 详情。
|
||||
*/
|
||||
@GetMapping("/faq/detail")
|
||||
public Result<FaqItem> faqDetail(
|
||||
@RequestHeader("ApiKey") String apiKey,
|
||||
@RequestParam BigInteger knowledgeId,
|
||||
@RequestParam String id,
|
||||
HttpServletRequest request
|
||||
) {
|
||||
assertApiShare(apiKey, request.getRequestURI(), knowledgeId, KnowledgeShareActionScope.VIEW.name());
|
||||
requireFaqKnowledge(knowledgeId);
|
||||
FaqItem faqItem = requireFaq(new BigInteger(id), knowledgeId);
|
||||
return Result.ok(faqItem);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增 FAQ。
|
||||
*/
|
||||
@PostMapping("/faq/save")
|
||||
public Result<?> saveFaq(
|
||||
@RequestHeader("ApiKey") String apiKey,
|
||||
@JsonBody FaqItem entity,
|
||||
HttpServletRequest request
|
||||
) {
|
||||
assertApiShare(apiKey, request.getRequestURI(), entity.getCollectionId(), KnowledgeShareActionScope.CONTENT_CREATE.name());
|
||||
requireFaqKnowledge(entity.getCollectionId());
|
||||
audit(apiKey, "API新增FAQ", "KNOWLEDGE_API_SHARE_WRITE", request.getRequestURI(), Map.of("knowledgeId", entity.getCollectionId()));
|
||||
return Result.ok(faqItemService.saveFaqItem(entity));
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 FAQ。
|
||||
*/
|
||||
@PostMapping("/faq/update")
|
||||
public Result<?> updateFaq(
|
||||
@RequestHeader("ApiKey") String apiKey,
|
||||
@RequestParam BigInteger knowledgeId,
|
||||
@JsonBody FaqItem entity,
|
||||
HttpServletRequest request
|
||||
) {
|
||||
assertApiShare(apiKey, request.getRequestURI(), knowledgeId, KnowledgeShareActionScope.CONTENT_UPDATE.name());
|
||||
requireFaqKnowledge(knowledgeId);
|
||||
requireFaq(entity.getId(), knowledgeId);
|
||||
audit(apiKey, "API更新FAQ", "KNOWLEDGE_API_SHARE_WRITE", request.getRequestURI(), Map.of("knowledgeId", knowledgeId, "faqId", entity.getId()));
|
||||
return Result.ok(faqItemService.updateFaqItem(entity));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除 FAQ。
|
||||
*/
|
||||
@PostMapping("/faq/remove")
|
||||
public Result<?> removeFaq(
|
||||
@RequestHeader("ApiKey") String apiKey,
|
||||
@RequestParam BigInteger knowledgeId,
|
||||
@JsonBody("id") BigInteger id,
|
||||
HttpServletRequest request
|
||||
) {
|
||||
assertApiShare(apiKey, request.getRequestURI(), knowledgeId, KnowledgeShareActionScope.CONTENT_DELETE.name());
|
||||
requireFaqKnowledge(knowledgeId);
|
||||
requireFaq(id, knowledgeId);
|
||||
audit(apiKey, "API删除FAQ", "KNOWLEDGE_API_SHARE_WRITE", request.getRequestURI(), Map.of("knowledgeId", knowledgeId, "faqId", id));
|
||||
return Result.ok(faqItemService.removeFaqItem(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入 FAQ Excel。
|
||||
*/
|
||||
@PostMapping(value = "/faq/importExcel", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
public Result<FaqImportResultVo> importFaqExcel(
|
||||
@RequestHeader("ApiKey") String apiKey,
|
||||
MultipartFile file,
|
||||
BigInteger collectionId,
|
||||
HttpServletRequest request
|
||||
) {
|
||||
assertApiShare(apiKey, request.getRequestURI(), collectionId, KnowledgeShareActionScope.IMPORT_EXPORT.name());
|
||||
requireFaqKnowledge(collectionId);
|
||||
audit(apiKey, "API导入FAQ Excel", "KNOWLEDGE_API_SHARE_WRITE", request.getRequestURI(), Map.of("knowledgeId", collectionId));
|
||||
return Result.ok(faqItemService.importFromExcel(collectionId, file));
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载 FAQ 导入模板。
|
||||
*/
|
||||
@GetMapping(value = "/faq/downloadImportTemplate", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
|
||||
public void downloadFaqImportTemplate(
|
||||
@RequestHeader("ApiKey") String apiKey,
|
||||
@RequestParam BigInteger knowledgeId,
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response
|
||||
) throws Exception {
|
||||
assertApiShare(apiKey, request.getRequestURI(), knowledgeId, KnowledgeShareActionScope.IMPORT_EXPORT.name());
|
||||
requireFaqKnowledge(knowledgeId);
|
||||
response.setContentType("application/octet-stream");
|
||||
response.setHeader(
|
||||
"Content-disposition",
|
||||
"attachment;filename*=utf-8''" + URLEncoder.encode("faq_import_template.xlsx", StandardCharsets.UTF_8)
|
||||
);
|
||||
faqItemService.writeImportTemplate(response.getOutputStream());
|
||||
response.flushBuffer();
|
||||
audit(apiKey, "API下载FAQ导入模板", "KNOWLEDGE_API_SHARE_ACCESS", request.getRequestURI(), Map.of("knowledgeId", knowledgeId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出 FAQ Excel。
|
||||
*/
|
||||
@GetMapping(value = "/faq/exportExcel", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
|
||||
public void exportFaqExcel(
|
||||
@RequestHeader("ApiKey") String apiKey,
|
||||
@RequestParam BigInteger knowledgeId,
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response
|
||||
) throws Exception {
|
||||
assertApiShare(apiKey, request.getRequestURI(), knowledgeId, KnowledgeShareActionScope.IMPORT_EXPORT.name());
|
||||
requireFaqKnowledge(knowledgeId);
|
||||
String fileName = "faq_export_" + new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()) + ".xlsx";
|
||||
response.setContentType("application/octet-stream");
|
||||
response.setHeader(
|
||||
"Content-disposition",
|
||||
"attachment;filename*=utf-8''" + URLEncoder.encode(fileName, StandardCharsets.UTF_8)
|
||||
);
|
||||
faqItemService.exportToExcel(knowledgeId, response.getOutputStream());
|
||||
response.flushBuffer();
|
||||
audit(apiKey, "API导出FAQ Excel", "KNOWLEDGE_API_SHARE_ACCESS", request.getRequestURI(), Map.of("knowledgeId", knowledgeId));
|
||||
}
|
||||
|
||||
private void assertApiShare(String apiKey, String requestUri, BigInteger knowledgeId, String actionScope) {
|
||||
SysApiKey sysApiKey = sysApiKeyService.getSysApiKey(apiKey);
|
||||
knowledgeSharePermissionService.assertApiShare(sysApiKey.getId(), requestUri, knowledgeId, actionScope);
|
||||
}
|
||||
|
||||
/**
|
||||
* 断言知识库为文档类型。
|
||||
*
|
||||
* @param knowledgeId 知识库ID
|
||||
* @return 知识库实体
|
||||
*/
|
||||
private DocumentCollection requireDocumentKnowledge(BigInteger knowledgeId) {
|
||||
DocumentCollection knowledge = requireKnowledge(knowledgeId);
|
||||
if (!knowledge.isDocumentCollection()) {
|
||||
throw new BusinessException("当前知识库类型不支持文档接口");
|
||||
}
|
||||
return knowledge;
|
||||
}
|
||||
|
||||
/**
|
||||
* 断言知识库为 FAQ 类型。
|
||||
*
|
||||
* @param knowledgeId 知识库ID
|
||||
* @return 知识库实体
|
||||
*/
|
||||
private DocumentCollection requireFaqKnowledge(BigInteger knowledgeId) {
|
||||
DocumentCollection knowledge = requireKnowledge(knowledgeId);
|
||||
if (!knowledge.isFaqCollection()) {
|
||||
throw new BusinessException("当前知识库类型不支持FAQ接口");
|
||||
}
|
||||
return knowledge;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取知识库并保证存在。
|
||||
*
|
||||
* @param knowledgeId 知识库ID
|
||||
* @return 知识库实体
|
||||
*/
|
||||
private DocumentCollection requireKnowledge(BigInteger knowledgeId) {
|
||||
DocumentCollection knowledge = documentCollectionService.getById(knowledgeId);
|
||||
if (knowledge == null) {
|
||||
throw new BusinessException("知识库不存在");
|
||||
}
|
||||
return knowledge;
|
||||
}
|
||||
|
||||
private Document requireDocument(BigInteger documentId, BigInteger knowledgeId) {
|
||||
Document document = documentService.getById(documentId);
|
||||
if (document == null || document.getCollectionId() == null || document.getCollectionId().compareTo(knowledgeId) != 0) {
|
||||
throw new BusinessException("文档不存在");
|
||||
}
|
||||
return document;
|
||||
}
|
||||
|
||||
private DocumentChunk requireDocumentChunk(BigInteger chunkId, BigInteger knowledgeId) {
|
||||
DocumentChunk chunk = documentChunkService.getById(chunkId);
|
||||
if (chunk == null || chunk.getDocumentCollectionId() == null || chunk.getDocumentCollectionId().compareTo(knowledgeId) != 0) {
|
||||
throw new BusinessException("记录不存在");
|
||||
}
|
||||
return chunk;
|
||||
}
|
||||
|
||||
private FaqItem requireFaq(BigInteger faqId, BigInteger knowledgeId) {
|
||||
FaqItem faqItem = faqItemService.getById(faqId);
|
||||
if (faqItem == null || faqItem.getCollectionId() == null || faqItem.getCollectionId().compareTo(knowledgeId) != 0) {
|
||||
throw new BusinessException("FAQ不存在");
|
||||
}
|
||||
return faqItem;
|
||||
}
|
||||
|
||||
private void audit(String apiKey, String actionName, String actionType, String actionUrl, Map<String, Object> detail) {
|
||||
SysApiKey sysApiKey = sysApiKeyService.getSysApiKey(apiKey);
|
||||
Map<String, Object> payload = new HashMap<>(detail);
|
||||
payload.put("apiKeyId", sysApiKey.getId());
|
||||
payload.put("channel", "API");
|
||||
knowledgeShareAuditService.log(null, actionName, actionType, actionUrl, payload);
|
||||
}
|
||||
|
||||
private List<KnowledgeSearchResultItem> toKnowledgeSearchResult(List<com.easyagents.core.document.Document> documents) {
|
||||
List<KnowledgeSearchResultItem> result = new java.util.ArrayList<>();
|
||||
for (com.easyagents.core.document.Document document : documents) {
|
||||
KnowledgeSearchResultItem item = new KnowledgeSearchResultItem();
|
||||
item.setContent(document.getContent());
|
||||
item.setScore(document.getScore());
|
||||
Object hitSource = document.getMetadata("hitSource");
|
||||
item.setHitSource(hitSource == null ? null : String.valueOf(hitSource));
|
||||
item.setVectorScore(asDouble(document.getMetadata("vectorScore")));
|
||||
item.setKeywordScore(asDouble(document.getMetadata("keywordScore")));
|
||||
result.add(item);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Double asDouble(Object value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value instanceof Number number) {
|
||||
return number.doubleValue();
|
||||
}
|
||||
return Double.parseDouble(String.valueOf(value));
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package tech.easyflow.publicapi.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import com.easyagents.flow.core.chain.ChainDefinition;
|
||||
import com.easyagents.flow.core.chain.Parameter;
|
||||
import com.easyagents.flow.core.chain.runtime.ChainExecutor;
|
||||
@@ -18,7 +19,9 @@ import tech.easyflow.ai.easyagentsflow.service.WorkflowCheckService;
|
||||
import tech.easyflow.ai.easyagentsflow.service.WorkflowDatacenterContentService;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
import tech.easyflow.ai.service.WorkflowService;
|
||||
import tech.easyflow.common.constant.Constants;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||
|
||||
import java.math.BigInteger;
|
||||
@@ -75,6 +78,12 @@ public class PublicWorkflowController {
|
||||
if (workflow == null) {
|
||||
return Result.fail(1, "工作流不存在");
|
||||
}
|
||||
if (variables == null) {
|
||||
variables = new HashMap<>();
|
||||
}
|
||||
if (StpUtil.isLogin()) {
|
||||
variables.put(Constants.LOGIN_USER_KEY, SaTokenUtil.getLoginAccount());
|
||||
}
|
||||
Map<String, Object> res = chainExecutor.executeNode(workflowId.toString(), nodeId, variables);
|
||||
return Result.ok(res);
|
||||
}
|
||||
|
||||
@@ -83,6 +83,12 @@ public class UcWorkflowController extends BaseCurdController<WorkflowService, Wo
|
||||
if (workflow == null) {
|
||||
return Result.fail(1, "工作流不存在");
|
||||
}
|
||||
if (variables == null) {
|
||||
variables = new HashMap<>();
|
||||
}
|
||||
if (StpUtil.isLogin()) {
|
||||
variables.put(Constants.LOGIN_USER_KEY, SaTokenUtil.getLoginAccount());
|
||||
}
|
||||
Map<String, Object> res = chainExecutor.executeNode(workflowId.toString(), nodeId, variables);
|
||||
return Result.ok(res);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package tech.easyflow.common.mq.redis;
|
||||
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.context.SmartLifecycle;
|
||||
import org.springframework.data.domain.Range;
|
||||
import org.springframework.data.redis.connection.RedisConnection;
|
||||
@@ -34,6 +36,8 @@ import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class RedisMQConsumerContainer implements MQConsumerContainer, SmartLifecycle {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(RedisMQConsumerContainer.class);
|
||||
|
||||
private final RedisConnectionFactory redisConnectionFactory;
|
||||
private final StringRedisTemplate stringRedisTemplate;
|
||||
private final MQProperties properties;
|
||||
@@ -71,6 +75,8 @@ public class RedisMQConsumerContainer implements MQConsumerContainer, SmartLifec
|
||||
MQSubscription subscription = handler.subscription();
|
||||
for (int shard = 0; shard < Math.max(subscription.getShardCount(), 1); shard++) {
|
||||
int currentShard = shard;
|
||||
LOG.info("启动 MQ 消费线程: topic={}, group={}, shard={}, handler={}",
|
||||
subscription.getTopic(), subscription.getConsumerGroup(), currentShard, handler.getClass().getSimpleName());
|
||||
executorService.submit(() -> consumeLoop(handler, subscription, currentShard));
|
||||
}
|
||||
}
|
||||
@@ -106,6 +112,8 @@ public class RedisMQConsumerContainer implements MQConsumerContainer, SmartLifec
|
||||
String streamKey = keySupport.streamKey(subscription.getTopic(), shard);
|
||||
String consumerName = subscription.getConsumerGroup() + "-" + shard;
|
||||
ensureConsumerGroup(streamKey, subscription.getConsumerGroup());
|
||||
LOG.info("MQ 消费循环已启动: topic={}, group={}, shard={}, consumer={}, streamKey={}, handler={}",
|
||||
subscription.getTopic(), subscription.getConsumerGroup(), shard, consumerName, streamKey, handler.getClass().getSimpleName());
|
||||
while (running) {
|
||||
try {
|
||||
reclaimPending(streamKey, subscription.getConsumerGroup(), consumerName);
|
||||
@@ -123,8 +131,18 @@ public class RedisMQConsumerContainer implements MQConsumerContainer, SmartLifec
|
||||
if (messages.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
LOG.info("MQ 收到消息批次: topic={}, group={}, shard={}, consumer={}, streamKey={}, count={}",
|
||||
subscription.getTopic(), subscription.getConsumerGroup(), shard, consumerName, streamKey, messages.size());
|
||||
handleMessages(handler, streamKey, subscription.getConsumerGroup(), messages);
|
||||
} catch (Exception ignored) {
|
||||
} catch (Exception exception) {
|
||||
LOG.error("MQ 消费循环异常: topic={}, group={}, shard={}, consumer={}, streamKey={}, handler={}",
|
||||
subscription.getTopic(),
|
||||
subscription.getConsumerGroup(),
|
||||
shard,
|
||||
consumerName,
|
||||
streamKey,
|
||||
handler.getClass().getSimpleName(),
|
||||
exception);
|
||||
sleepSilently(1000L);
|
||||
}
|
||||
}
|
||||
@@ -192,8 +210,12 @@ public class RedisMQConsumerContainer implements MQConsumerContainer, SmartLifec
|
||||
message.setRetryCount(retryCount);
|
||||
message.getHeaders().put("lastError", reason == null ? "" : reason);
|
||||
if (retryCount > properties.getRedis().getMaxRetry()) {
|
||||
LOG.error("MQ 消息超过最大重试次数,进入死信队列: topic={}, messageId={}, streamKey={}, retryCount={}, reason={}",
|
||||
message.getTopic(), message.getMessageId(), message.getStreamKey(), retryCount, reason);
|
||||
deadLetterService.deadLetter(message, reason);
|
||||
} else {
|
||||
LOG.warn("MQ 消息消费失败,准备重试: topic={}, messageId={}, streamKey={}, retryCount={}, reason={}",
|
||||
message.getTopic(), message.getMessageId(), message.getStreamKey(), retryCount, reason);
|
||||
stringRedisTemplate.opsForStream().add(
|
||||
org.springframework.data.redis.connection.stream.StreamRecords.string(
|
||||
Map.of("payload", messageConverter.serialize(message))
|
||||
@@ -205,10 +227,16 @@ public class RedisMQConsumerContainer implements MQConsumerContainer, SmartLifec
|
||||
|
||||
private void handleMessages(MQConsumerHandler handler, String streamKey, String group, List<MQMessage> messages) throws Exception {
|
||||
try {
|
||||
LOG.info("MQ 开始批量处理消息: group={}, streamKey={}, count={}, handler={}",
|
||||
group, streamKey, messages.size(), handler.getClass().getSimpleName());
|
||||
handler.handle(messages);
|
||||
acknowledge(streamKey, group, messages);
|
||||
LOG.info("MQ 批量处理消息完成: group={}, streamKey={}, count={}, handler={}",
|
||||
group, streamKey, messages.size(), handler.getClass().getSimpleName());
|
||||
return;
|
||||
} catch (Exception batchEx) {
|
||||
LOG.error("MQ 批量处理消息失败,准备降级单条处理: group={}, streamKey={}, count={}, handler={}",
|
||||
group, streamKey, messages.size(), handler.getClass().getSimpleName(), batchEx);
|
||||
if (messages.size() == 1) {
|
||||
retryOrDeadLetter(messages, resolveReason(batchEx));
|
||||
acknowledge(streamKey, group, messages);
|
||||
@@ -218,7 +246,11 @@ public class RedisMQConsumerContainer implements MQConsumerContainer, SmartLifec
|
||||
|
||||
for (MQMessage message : messages) {
|
||||
try {
|
||||
LOG.info("MQ 开始单条处理消息: group={}, streamKey={}, messageId={}, handler={}",
|
||||
group, streamKey, message.getMessageId(), handler.getClass().getSimpleName());
|
||||
handler.handle(List.of(message));
|
||||
LOG.info("MQ 单条处理消息完成: group={}, streamKey={}, messageId={}, handler={}",
|
||||
group, streamKey, message.getMessageId(), handler.getClass().getSimpleName());
|
||||
} catch (Exception singleEx) {
|
||||
retryOrDeadLetter(List.of(message), resolveReason(singleEx));
|
||||
} finally {
|
||||
@@ -240,6 +272,7 @@ public class RedisMQConsumerContainer implements MQConsumerContainer, SmartLifec
|
||||
}
|
||||
MQAcknowledger acknowledger = records -> stringRedisTemplate.opsForStream().acknowledge(streamKey, group, ids);
|
||||
acknowledger.acknowledge(messages);
|
||||
LOG.info("MQ 消息确认完成: group={}, streamKey={}, count={}", group, streamKey, ids.length);
|
||||
}
|
||||
|
||||
private String resolveReason(Exception exception) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package tech.easyflow.common.mq.redis;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.data.redis.connection.stream.RecordId;
|
||||
import org.springframework.data.redis.connection.stream.StreamRecords;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
@@ -15,6 +17,8 @@ import java.util.UUID;
|
||||
|
||||
public class RedisMQProducer implements MQProducer {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(RedisMQProducer.class);
|
||||
|
||||
private final StringRedisTemplate stringRedisTemplate;
|
||||
private final MQProperties properties;
|
||||
private final MQMessageConverter messageConverter;
|
||||
@@ -47,12 +51,16 @@ public class RedisMQProducer implements MQProducer {
|
||||
int shardCount = Math.max(properties.getRedis().getChatPersistShardCount(), 1);
|
||||
int shard = keySupport.resolveShard(message.getKey(), shardCount);
|
||||
String streamKey = keySupport.streamKey(message.getTopic(), shard);
|
||||
LOG.info("MQ 开始投递消息: topic={}, messageId={}, key={}, shard={}, streamKey={}",
|
||||
message.getTopic(), message.getMessageId(), message.getKey(), shard, streamKey);
|
||||
RecordId recordId = stringRedisTemplate.opsForStream().add(
|
||||
StreamRecords.string(Map.of("payload", messageConverter.serialize(message))).withStreamKey(streamKey)
|
||||
);
|
||||
if (recordId == null) {
|
||||
throw new MQException("MQ 消息投递失败");
|
||||
}
|
||||
LOG.info("MQ 消息投递完成: topic={}, messageId={}, key={}, shard={}, streamKey={}, recordId={}",
|
||||
message.getTopic(), message.getMessageId(), message.getKey(), shard, streamKey, recordId.getValue());
|
||||
return recordId.getValue();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,14 @@ public class GlobalErrorResolver implements HandlerExceptionResolver {
|
||||
} else if (ex instanceof ConstraintViolationException) {
|
||||
error = Result.fail(400, ex.getMessage());
|
||||
} else if (ex instanceof BusinessException) {
|
||||
error = Result.fail(1, ex.getMessage());
|
||||
String message = ex.getMessage();
|
||||
if (message != null && message.matches("^\\d{4,}:.+$")) {
|
||||
int delimiterIndex = message.indexOf(':');
|
||||
int errorCode = Integer.parseInt(message.substring(0, delimiterIndex));
|
||||
error = Result.fail(errorCode, message.substring(delimiterIndex + 1));
|
||||
} else {
|
||||
error = Result.fail(1, message);
|
||||
}
|
||||
} else {
|
||||
LOG.error(ex.toString(), ex);
|
||||
error = Result.fail(1, "错误信息:" + ex.getMessage());
|
||||
|
||||
@@ -41,6 +41,10 @@
|
||||
<groupId>com.easyagents</groupId>
|
||||
<artifactId>easy-agents-support</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.easyagents</groupId>
|
||||
<artifactId>easy-agents-document-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.easyagents</groupId>
|
||||
<artifactId>easy-agents-rag-retrieval</artifactId>
|
||||
@@ -99,6 +103,10 @@
|
||||
<groupId>tech.easyflow</groupId>
|
||||
<artifactId>easyflow-common-chat-protocol</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>tech.easyflow</groupId>
|
||||
<artifactId>easyflow-common-mq</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.easyagents</groupId>
|
||||
|
||||
@@ -12,8 +12,9 @@ public class ThreadPoolConfig {
|
||||
private static final Logger log = LoggerFactory.getLogger(ThreadPoolConfig.class);
|
||||
|
||||
/**
|
||||
* SSE消息发送专用线程池
|
||||
* 核心原则:IO密集型任务(网络推送),线程数 = CPU核心数 * 2 + 1
|
||||
* 创建 SSE 消息发送线程池。
|
||||
*
|
||||
* @return SSE 推送线程池
|
||||
*/
|
||||
@Bean(name = "sseThreadPool")
|
||||
public ThreadPoolTaskExecutor sseThreadPool() {
|
||||
@@ -37,4 +38,29 @@ public class ThreadPoolConfig {
|
||||
executor.initialize();
|
||||
return executor;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建知识库文档导入任务线程池。
|
||||
*
|
||||
* @return 文档导入任务线程池
|
||||
*/
|
||||
@Bean(name = "documentImportTaskExecutor")
|
||||
public ThreadPoolTaskExecutor documentImportTaskExecutor() {
|
||||
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||
int cpuCoreNum = Runtime.getRuntime().availableProcessors();
|
||||
executor.setCorePoolSize(Math.max(2, cpuCoreNum));
|
||||
executor.setMaxPoolSize(Math.max(4, cpuCoreNum * 2));
|
||||
executor.setQueueCapacity(200);
|
||||
executor.setKeepAliveSeconds(60);
|
||||
executor.setThreadNamePrefix("document-import-");
|
||||
executor.setRejectedExecutionHandler((runnable, executorService) -> {
|
||||
log.error("文档导入线程池过载!核心线程数:{},最大线程数:{},队列任务数:{}",
|
||||
executorService.getCorePoolSize(),
|
||||
executorService.getMaximumPoolSize(),
|
||||
executorService.getQueue().size());
|
||||
throw new BusinessException("文档导入任务繁忙,请稍后重试");
|
||||
});
|
||||
executor.initialize();
|
||||
return executor;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package tech.easyflow.ai.constants;
|
||||
|
||||
/**
|
||||
* 知识库分享错误码定义。
|
||||
*/
|
||||
public final class KnowledgeShareErrorCode {
|
||||
|
||||
public static final int KNOWLEDGE_SHARE_EXPIRED = 4601;
|
||||
public static final int KNOWLEDGE_SHARE_INVALID = 4602;
|
||||
public static final int KNOWLEDGE_SHARE_FORBIDDEN = 4603;
|
||||
|
||||
private KnowledgeShareErrorCode() {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
package tech.easyflow.ai.document.exception;
|
||||
|
||||
/**
|
||||
* 文档解析桥接层异常。
|
||||
*
|
||||
* <p>桥接层负责把底层文档服务异常和文件加载异常转换为稳定语义,
|
||||
* 供上层业务按场景进行处理。</p>
|
||||
*
|
||||
* @author Codex
|
||||
* @since 2026-04-14
|
||||
*/
|
||||
public class DocumentParseBridgeException extends RuntimeException {
|
||||
|
||||
private final String code;
|
||||
|
||||
public DocumentParseBridgeException(String code, String message) {
|
||||
super(message);
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public DocumentParseBridgeException(String code, String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取稳定错误码。
|
||||
*
|
||||
* @return 错误码
|
||||
*/
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public static DocumentParseBridgeException serviceNotEnabled() {
|
||||
return new DocumentParseBridgeException(
|
||||
"service_not_enabled",
|
||||
"统一文档解析服务未启用,请先配置 easy-agents.document.pdf.provider"
|
||||
);
|
||||
}
|
||||
|
||||
public static DocumentParseBridgeException unsupportedSource(String message) {
|
||||
return new DocumentParseBridgeException("unsupported_source", message);
|
||||
}
|
||||
|
||||
public static DocumentParseBridgeException sourceLoadFailed(String message, Throwable cause) {
|
||||
return new DocumentParseBridgeException("source_load_failed", message, cause);
|
||||
}
|
||||
|
||||
public static DocumentParseBridgeException requestBuildFailed(String message, Throwable cause) {
|
||||
return new DocumentParseBridgeException("request_build_failed", message, cause);
|
||||
}
|
||||
|
||||
public static DocumentParseBridgeException requestBuildFailed(String message) {
|
||||
return new DocumentParseBridgeException("request_build_failed", message);
|
||||
}
|
||||
|
||||
public static DocumentParseBridgeException parseFailed(String message, Throwable cause) {
|
||||
return new DocumentParseBridgeException("parse_failed", message, cause);
|
||||
}
|
||||
|
||||
public static DocumentParseBridgeException parseFailed(String message) {
|
||||
return new DocumentParseBridgeException("parse_failed", message);
|
||||
}
|
||||
|
||||
public static DocumentParseBridgeException taskFailed(String message, Throwable cause) {
|
||||
return new DocumentParseBridgeException("task_failed", message, cause);
|
||||
}
|
||||
|
||||
public static DocumentParseBridgeException taskFailed(String message) {
|
||||
return new DocumentParseBridgeException("task_failed", message);
|
||||
}
|
||||
|
||||
public static DocumentParseBridgeException resultFetchFailed(String message, Throwable cause) {
|
||||
return new DocumentParseBridgeException("result_fetch_failed", message, cause);
|
||||
}
|
||||
|
||||
public static DocumentParseBridgeException resultFetchFailed(String message) {
|
||||
return new DocumentParseBridgeException("result_fetch_failed", message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package tech.easyflow.ai.document.model;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 文档解析工件。
|
||||
*
|
||||
* @author Codex
|
||||
* @since 2026-04-14
|
||||
*/
|
||||
public class DocumentParseArtifacts {
|
||||
|
||||
private Object middleJson;
|
||||
private Object contentList;
|
||||
private Object modelOutput;
|
||||
private Map<String, Object> extraJsonArtifacts = new LinkedHashMap<String, Object>();
|
||||
private Map<String, byte[]> extraBinaryArtifacts = new LinkedHashMap<String, byte[]>();
|
||||
|
||||
public Object getMiddleJson() {
|
||||
return middleJson;
|
||||
}
|
||||
|
||||
public void setMiddleJson(Object middleJson) {
|
||||
this.middleJson = middleJson;
|
||||
}
|
||||
|
||||
public Object getContentList() {
|
||||
return contentList;
|
||||
}
|
||||
|
||||
public void setContentList(Object contentList) {
|
||||
this.contentList = contentList;
|
||||
}
|
||||
|
||||
public Object getModelOutput() {
|
||||
return modelOutput;
|
||||
}
|
||||
|
||||
public void setModelOutput(Object modelOutput) {
|
||||
this.modelOutput = modelOutput;
|
||||
}
|
||||
|
||||
public Map<String, Object> getExtraJsonArtifacts() {
|
||||
return extraJsonArtifacts;
|
||||
}
|
||||
|
||||
public void setExtraJsonArtifacts(Map<String, Object> extraJsonArtifacts) {
|
||||
this.extraJsonArtifacts = extraJsonArtifacts == null
|
||||
? new LinkedHashMap<String, Object>()
|
||||
: extraJsonArtifacts;
|
||||
}
|
||||
|
||||
public Map<String, byte[]> getExtraBinaryArtifacts() {
|
||||
return extraBinaryArtifacts;
|
||||
}
|
||||
|
||||
public void setExtraBinaryArtifacts(Map<String, byte[]> extraBinaryArtifacts) {
|
||||
this.extraBinaryArtifacts = extraBinaryArtifacts == null
|
||||
? new LinkedHashMap<String, byte[]>()
|
||||
: extraBinaryArtifacts;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package tech.easyflow.ai.document.model;
|
||||
|
||||
/**
|
||||
* 文档解析场景预设。
|
||||
*
|
||||
* <p>场景由 easyflow 业务层传入,桥接层负责将场景映射为底层解析请求参数,
|
||||
* 避免业务模块直接感知多个布尔开关。</p>
|
||||
*
|
||||
* @author Codex
|
||||
* @since 2026-04-14
|
||||
*/
|
||||
public enum DocumentParseScenario {
|
||||
|
||||
/**
|
||||
* 工作流文本提取场景,仅要求尽快返回可直接消费的文本结果。
|
||||
*/
|
||||
WORKFLOW_TEXT,
|
||||
|
||||
/**
|
||||
* 知识库导入场景,需要保留更多结构化工件供后续分块分析使用。
|
||||
*/
|
||||
KNOWLEDGE_IMPORT,
|
||||
|
||||
/**
|
||||
* 尽可能返回完整工件,供后续高级消费场景使用。
|
||||
*/
|
||||
FULL_ARTIFACTS
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package tech.easyflow.ai.document.model;
|
||||
|
||||
/**
|
||||
* 任务聚合查询结果。
|
||||
*
|
||||
* <p>该对象在任务状态基础上按需附带最终解析结果。
|
||||
* 当任务尚未完成时仅返回状态信息;当任务已完成时可同时返回标准化结果。</p>
|
||||
*
|
||||
* @author Codex
|
||||
* @since 2026-04-14
|
||||
*/
|
||||
public class DocumentParseTaskInfo extends DocumentParseTaskStatus {
|
||||
|
||||
private DocumentParsedResult result;
|
||||
|
||||
/**
|
||||
* 获取标准化解析结果。
|
||||
*
|
||||
* @return 解析结果;任务未完成时可能为空
|
||||
*/
|
||||
public DocumentParsedResult getResult() {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置标准化解析结果。
|
||||
*
|
||||
* @param result 解析结果
|
||||
*/
|
||||
public void setResult(DocumentParsedResult result) {
|
||||
this.result = result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package tech.easyflow.ai.document.model;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 标准化异步解析任务状态。
|
||||
*
|
||||
* @author Codex
|
||||
* @since 2026-04-14
|
||||
*/
|
||||
public class DocumentParseTaskStatus {
|
||||
|
||||
private String taskId;
|
||||
private String status;
|
||||
private String backend;
|
||||
private List<String> fileNames = new ArrayList<String>();
|
||||
private String createdAt;
|
||||
private String startedAt;
|
||||
private String completedAt;
|
||||
private String error;
|
||||
private String statusUrl;
|
||||
private String resultUrl;
|
||||
private Integer queuedAhead;
|
||||
|
||||
public String getTaskId() {
|
||||
return taskId;
|
||||
}
|
||||
|
||||
public void setTaskId(String taskId) {
|
||||
this.taskId = taskId;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getBackend() {
|
||||
return backend;
|
||||
}
|
||||
|
||||
public void setBackend(String backend) {
|
||||
this.backend = backend;
|
||||
}
|
||||
|
||||
public List<String> getFileNames() {
|
||||
return fileNames;
|
||||
}
|
||||
|
||||
public void setFileNames(List<String> fileNames) {
|
||||
this.fileNames = fileNames == null ? new ArrayList<String>() : fileNames;
|
||||
}
|
||||
|
||||
public String getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(String createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public String getStartedAt() {
|
||||
return startedAt;
|
||||
}
|
||||
|
||||
public void setStartedAt(String startedAt) {
|
||||
this.startedAt = startedAt;
|
||||
}
|
||||
|
||||
public String getCompletedAt() {
|
||||
return completedAt;
|
||||
}
|
||||
|
||||
public void setCompletedAt(String completedAt) {
|
||||
this.completedAt = completedAt;
|
||||
}
|
||||
|
||||
public String getError() {
|
||||
return error;
|
||||
}
|
||||
|
||||
public void setError(String error) {
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
public String getStatusUrl() {
|
||||
return statusUrl;
|
||||
}
|
||||
|
||||
public void setStatusUrl(String statusUrl) {
|
||||
this.statusUrl = statusUrl;
|
||||
}
|
||||
|
||||
public String getResultUrl() {
|
||||
return resultUrl;
|
||||
}
|
||||
|
||||
public void setResultUrl(String resultUrl) {
|
||||
this.resultUrl = resultUrl;
|
||||
}
|
||||
|
||||
public Integer getQueuedAhead() {
|
||||
return queuedAhead;
|
||||
}
|
||||
|
||||
public void setQueuedAhead(Integer queuedAhead) {
|
||||
this.queuedAhead = queuedAhead;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package tech.easyflow.ai.document.model;
|
||||
|
||||
import com.easyagents.document.core.model.DocumentBlock;
|
||||
import com.easyagents.document.core.model.DocumentImage;
|
||||
import com.easyagents.document.core.model.DocumentPage;
|
||||
import com.easyagents.document.core.model.DocumentTable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 标准化单文档解析结果。
|
||||
*
|
||||
* @author Codex
|
||||
* @since 2026-04-14
|
||||
*/
|
||||
public class DocumentParsedResult {
|
||||
|
||||
private String fileName;
|
||||
private String preferredText;
|
||||
private String markdown;
|
||||
private String plainText;
|
||||
private List<DocumentPage> pages = new ArrayList<DocumentPage>();
|
||||
private List<DocumentBlock> blocks = new ArrayList<DocumentBlock>();
|
||||
private List<DocumentTable> tables = new ArrayList<DocumentTable>();
|
||||
private List<DocumentImage> images = new ArrayList<DocumentImage>();
|
||||
private List<String> warnings = new ArrayList<String>();
|
||||
private Map<String, Object> metadata = new LinkedHashMap<String, Object>();
|
||||
private DocumentParseArtifacts artifacts = new DocumentParseArtifacts();
|
||||
|
||||
public String getFileName() {
|
||||
return fileName;
|
||||
}
|
||||
|
||||
public void setFileName(String fileName) {
|
||||
this.fileName = fileName;
|
||||
}
|
||||
|
||||
public String getPreferredText() {
|
||||
return preferredText;
|
||||
}
|
||||
|
||||
public void setPreferredText(String preferredText) {
|
||||
this.preferredText = preferredText;
|
||||
}
|
||||
|
||||
public String getMarkdown() {
|
||||
return markdown;
|
||||
}
|
||||
|
||||
public void setMarkdown(String markdown) {
|
||||
this.markdown = markdown;
|
||||
}
|
||||
|
||||
public String getPlainText() {
|
||||
return plainText;
|
||||
}
|
||||
|
||||
public void setPlainText(String plainText) {
|
||||
this.plainText = plainText;
|
||||
}
|
||||
|
||||
public List<DocumentPage> getPages() {
|
||||
return pages;
|
||||
}
|
||||
|
||||
public void setPages(List<DocumentPage> pages) {
|
||||
this.pages = pages == null ? new ArrayList<DocumentPage>() : pages;
|
||||
}
|
||||
|
||||
public List<DocumentBlock> getBlocks() {
|
||||
return blocks;
|
||||
}
|
||||
|
||||
public void setBlocks(List<DocumentBlock> blocks) {
|
||||
this.blocks = blocks == null ? new ArrayList<DocumentBlock>() : blocks;
|
||||
}
|
||||
|
||||
public List<DocumentTable> getTables() {
|
||||
return tables;
|
||||
}
|
||||
|
||||
public void setTables(List<DocumentTable> tables) {
|
||||
this.tables = tables == null ? new ArrayList<DocumentTable>() : tables;
|
||||
}
|
||||
|
||||
public List<DocumentImage> getImages() {
|
||||
return images;
|
||||
}
|
||||
|
||||
public void setImages(List<DocumentImage> images) {
|
||||
this.images = images == null ? new ArrayList<DocumentImage>() : images;
|
||||
}
|
||||
|
||||
public List<String> getWarnings() {
|
||||
return warnings;
|
||||
}
|
||||
|
||||
public void setWarnings(List<String> warnings) {
|
||||
this.warnings = warnings == null ? new ArrayList<String>() : warnings;
|
||||
}
|
||||
|
||||
public Map<String, Object> getMetadata() {
|
||||
return metadata;
|
||||
}
|
||||
|
||||
public void setMetadata(Map<String, Object> metadata) {
|
||||
this.metadata = metadata == null ? new LinkedHashMap<String, Object>() : metadata;
|
||||
}
|
||||
|
||||
public DocumentParseArtifacts getArtifacts() {
|
||||
return artifacts;
|
||||
}
|
||||
|
||||
public void setArtifacts(DocumentParseArtifacts artifacts) {
|
||||
this.artifacts = artifacts == null ? new DocumentParseArtifacts() : artifacts;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package tech.easyflow.ai.document.model;
|
||||
|
||||
/**
|
||||
* 统一文档源引用。
|
||||
*
|
||||
* <p>该模型用于屏蔽业务模块和底层解析框架的文件输入差异,业务方只需要描述文件来自哪里,
|
||||
* 具体由桥接层负责加载字节内容并转成统一解析请求。</p>
|
||||
*
|
||||
* @author Codex
|
||||
* @since 2026-04-14
|
||||
*/
|
||||
public class DocumentSourceRef {
|
||||
|
||||
private String fileName;
|
||||
private String filePath;
|
||||
private String contentType;
|
||||
private Long size;
|
||||
private String url;
|
||||
private byte[] contentBytes;
|
||||
|
||||
/**
|
||||
* 创建基于文件存储路径的文档源。
|
||||
*
|
||||
* @param filePath 存储路径
|
||||
* @return 文档源
|
||||
*/
|
||||
public static DocumentSourceRef ofPath(String filePath) {
|
||||
DocumentSourceRef sourceRef = new DocumentSourceRef();
|
||||
sourceRef.setFilePath(filePath);
|
||||
return sourceRef;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建基于内存字节的文档源。
|
||||
*
|
||||
* @param fileName 文件名
|
||||
* @param contentBytes 文件字节
|
||||
* @return 文档源
|
||||
*/
|
||||
public static DocumentSourceRef ofBytes(String fileName, byte[] contentBytes) {
|
||||
DocumentSourceRef sourceRef = new DocumentSourceRef();
|
||||
sourceRef.setFileName(fileName);
|
||||
sourceRef.setContentBytes(contentBytes);
|
||||
return sourceRef;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建基于 URL 的文档源。
|
||||
*
|
||||
* @param url 文件 URL
|
||||
* @return 文档源
|
||||
*/
|
||||
public static DocumentSourceRef ofUrl(String url) {
|
||||
DocumentSourceRef sourceRef = new DocumentSourceRef();
|
||||
sourceRef.setUrl(url);
|
||||
return sourceRef;
|
||||
}
|
||||
|
||||
public String getFileName() {
|
||||
return fileName;
|
||||
}
|
||||
|
||||
public void setFileName(String fileName) {
|
||||
this.fileName = fileName;
|
||||
}
|
||||
|
||||
public String getFilePath() {
|
||||
return filePath;
|
||||
}
|
||||
|
||||
public void setFilePath(String filePath) {
|
||||
this.filePath = filePath;
|
||||
}
|
||||
|
||||
public String getContentType() {
|
||||
return contentType;
|
||||
}
|
||||
|
||||
public void setContentType(String contentType) {
|
||||
this.contentType = contentType;
|
||||
}
|
||||
|
||||
public Long getSize() {
|
||||
return size;
|
||||
}
|
||||
|
||||
public void setSize(Long size) {
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public void setUrl(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public byte[] getContentBytes() {
|
||||
return contentBytes;
|
||||
}
|
||||
|
||||
public void setContentBytes(byte[] contentBytes) {
|
||||
this.contentBytes = contentBytes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package tech.easyflow.ai.document.service;
|
||||
|
||||
import tech.easyflow.ai.document.model.DocumentParseScenario;
|
||||
import tech.easyflow.ai.document.model.DocumentSourceRef;
|
||||
import tech.easyflow.ai.document.model.DocumentParseTaskInfo;
|
||||
import tech.easyflow.ai.document.model.DocumentParseTaskStatus;
|
||||
import tech.easyflow.ai.document.model.DocumentParsedResult;
|
||||
|
||||
/**
|
||||
* 统一文档解析桥接门面。
|
||||
*
|
||||
* <p>业务模块通过该门面使用文档解析能力,而不是直接依赖 easy-agents 的原始请求和结果模型。</p>
|
||||
*
|
||||
* @author Codex
|
||||
* @since 2026-04-14
|
||||
*/
|
||||
public interface DocumentParseBridgeService {
|
||||
|
||||
/**
|
||||
* 同步解析单文档。
|
||||
*
|
||||
* @param source 文档源
|
||||
* @param scenario 解析场景
|
||||
* @return 标准化解析结果
|
||||
*/
|
||||
DocumentParsedResult parse(DocumentSourceRef source, DocumentParseScenario scenario);
|
||||
|
||||
/**
|
||||
* 异步提交单文档解析任务。
|
||||
*
|
||||
* @param source 文档源
|
||||
* @param scenario 解析场景
|
||||
* @return 异步任务状态
|
||||
*/
|
||||
DocumentParseTaskStatus submit(DocumentSourceRef source, DocumentParseScenario scenario);
|
||||
|
||||
/**
|
||||
* 查询异步任务状态。
|
||||
*
|
||||
* @param taskId 任务 ID
|
||||
* @return 异步任务状态
|
||||
*/
|
||||
DocumentParseTaskStatus queryTask(String taskId);
|
||||
|
||||
/**
|
||||
* 获取异步任务最终结果。
|
||||
*
|
||||
* <p>该方法面向“结果读取”语义,底层 provider 可能在内部等待任务完成后再返回最终结果,
|
||||
* 因此不适合直接作为轻量状态轮询接口;如果业务需要统一查看“当前状态 + 已完成结果”,
|
||||
* 应优先使用 {@link #queryTaskInfo(String)}。</p>
|
||||
*
|
||||
* @param taskId 任务 ID
|
||||
* @return 标准化解析结果
|
||||
*/
|
||||
DocumentParsedResult queryResult(String taskId);
|
||||
|
||||
/**
|
||||
* 聚合查询异步任务信息。
|
||||
*
|
||||
* <p>当任务仍在处理中时仅返回状态;当任务已完成时会附带标准化结果。
|
||||
* 该方法适合用于页面或业务侧统一读取“当前状态 + 可用结果”。</p>
|
||||
*
|
||||
* @param taskId 任务 ID
|
||||
* @return 聚合任务信息
|
||||
*/
|
||||
DocumentParseTaskInfo queryTaskInfo(String taskId);
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
package tech.easyflow.ai.document.service.impl;
|
||||
|
||||
import com.easyagents.document.core.DocumentParseService;
|
||||
import com.easyagents.document.core.model.ParseResponse;
|
||||
import com.easyagents.document.core.model.ParseResult;
|
||||
import com.easyagents.document.core.model.ParseTaskInfo;
|
||||
import com.easyagents.document.core.model.ParseTaskStatus;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
import tech.easyflow.ai.document.exception.DocumentParseBridgeException;
|
||||
import tech.easyflow.ai.document.model.DocumentParseScenario;
|
||||
import tech.easyflow.ai.document.model.DocumentSourceRef;
|
||||
import tech.easyflow.ai.document.model.DocumentParseTaskInfo;
|
||||
import tech.easyflow.ai.document.model.DocumentParseTaskStatus;
|
||||
import tech.easyflow.ai.document.model.DocumentParsedResult;
|
||||
import tech.easyflow.ai.document.service.DocumentParseBridgeService;
|
||||
import tech.easyflow.ai.document.support.DocumentSourceLoader;
|
||||
import tech.easyflow.ai.document.support.DocumentParseRequestFactory;
|
||||
import tech.easyflow.ai.document.support.DocumentParseResultMapper;
|
||||
import tech.easyflow.ai.document.support.LoadedDocumentSource;
|
||||
import tech.easyflow.ai.utils.DocUtil;
|
||||
|
||||
/**
|
||||
* 统一文档解析桥接门面默认实现。
|
||||
*
|
||||
* @author Codex
|
||||
* @since 2026-04-14
|
||||
*/
|
||||
@Service
|
||||
public class DocumentParseBridgeServiceImpl implements DocumentParseBridgeService {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DocumentParseBridgeServiceImpl.class);
|
||||
|
||||
@Nullable
|
||||
private final DocumentParseService documentParseService;
|
||||
private final DocumentSourceLoader documentSourceLoader;
|
||||
private final DocumentParseRequestFactory parseRequestFactory;
|
||||
private final DocumentParseResultMapper parseResultMapper;
|
||||
|
||||
public DocumentParseBridgeServiceImpl(@Nullable DocumentParseService documentParseService,
|
||||
DocumentSourceLoader documentSourceLoader,
|
||||
DocumentParseRequestFactory parseRequestFactory,
|
||||
DocumentParseResultMapper parseResultMapper) {
|
||||
this.documentParseService = documentParseService;
|
||||
this.documentSourceLoader = documentSourceLoader;
|
||||
this.parseRequestFactory = parseRequestFactory;
|
||||
this.parseResultMapper = parseResultMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public DocumentParsedResult parse(DocumentSourceRef source, DocumentParseScenario scenario) {
|
||||
try {
|
||||
LoadedDocumentSource loadedSource = prepareSupportedSource(source);
|
||||
LOG.info("桥接服务开始同步解析文档: fileName={}, contentType={}, scenario={}",
|
||||
loadedSource.getFileName(), loadedSource.getContentType(), scenario);
|
||||
ParseResponse response = requireService().parse(parseRequestFactory.build(loadedSource, scenario));
|
||||
DocumentParsedResult result = parseResultMapper.map(extractSingleResult(response, false));
|
||||
LOG.info("桥接服务同步解析完成: fileName={}, scenario={}, preferredTextLength={}",
|
||||
loadedSource.getFileName(), scenario, resolveTextLength(result));
|
||||
return result;
|
||||
} catch (DocumentParseBridgeException e) {
|
||||
LOG.error("桥接服务同步解析失败: fileName={}, scenario={}",
|
||||
source == null ? null : source.getFileName(), scenario, e);
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
LOG.error("桥接服务同步解析异常: fileName={}, scenario={}",
|
||||
source == null ? null : source.getFileName(), scenario, e);
|
||||
throw DocumentParseBridgeException.parseFailed("同步文档解析失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public DocumentParseTaskStatus submit(DocumentSourceRef source, DocumentParseScenario scenario) {
|
||||
try {
|
||||
LoadedDocumentSource loadedSource = prepareSupportedSource(source);
|
||||
LOG.info("桥接服务开始提交异步解析任务: fileName={}, contentType={}, scenario={}",
|
||||
loadedSource.getFileName(), loadedSource.getContentType(), scenario);
|
||||
ParseTaskStatus taskStatus = requireService().submit(parseRequestFactory.build(loadedSource, scenario));
|
||||
DocumentParseTaskStatus mappedStatus = parseResultMapper.map(taskStatus);
|
||||
LOG.info("桥接服务异步解析任务提交完成: fileName={}, scenario={}, providerTaskId={}, status={}",
|
||||
loadedSource.getFileName(), scenario, mappedStatus.getTaskId(), mappedStatus.getStatus());
|
||||
return mappedStatus;
|
||||
} catch (DocumentParseBridgeException e) {
|
||||
LOG.error("桥接服务提交异步解析任务失败: fileName={}, scenario={}",
|
||||
source == null ? null : source.getFileName(), scenario, e);
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
LOG.error("桥接服务提交异步解析任务异常: fileName={}, scenario={}",
|
||||
source == null ? null : source.getFileName(), scenario, e);
|
||||
throw DocumentParseBridgeException.taskFailed("提交异步文档解析任务失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public DocumentParseTaskStatus queryTask(String taskId) {
|
||||
if (!StringUtils.hasText(taskId)) {
|
||||
throw DocumentParseBridgeException.taskFailed("taskId 不能为空");
|
||||
}
|
||||
try {
|
||||
return parseResultMapper.map(requireService().queryTask(taskId));
|
||||
} catch (DocumentParseBridgeException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw DocumentParseBridgeException.taskFailed("查询异步文档解析任务状态失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public DocumentParsedResult queryResult(String taskId) {
|
||||
if (!StringUtils.hasText(taskId)) {
|
||||
throw DocumentParseBridgeException.resultFetchFailed("taskId 不能为空");
|
||||
}
|
||||
try {
|
||||
LOG.info("桥接服务开始获取异步解析结果: providerTaskId={}", taskId);
|
||||
ParseResponse response = requireService().queryResult(taskId);
|
||||
DocumentParsedResult result = parseResultMapper.map(extractSingleResult(response, true));
|
||||
LOG.info("桥接服务获取异步解析结果完成: providerTaskId={}, preferredTextLength={}",
|
||||
taskId, resolveTextLength(result));
|
||||
return result;
|
||||
} catch (DocumentParseBridgeException e) {
|
||||
LOG.error("桥接服务获取异步解析结果失败: providerTaskId={}", taskId, e);
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
LOG.error("桥接服务获取异步解析结果异常: providerTaskId={}", taskId, e);
|
||||
throw DocumentParseBridgeException.resultFetchFailed("获取异步文档解析结果失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public DocumentParseTaskInfo queryTaskInfo(String taskId) {
|
||||
if (!StringUtils.hasText(taskId)) {
|
||||
throw DocumentParseBridgeException.taskFailed("taskId 不能为空");
|
||||
}
|
||||
try {
|
||||
ParseTaskInfo taskInfo = requireService().queryTaskInfo(taskId);
|
||||
DocumentParseTaskInfo mappedTaskInfo = parseResultMapper.map(taskInfo);
|
||||
LOG.info("桥接服务查询异步解析任务状态: providerTaskId={}, status={}, hasResult={}",
|
||||
taskId,
|
||||
mappedTaskInfo == null ? null : mappedTaskInfo.getStatus(),
|
||||
mappedTaskInfo != null && mappedTaskInfo.getResult() != null);
|
||||
return mappedTaskInfo;
|
||||
} catch (DocumentParseBridgeException e) {
|
||||
LOG.error("桥接服务查询异步解析任务状态失败: providerTaskId={}", taskId, e);
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
LOG.error("桥接服务查询异步解析任务状态异常: providerTaskId={}", taskId, e);
|
||||
throw DocumentParseBridgeException.taskFailed("聚合查询异步文档解析任务信息失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
private int resolveTextLength(DocumentParsedResult result) {
|
||||
String text = result == null ? null : result.getPreferredText();
|
||||
if (!StringUtils.hasText(text) && result != null) {
|
||||
text = result.getMarkdown();
|
||||
}
|
||||
if (!StringUtils.hasText(text) && result != null) {
|
||||
text = result.getPlainText();
|
||||
}
|
||||
return text == null ? 0 : text.length();
|
||||
}
|
||||
|
||||
private DocumentParseService requireService() {
|
||||
if (documentParseService == null) {
|
||||
throw DocumentParseBridgeException.serviceNotEnabled();
|
||||
}
|
||||
return documentParseService;
|
||||
}
|
||||
|
||||
private LoadedDocumentSource prepareSupportedSource(DocumentSourceRef source) {
|
||||
LoadedDocumentSource loadedSource = documentSourceLoader.load(source);
|
||||
if (!isSupportedByBridge(loadedSource)) {
|
||||
throw DocumentParseBridgeException.unsupportedSource("统一文档解析桥接当前仅支持 PDF、DOCX 文件");
|
||||
}
|
||||
return loadedSource;
|
||||
}
|
||||
|
||||
private boolean isSupportedByBridge(LoadedDocumentSource loadedSource) {
|
||||
String contentType = loadedSource.getContentType();
|
||||
if (StringUtils.hasText(contentType)) {
|
||||
String normalizedContentType = contentType.toLowerCase();
|
||||
if (normalizedContentType.contains("pdf")
|
||||
|| normalizedContentType.contains("wordprocessingml.document")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
String fileName = loadedSource.getFileName();
|
||||
if (!StringUtils.hasText(fileName) || !fileName.contains(".")) {
|
||||
return false;
|
||||
}
|
||||
String suffix = DocUtil.normalizeSuffix(DocUtil.getSuffix(fileName));
|
||||
if ("pdf".equals(suffix) || "docx".equals(suffix)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private ParseResult extractSingleResult(ParseResponse response, boolean resultFetchPhase) {
|
||||
if (response == null || response.getResults() == null || response.getResults().isEmpty()) {
|
||||
if (resultFetchPhase) {
|
||||
throw DocumentParseBridgeException.resultFetchFailed("异步文档解析结果为空");
|
||||
}
|
||||
throw DocumentParseBridgeException.parseFailed("同步文档解析结果为空");
|
||||
}
|
||||
return response.getResults().get(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package tech.easyflow.ai.document.support;
|
||||
|
||||
import com.easyagents.document.core.model.ParseFile;
|
||||
import com.easyagents.document.core.model.ParseRequest;
|
||||
import org.springframework.stereotype.Component;
|
||||
import tech.easyflow.ai.document.exception.DocumentParseBridgeException;
|
||||
import tech.easyflow.ai.document.model.DocumentParseScenario;
|
||||
|
||||
/**
|
||||
* easy-agents 解析请求工厂。
|
||||
*
|
||||
* <p>负责把 easyflow 业务场景预设映射为底层统一解析请求。</p>
|
||||
*
|
||||
* @author Codex
|
||||
* @since 2026-04-14
|
||||
*/
|
||||
@Component
|
||||
public class DocumentParseRequestFactory {
|
||||
|
||||
/**
|
||||
* 构建统一解析请求。
|
||||
*
|
||||
* @param source 已加载文档源
|
||||
* @param scenario 解析场景
|
||||
* @return 统一解析请求
|
||||
*/
|
||||
public ParseRequest build(LoadedDocumentSource source, DocumentParseScenario scenario) {
|
||||
if (source == null || source.getContentBytes() == null || source.getContentBytes().length == 0) {
|
||||
throw DocumentParseBridgeException.requestBuildFailed("文档源内容为空,无法构建解析请求");
|
||||
}
|
||||
if (scenario == null) {
|
||||
throw DocumentParseBridgeException.requestBuildFailed("解析场景不能为空");
|
||||
}
|
||||
ParseRequest request = new ParseRequest();
|
||||
// 保持为空,交由 easy-agents provider 按环境配置回填默认值。
|
||||
request.setParseMethod(null);
|
||||
request.setFormulaEnabled(null);
|
||||
request.setTableEnabled(null);
|
||||
request.addFile(ParseFile.of(source.getFileName(), source.getContentBytes(), source.getContentType()));
|
||||
applyScenario(request, scenario);
|
||||
return request;
|
||||
}
|
||||
|
||||
private void applyScenario(ParseRequest request, DocumentParseScenario scenario) {
|
||||
switch (scenario) {
|
||||
case WORKFLOW_TEXT:
|
||||
request.setReturnMarkdown(Boolean.TRUE);
|
||||
request.setReturnMiddleJson(Boolean.FALSE);
|
||||
request.setReturnContentList(Boolean.FALSE);
|
||||
request.setReturnModelOutput(Boolean.FALSE);
|
||||
request.setReturnImages(Boolean.FALSE);
|
||||
break;
|
||||
case KNOWLEDGE_IMPORT:
|
||||
request.setReturnMarkdown(Boolean.TRUE);
|
||||
request.setReturnMiddleJson(Boolean.TRUE);
|
||||
request.setReturnContentList(Boolean.TRUE);
|
||||
request.setReturnModelOutput(Boolean.FALSE);
|
||||
request.setReturnImages(Boolean.TRUE);
|
||||
break;
|
||||
case FULL_ARTIFACTS:
|
||||
request.setReturnMarkdown(Boolean.TRUE);
|
||||
request.setReturnMiddleJson(Boolean.TRUE);
|
||||
request.setReturnContentList(Boolean.TRUE);
|
||||
request.setReturnModelOutput(Boolean.TRUE);
|
||||
request.setReturnImages(Boolean.TRUE);
|
||||
break;
|
||||
default:
|
||||
throw DocumentParseBridgeException.requestBuildFailed("不支持的文档解析场景: " + scenario);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package tech.easyflow.ai.document.support;
|
||||
|
||||
import com.easyagents.document.core.model.ParseArtifacts;
|
||||
import com.easyagents.document.core.model.ParseResult;
|
||||
import com.easyagents.document.core.model.ParseTaskInfo;
|
||||
import com.easyagents.document.core.model.ParseTaskStatus;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
import tech.easyflow.ai.document.model.DocumentParseArtifacts;
|
||||
import tech.easyflow.ai.document.model.DocumentParseTaskInfo;
|
||||
import tech.easyflow.ai.document.model.DocumentParseTaskStatus;
|
||||
import tech.easyflow.ai.document.model.DocumentParsedResult;
|
||||
|
||||
/**
|
||||
* easy-agents 结果映射器。
|
||||
*
|
||||
* <p>负责把底层解析结果转换为 easyflow 侧稳定 DTO,并统一 preferredText 规则。</p>
|
||||
*
|
||||
* @author Codex
|
||||
* @since 2026-04-14
|
||||
*/
|
||||
@Component
|
||||
public class DocumentParseResultMapper {
|
||||
|
||||
/**
|
||||
* 映射单文件解析结果。
|
||||
*
|
||||
* @param parseResult 底层结果
|
||||
* @return easyflow 结果
|
||||
*/
|
||||
public DocumentParsedResult map(ParseResult parseResult) {
|
||||
DocumentParsedResult document = new DocumentParsedResult();
|
||||
if (parseResult == null) {
|
||||
return document;
|
||||
}
|
||||
document.setFileName(parseResult.getFileName());
|
||||
document.setMarkdown(parseResult.getMarkdown());
|
||||
document.setPlainText(parseResult.getPlainText());
|
||||
document.setPreferredText(resolvePreferredText(parseResult));
|
||||
document.setPages(parseResult.getPages());
|
||||
document.setBlocks(parseResult.getBlocks());
|
||||
document.setTables(parseResult.getTables());
|
||||
document.setImages(parseResult.getImages());
|
||||
document.setWarnings(parseResult.getWarnings());
|
||||
document.setMetadata(parseResult.getMetadata());
|
||||
document.setArtifacts(mapArtifacts(parseResult.getArtifacts()));
|
||||
return document;
|
||||
}
|
||||
|
||||
/**
|
||||
* 映射异步任务状态。
|
||||
*
|
||||
* @param taskStatus 底层任务状态
|
||||
* @return easyflow 任务状态
|
||||
*/
|
||||
public DocumentParseTaskStatus map(ParseTaskStatus taskStatus) {
|
||||
DocumentParseTaskStatus status = new DocumentParseTaskStatus();
|
||||
if (taskStatus == null) {
|
||||
return status;
|
||||
}
|
||||
status.setTaskId(taskStatus.getTaskId());
|
||||
status.setStatus(taskStatus.getStatus());
|
||||
status.setBackend(taskStatus.getBackend());
|
||||
status.setFileNames(taskStatus.getFileNames());
|
||||
status.setCreatedAt(taskStatus.getCreatedAt());
|
||||
status.setStartedAt(taskStatus.getStartedAt());
|
||||
status.setCompletedAt(taskStatus.getCompletedAt());
|
||||
status.setError(taskStatus.getError());
|
||||
status.setStatusUrl(taskStatus.getStatusUrl());
|
||||
status.setResultUrl(taskStatus.getResultUrl());
|
||||
status.setQueuedAhead(taskStatus.getQueuedAhead());
|
||||
return status;
|
||||
}
|
||||
|
||||
/**
|
||||
* 映射任务聚合查询结果。
|
||||
*
|
||||
* @param taskInfo 底层任务聚合结果
|
||||
* @return easyflow 聚合任务结果
|
||||
*/
|
||||
public DocumentParseTaskInfo map(ParseTaskInfo taskInfo) {
|
||||
DocumentParseTaskInfo mapped = new DocumentParseTaskInfo();
|
||||
if (taskInfo == null) {
|
||||
return mapped;
|
||||
}
|
||||
fillTaskStatus(mapped, taskInfo);
|
||||
if (taskInfo.getResult() != null
|
||||
&& taskInfo.getResult().getResults() != null
|
||||
&& !taskInfo.getResult().getResults().isEmpty()) {
|
||||
mapped.setResult(map(taskInfo.getResult().getResults().get(0)));
|
||||
}
|
||||
return mapped;
|
||||
}
|
||||
|
||||
private void fillTaskStatus(DocumentParseTaskStatus status, ParseTaskStatus taskStatus) {
|
||||
status.setTaskId(taskStatus.getTaskId());
|
||||
status.setStatus(taskStatus.getStatus());
|
||||
status.setBackend(taskStatus.getBackend());
|
||||
status.setFileNames(taskStatus.getFileNames());
|
||||
status.setCreatedAt(taskStatus.getCreatedAt());
|
||||
status.setStartedAt(taskStatus.getStartedAt());
|
||||
status.setCompletedAt(taskStatus.getCompletedAt());
|
||||
status.setError(taskStatus.getError());
|
||||
status.setStatusUrl(taskStatus.getStatusUrl());
|
||||
status.setResultUrl(taskStatus.getResultUrl());
|
||||
status.setQueuedAhead(taskStatus.getQueuedAhead());
|
||||
}
|
||||
|
||||
private String resolvePreferredText(ParseResult parseResult) {
|
||||
if (StringUtils.hasText(parseResult.getMarkdown())) {
|
||||
return parseResult.getMarkdown();
|
||||
}
|
||||
return parseResult.getPlainText();
|
||||
}
|
||||
|
||||
private DocumentParseArtifacts mapArtifacts(ParseArtifacts artifacts) {
|
||||
DocumentParseArtifacts mappedArtifacts = new DocumentParseArtifacts();
|
||||
if (artifacts == null) {
|
||||
return mappedArtifacts;
|
||||
}
|
||||
mappedArtifacts.setMiddleJson(artifacts.getMiddleJson());
|
||||
mappedArtifacts.setContentList(artifacts.getContentList());
|
||||
mappedArtifacts.setModelOutput(artifacts.getModelOutput());
|
||||
mappedArtifacts.setExtraJsonArtifacts(artifacts.getExtraJsonArtifacts());
|
||||
mappedArtifacts.setExtraBinaryArtifacts(artifacts.getExtraBinaryArtifacts());
|
||||
return mappedArtifacts;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package tech.easyflow.ai.document.support;
|
||||
|
||||
import cn.hutool.http.HttpUtil;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
import tech.easyflow.ai.document.exception.DocumentParseBridgeException;
|
||||
import tech.easyflow.ai.document.model.DocumentSourceRef;
|
||||
import tech.easyflow.common.filestorage.FileStorageService;
|
||||
import tech.easyflow.common.filestorage.utils.PathGeneratorUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URLConnection;
|
||||
|
||||
/**
|
||||
* 文档源加载器。
|
||||
*
|
||||
* <p>负责把不同来源的文件引用统一转换为内存字节和标准文件元数据。</p>
|
||||
*
|
||||
* @author Codex
|
||||
* @since 2026-04-14
|
||||
*/
|
||||
@Component
|
||||
public class DocumentSourceLoader {
|
||||
|
||||
private final FileStorageService fileStorageService;
|
||||
|
||||
public DocumentSourceLoader(@Qualifier("default") FileStorageService fileStorageService) {
|
||||
this.fileStorageService = fileStorageService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载文档源。
|
||||
*
|
||||
* @param sourceRef easyflow 文档源
|
||||
* @return 内部已加载文档对象
|
||||
*/
|
||||
public LoadedDocumentSource load(DocumentSourceRef sourceRef) {
|
||||
if (sourceRef == null) {
|
||||
throw DocumentParseBridgeException.unsupportedSource("文档源不能为空");
|
||||
}
|
||||
if (hasContentBytes(sourceRef)) {
|
||||
return buildLoadedSource(
|
||||
resolveFileName(sourceRef),
|
||||
resolveContentType(sourceRef, resolveFileName(sourceRef)),
|
||||
resolveSize(sourceRef, sourceRef.getContentBytes().length),
|
||||
sourceRef.getContentBytes()
|
||||
);
|
||||
}
|
||||
if (StringUtils.hasText(sourceRef.getFilePath())) {
|
||||
if (isRemoteUrl(sourceRef.getFilePath())) {
|
||||
return loadFromRemoteValue(sourceRef, sourceRef.getFilePath());
|
||||
}
|
||||
return loadFromFilePath(sourceRef);
|
||||
}
|
||||
if (StringUtils.hasText(sourceRef.getUrl())) {
|
||||
return loadFromUrl(sourceRef);
|
||||
}
|
||||
throw DocumentParseBridgeException.unsupportedSource("文档源缺少 filePath、url 或 contentBytes");
|
||||
}
|
||||
|
||||
private LoadedDocumentSource loadFromFilePath(DocumentSourceRef sourceRef) {
|
||||
String fileName = resolveFileName(sourceRef);
|
||||
try (InputStream inputStream = fileStorageService.readStream(sourceRef.getFilePath())) {
|
||||
byte[] contentBytes = inputStream.readAllBytes();
|
||||
long actualSize = sourceRef.getSize() != null ? sourceRef.getSize() : fileStorageService.getFileSize(sourceRef.getFilePath());
|
||||
return buildLoadedSource(
|
||||
fileName,
|
||||
resolveContentType(sourceRef, fileName),
|
||||
resolveSize(sourceRef, actualSize),
|
||||
contentBytes
|
||||
);
|
||||
} catch (IOException e) {
|
||||
throw DocumentParseBridgeException.sourceLoadFailed(
|
||||
"读取文档存储文件失败: " + sourceRef.getFilePath(),
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private LoadedDocumentSource loadFromUrl(DocumentSourceRef sourceRef) {
|
||||
return loadFromRemoteValue(sourceRef, sourceRef.getUrl());
|
||||
}
|
||||
|
||||
private LoadedDocumentSource loadFromRemoteValue(DocumentSourceRef sourceRef, String remoteUrl) {
|
||||
String fileName = resolveFileName(sourceRef);
|
||||
try {
|
||||
byte[] contentBytes = HttpUtil.downloadBytes(remoteUrl);
|
||||
return buildLoadedSource(
|
||||
fileName,
|
||||
resolveContentType(sourceRef, fileName),
|
||||
resolveSize(sourceRef, contentBytes.length),
|
||||
contentBytes
|
||||
);
|
||||
} catch (Exception e) {
|
||||
throw DocumentParseBridgeException.sourceLoadFailed(
|
||||
"下载文档 URL 失败: " + remoteUrl,
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private LoadedDocumentSource buildLoadedSource(String fileName, String contentType, Long size, byte[] contentBytes) {
|
||||
LoadedDocumentSource loadedSource = new LoadedDocumentSource();
|
||||
loadedSource.setFileName(fileName);
|
||||
loadedSource.setContentType(contentType);
|
||||
loadedSource.setSize(size);
|
||||
loadedSource.setContentBytes(contentBytes);
|
||||
return loadedSource;
|
||||
}
|
||||
|
||||
private String resolveFileName(DocumentSourceRef sourceRef) {
|
||||
if (StringUtils.hasText(sourceRef.getFileName())) {
|
||||
return PathGeneratorUtil.getPureFileName(sourceRef.getFileName());
|
||||
}
|
||||
if (StringUtils.hasText(sourceRef.getFilePath())) {
|
||||
return PathGeneratorUtil.getPureFileName(sourceRef.getFilePath());
|
||||
}
|
||||
if (StringUtils.hasText(sourceRef.getUrl())) {
|
||||
String pureName = PathGeneratorUtil.getPureFileName(sourceRef.getUrl());
|
||||
int queryIndex = pureName.indexOf('?');
|
||||
return queryIndex >= 0 ? pureName.substring(0, queryIndex) : pureName;
|
||||
}
|
||||
throw DocumentParseBridgeException.unsupportedSource("文档源缺少可用文件名");
|
||||
}
|
||||
|
||||
private String resolveContentType(DocumentSourceRef sourceRef, String fileName) {
|
||||
if (StringUtils.hasText(sourceRef.getContentType())) {
|
||||
return sourceRef.getContentType();
|
||||
}
|
||||
return URLConnection.guessContentTypeFromName(fileName);
|
||||
}
|
||||
|
||||
private Long resolveSize(DocumentSourceRef sourceRef, long fallbackSize) {
|
||||
return sourceRef.getSize() != null ? sourceRef.getSize() : fallbackSize;
|
||||
}
|
||||
|
||||
private boolean hasContentBytes(DocumentSourceRef sourceRef) {
|
||||
return sourceRef.getContentBytes() != null && sourceRef.getContentBytes().length > 0;
|
||||
}
|
||||
|
||||
private boolean isRemoteUrl(String value) {
|
||||
return value.startsWith("http://") || value.startsWith("https://");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package tech.easyflow.ai.document.support;
|
||||
|
||||
/**
|
||||
* 桥接层内部已加载文档源。
|
||||
*
|
||||
* <p>该对象只在桥接层内部流转,用于承接已解析出的文件名、类型和字节内容。</p>
|
||||
*
|
||||
* @author Codex
|
||||
* @since 2026-04-14
|
||||
*/
|
||||
public class LoadedDocumentSource {
|
||||
|
||||
private String fileName;
|
||||
private String contentType;
|
||||
private Long size;
|
||||
private byte[] contentBytes;
|
||||
|
||||
public String getFileName() {
|
||||
return fileName;
|
||||
}
|
||||
|
||||
public void setFileName(String fileName) {
|
||||
this.fileName = fileName;
|
||||
}
|
||||
|
||||
public String getContentType() {
|
||||
return contentType;
|
||||
}
|
||||
|
||||
public void setContentType(String contentType) {
|
||||
this.contentType = contentType;
|
||||
}
|
||||
|
||||
public Long getSize() {
|
||||
return size;
|
||||
}
|
||||
|
||||
public void setSize(Long size) {
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
public byte[] getContentBytes() {
|
||||
return contentBytes;
|
||||
}
|
||||
|
||||
public void setContentBytes(byte[] contentBytes) {
|
||||
this.contentBytes = contentBytes;
|
||||
}
|
||||
}
|
||||
@@ -93,6 +93,7 @@ public final class DocumentImportDtos {
|
||||
|
||||
public static class PreviewRequest implements Serializable {
|
||||
private BigInteger knowledgeId;
|
||||
private BigInteger documentId;
|
||||
private List<PreviewFileRequest> files = new ArrayList<PreviewFileRequest>();
|
||||
|
||||
public BigInteger getKnowledgeId() {
|
||||
@@ -103,6 +104,14 @@ public final class DocumentImportDtos {
|
||||
this.knowledgeId = knowledgeId;
|
||||
}
|
||||
|
||||
public BigInteger getDocumentId() {
|
||||
return documentId;
|
||||
}
|
||||
|
||||
public void setDocumentId(BigInteger documentId) {
|
||||
this.documentId = documentId;
|
||||
}
|
||||
|
||||
public List<PreviewFileRequest> getFiles() {
|
||||
return files;
|
||||
}
|
||||
@@ -114,6 +123,7 @@ public final class DocumentImportDtos {
|
||||
|
||||
public static class CommitRequest implements Serializable {
|
||||
private BigInteger knowledgeId;
|
||||
private BigInteger documentId;
|
||||
private List<String> previewSessionIds = new ArrayList<String>();
|
||||
|
||||
public BigInteger getKnowledgeId() {
|
||||
@@ -124,6 +134,14 @@ public final class DocumentImportDtos {
|
||||
this.knowledgeId = knowledgeId;
|
||||
}
|
||||
|
||||
public BigInteger getDocumentId() {
|
||||
return documentId;
|
||||
}
|
||||
|
||||
public void setDocumentId(BigInteger documentId) {
|
||||
this.documentId = documentId;
|
||||
}
|
||||
|
||||
public List<String> getPreviewSessionIds() {
|
||||
return previewSessionIds;
|
||||
}
|
||||
@@ -241,16 +259,158 @@ public final class DocumentImportDtos {
|
||||
}
|
||||
}
|
||||
|
||||
public static class PreviewSourceRange implements Serializable {
|
||||
private Integer start;
|
||||
private Integer end;
|
||||
|
||||
public Integer getStart() {
|
||||
return start;
|
||||
}
|
||||
|
||||
public void setStart(Integer start) {
|
||||
this.start = start;
|
||||
}
|
||||
|
||||
public Integer getEnd() {
|
||||
return end;
|
||||
}
|
||||
|
||||
public void setEnd(Integer end) {
|
||||
this.end = end;
|
||||
}
|
||||
}
|
||||
|
||||
public static class PreviewChunkResult implements Serializable {
|
||||
private String answer;
|
||||
private Integer charCount;
|
||||
private String chunkId;
|
||||
private String chunkType;
|
||||
private String content;
|
||||
private List<String> headingPath = new ArrayList<String>();
|
||||
private Integer partNo;
|
||||
private Integer partTotal;
|
||||
private String question;
|
||||
private String sourceLabel;
|
||||
private Integer tokenEstimate;
|
||||
private List<String> warnings = new ArrayList<String>();
|
||||
private List<PreviewSourceRange> sourceRanges = new ArrayList<PreviewSourceRange>();
|
||||
|
||||
public String getAnswer() {
|
||||
return answer;
|
||||
}
|
||||
|
||||
public void setAnswer(String answer) {
|
||||
this.answer = answer;
|
||||
}
|
||||
|
||||
public Integer getCharCount() {
|
||||
return charCount;
|
||||
}
|
||||
|
||||
public void setCharCount(Integer charCount) {
|
||||
this.charCount = charCount;
|
||||
}
|
||||
|
||||
public String getChunkId() {
|
||||
return chunkId;
|
||||
}
|
||||
|
||||
public void setChunkId(String chunkId) {
|
||||
this.chunkId = chunkId;
|
||||
}
|
||||
|
||||
public String getChunkType() {
|
||||
return chunkType;
|
||||
}
|
||||
|
||||
public void setChunkType(String chunkType) {
|
||||
this.chunkType = chunkType;
|
||||
}
|
||||
|
||||
public String getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
public void setContent(String content) {
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
public List<String> getHeadingPath() {
|
||||
return headingPath;
|
||||
}
|
||||
|
||||
public void setHeadingPath(List<String> headingPath) {
|
||||
this.headingPath = headingPath;
|
||||
}
|
||||
|
||||
public Integer getPartNo() {
|
||||
return partNo;
|
||||
}
|
||||
|
||||
public void setPartNo(Integer partNo) {
|
||||
this.partNo = partNo;
|
||||
}
|
||||
|
||||
public Integer getPartTotal() {
|
||||
return partTotal;
|
||||
}
|
||||
|
||||
public void setPartTotal(Integer partTotal) {
|
||||
this.partTotal = partTotal;
|
||||
}
|
||||
|
||||
public String getQuestion() {
|
||||
return question;
|
||||
}
|
||||
|
||||
public void setQuestion(String question) {
|
||||
this.question = question;
|
||||
}
|
||||
|
||||
public String getSourceLabel() {
|
||||
return sourceLabel;
|
||||
}
|
||||
|
||||
public void setSourceLabel(String sourceLabel) {
|
||||
this.sourceLabel = sourceLabel;
|
||||
}
|
||||
|
||||
public Integer getTokenEstimate() {
|
||||
return tokenEstimate;
|
||||
}
|
||||
|
||||
public void setTokenEstimate(Integer tokenEstimate) {
|
||||
this.tokenEstimate = tokenEstimate;
|
||||
}
|
||||
|
||||
public List<String> getWarnings() {
|
||||
return warnings;
|
||||
}
|
||||
|
||||
public void setWarnings(List<String> warnings) {
|
||||
this.warnings = warnings;
|
||||
}
|
||||
|
||||
public List<PreviewSourceRange> getSourceRanges() {
|
||||
return sourceRanges;
|
||||
}
|
||||
|
||||
public void setSourceRanges(List<PreviewSourceRange> sourceRanges) {
|
||||
this.sourceRanges = sourceRanges;
|
||||
}
|
||||
}
|
||||
|
||||
public static class PreviewFileResult implements Serializable {
|
||||
private String previewSessionId;
|
||||
private String filePath;
|
||||
private String fileName;
|
||||
private String normalizedContent;
|
||||
private String strategyCode;
|
||||
private String strategyLabel;
|
||||
private AnalysisResult analysis;
|
||||
private Integer totalChunks;
|
||||
private Integer totalWarnings;
|
||||
private List<RagChunk> chunks = new ArrayList<RagChunk>();
|
||||
private List<PreviewChunkResult> chunks = new ArrayList<PreviewChunkResult>();
|
||||
|
||||
public String getPreviewSessionId() {
|
||||
return previewSessionId;
|
||||
@@ -276,6 +436,14 @@ public final class DocumentImportDtos {
|
||||
this.fileName = fileName;
|
||||
}
|
||||
|
||||
public String getNormalizedContent() {
|
||||
return normalizedContent;
|
||||
}
|
||||
|
||||
public void setNormalizedContent(String normalizedContent) {
|
||||
this.normalizedContent = normalizedContent;
|
||||
}
|
||||
|
||||
public String getStrategyCode() {
|
||||
return strategyCode;
|
||||
}
|
||||
@@ -316,11 +484,11 @@ public final class DocumentImportDtos {
|
||||
this.totalWarnings = totalWarnings;
|
||||
}
|
||||
|
||||
public List<RagChunk> getChunks() {
|
||||
public List<PreviewChunkResult> getChunks() {
|
||||
return chunks;
|
||||
}
|
||||
|
||||
public void setChunks(List<RagChunk> chunks) {
|
||||
public void setChunks(List<PreviewChunkResult> chunks) {
|
||||
this.chunks = chunks;
|
||||
}
|
||||
}
|
||||
@@ -454,6 +622,7 @@ public final class DocumentImportDtos {
|
||||
public static class PreviewSession implements Serializable {
|
||||
private String sessionId;
|
||||
private BigInteger knowledgeId;
|
||||
private BigInteger documentId;
|
||||
private String filePath;
|
||||
private String fileName;
|
||||
private String sourceFormat;
|
||||
@@ -480,6 +649,14 @@ public final class DocumentImportDtos {
|
||||
this.knowledgeId = knowledgeId;
|
||||
}
|
||||
|
||||
public BigInteger getDocumentId() {
|
||||
return documentId;
|
||||
}
|
||||
|
||||
public void setDocumentId(BigInteger documentId) {
|
||||
this.documentId = documentId;
|
||||
}
|
||||
|
||||
public String getFilePath() {
|
||||
return filePath;
|
||||
}
|
||||
@@ -552,4 +729,265 @@ public final class DocumentImportDtos {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
}
|
||||
|
||||
public static class TaskCreateRequest implements Serializable {
|
||||
private BigInteger knowledgeId;
|
||||
private String filePath;
|
||||
private String fileName;
|
||||
|
||||
public BigInteger getKnowledgeId() {
|
||||
return knowledgeId;
|
||||
}
|
||||
|
||||
public void setKnowledgeId(BigInteger knowledgeId) {
|
||||
this.knowledgeId = knowledgeId;
|
||||
}
|
||||
|
||||
public String getFilePath() {
|
||||
return filePath;
|
||||
}
|
||||
|
||||
public void setFilePath(String filePath) {
|
||||
this.filePath = filePath;
|
||||
}
|
||||
|
||||
public String getFileName() {
|
||||
return fileName;
|
||||
}
|
||||
|
||||
public void setFileName(String fileName) {
|
||||
this.fileName = fileName;
|
||||
}
|
||||
}
|
||||
|
||||
public static class TaskCreateResponse implements Serializable {
|
||||
private BigInteger documentId;
|
||||
private BigInteger taskId;
|
||||
private String processStatus;
|
||||
|
||||
public BigInteger getDocumentId() {
|
||||
return documentId;
|
||||
}
|
||||
|
||||
public void setDocumentId(BigInteger documentId) {
|
||||
this.documentId = documentId;
|
||||
}
|
||||
|
||||
public BigInteger getTaskId() {
|
||||
return taskId;
|
||||
}
|
||||
|
||||
public void setTaskId(BigInteger taskId) {
|
||||
this.taskId = taskId;
|
||||
}
|
||||
|
||||
public String getProcessStatus() {
|
||||
return processStatus;
|
||||
}
|
||||
|
||||
public void setProcessStatus(String processStatus) {
|
||||
this.processStatus = processStatus;
|
||||
}
|
||||
}
|
||||
|
||||
public static class TaskDetailResponse implements Serializable {
|
||||
private BigInteger taskId;
|
||||
private BigInteger documentId;
|
||||
private BigInteger knowledgeId;
|
||||
private String phase;
|
||||
private String status;
|
||||
private String processStatus;
|
||||
private Integer progressPercent;
|
||||
private Integer totalChunks;
|
||||
private Integer completedChunks;
|
||||
private Integer failedChunks;
|
||||
private String providerTaskId;
|
||||
private String errorSummary;
|
||||
private Date startedAt;
|
||||
private Date finishedAt;
|
||||
|
||||
public BigInteger getTaskId() {
|
||||
return taskId;
|
||||
}
|
||||
|
||||
public void setTaskId(BigInteger taskId) {
|
||||
this.taskId = taskId;
|
||||
}
|
||||
|
||||
public BigInteger getDocumentId() {
|
||||
return documentId;
|
||||
}
|
||||
|
||||
public void setDocumentId(BigInteger documentId) {
|
||||
this.documentId = documentId;
|
||||
}
|
||||
|
||||
public BigInteger getKnowledgeId() {
|
||||
return knowledgeId;
|
||||
}
|
||||
|
||||
public void setKnowledgeId(BigInteger knowledgeId) {
|
||||
this.knowledgeId = knowledgeId;
|
||||
}
|
||||
|
||||
public String getPhase() {
|
||||
return phase;
|
||||
}
|
||||
|
||||
public void setPhase(String phase) {
|
||||
this.phase = phase;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getProcessStatus() {
|
||||
return processStatus;
|
||||
}
|
||||
|
||||
public void setProcessStatus(String processStatus) {
|
||||
this.processStatus = processStatus;
|
||||
}
|
||||
|
||||
public Integer getProgressPercent() {
|
||||
return progressPercent;
|
||||
}
|
||||
|
||||
public void setProgressPercent(Integer progressPercent) {
|
||||
this.progressPercent = progressPercent;
|
||||
}
|
||||
|
||||
public Integer getTotalChunks() {
|
||||
return totalChunks;
|
||||
}
|
||||
|
||||
public void setTotalChunks(Integer totalChunks) {
|
||||
this.totalChunks = totalChunks;
|
||||
}
|
||||
|
||||
public Integer getCompletedChunks() {
|
||||
return completedChunks;
|
||||
}
|
||||
|
||||
public void setCompletedChunks(Integer completedChunks) {
|
||||
this.completedChunks = completedChunks;
|
||||
}
|
||||
|
||||
public Integer getFailedChunks() {
|
||||
return failedChunks;
|
||||
}
|
||||
|
||||
public void setFailedChunks(Integer failedChunks) {
|
||||
this.failedChunks = failedChunks;
|
||||
}
|
||||
|
||||
public String getProviderTaskId() {
|
||||
return providerTaskId;
|
||||
}
|
||||
|
||||
public void setProviderTaskId(String providerTaskId) {
|
||||
this.providerTaskId = providerTaskId;
|
||||
}
|
||||
|
||||
public String getErrorSummary() {
|
||||
return errorSummary;
|
||||
}
|
||||
|
||||
public void setErrorSummary(String errorSummary) {
|
||||
this.errorSummary = errorSummary;
|
||||
}
|
||||
|
||||
public Date getStartedAt() {
|
||||
return startedAt;
|
||||
}
|
||||
|
||||
public void setStartedAt(Date startedAt) {
|
||||
this.startedAt = startedAt;
|
||||
}
|
||||
|
||||
public Date getFinishedAt() {
|
||||
return finishedAt;
|
||||
}
|
||||
|
||||
public void setFinishedAt(Date finishedAt) {
|
||||
this.finishedAt = finishedAt;
|
||||
}
|
||||
}
|
||||
|
||||
public static class TaskStartIndexRequest implements Serializable {
|
||||
private BigInteger knowledgeId;
|
||||
private BigInteger documentId;
|
||||
private String previewSessionId;
|
||||
|
||||
public BigInteger getKnowledgeId() {
|
||||
return knowledgeId;
|
||||
}
|
||||
|
||||
public void setKnowledgeId(BigInteger knowledgeId) {
|
||||
this.knowledgeId = knowledgeId;
|
||||
}
|
||||
|
||||
public BigInteger getDocumentId() {
|
||||
return documentId;
|
||||
}
|
||||
|
||||
public void setDocumentId(BigInteger documentId) {
|
||||
this.documentId = documentId;
|
||||
}
|
||||
|
||||
public String getPreviewSessionId() {
|
||||
return previewSessionId;
|
||||
}
|
||||
|
||||
public void setPreviewSessionId(String previewSessionId) {
|
||||
this.previewSessionId = previewSessionId;
|
||||
}
|
||||
}
|
||||
|
||||
public static class TaskStartIndexResponse implements Serializable {
|
||||
private BigInteger taskId;
|
||||
private String processStatus;
|
||||
|
||||
public BigInteger getTaskId() {
|
||||
return taskId;
|
||||
}
|
||||
|
||||
public void setTaskId(BigInteger taskId) {
|
||||
this.taskId = taskId;
|
||||
}
|
||||
|
||||
public String getProcessStatus() {
|
||||
return processStatus;
|
||||
}
|
||||
|
||||
public void setProcessStatus(String processStatus) {
|
||||
this.processStatus = processStatus;
|
||||
}
|
||||
}
|
||||
|
||||
public static class TaskRetryRequest implements Serializable {
|
||||
private BigInteger knowledgeId;
|
||||
private BigInteger documentId;
|
||||
|
||||
public BigInteger getKnowledgeId() {
|
||||
return knowledgeId;
|
||||
}
|
||||
|
||||
public void setKnowledgeId(BigInteger knowledgeId) {
|
||||
this.knowledgeId = knowledgeId;
|
||||
}
|
||||
|
||||
public BigInteger getDocumentId() {
|
||||
return documentId;
|
||||
}
|
||||
|
||||
public void setDocumentId(BigInteger documentId) {
|
||||
this.documentId = documentId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,4 +18,8 @@ public final class DocumentImportKeys {
|
||||
public static final String KEY_DOCUMENT_ANALYSIS_SUMMARY = "splitter.analysisSummary";
|
||||
public static final String KEY_DOCUMENT_SOURCE_FILE_EXT = "splitter.sourceFileExt";
|
||||
public static final String KEY_DOCUMENT_PREVIEW_VERSION = "splitter.previewVersion";
|
||||
public static final String KEY_DOCUMENT_PARSE_BACKEND = "parse.backend";
|
||||
public static final String KEY_DOCUMENT_PARSE_METADATA = "parse.metadata";
|
||||
public static final String KEY_DOCUMENT_PARSE_WARNINGS = "parse.warnings";
|
||||
public static final String KEY_DOCUMENT_PROVIDER_TASK_ID = "parse.providerTaskId";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package tech.easyflow.ai.documentimport.task;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
import tech.easyflow.common.mq.config.MQProperties;
|
||||
import tech.easyflow.common.mq.core.MQConsumerHandler;
|
||||
import tech.easyflow.common.mq.core.MQMessage;
|
||||
import tech.easyflow.common.mq.core.MQSubscription;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 文档向量化任务消费者。
|
||||
*
|
||||
* @author Codex
|
||||
* @since 2026-04-14
|
||||
*/
|
||||
@Component
|
||||
public class DocumentImportIndexTaskConsumer implements MQConsumerHandler {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DocumentImportIndexTaskConsumer.class);
|
||||
|
||||
private final KnowledgeDocumentImportTaskAppService appService;
|
||||
private final MQProperties mqProperties;
|
||||
|
||||
public DocumentImportIndexTaskConsumer(KnowledgeDocumentImportTaskAppService appService,
|
||||
MQProperties mqProperties) {
|
||||
this.appService = appService;
|
||||
this.mqProperties = mqProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MQSubscription subscription() {
|
||||
MQSubscription subscription = new MQSubscription();
|
||||
subscription.setTopic(DocumentImportTaskMqConstants.INDEX_TOPIC);
|
||||
subscription.setConsumerGroup(DocumentImportTaskMqConstants.INDEX_GROUP);
|
||||
subscription.setShardCount(resolveShardCount());
|
||||
return subscription;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(List<MQMessage> messages) {
|
||||
LOG.info("文档向量化消费者收到消息批次: count={}", messages == null ? 0 : messages.size());
|
||||
for (MQMessage message : messages) {
|
||||
DocumentImportTaskMessage event = JSON.parseObject(message.getBody(), DocumentImportTaskMessage.class);
|
||||
if (event == null || event.getTaskId() == null) {
|
||||
LOG.warn("文档向量化消费者跳过非法消息: streamMessageId={}, messageId={}",
|
||||
message == null ? null : message.getStreamMessageId(),
|
||||
message == null ? null : message.getMessageId());
|
||||
continue;
|
||||
}
|
||||
LOG.info("文档向量化消费者开始处理消息: taskId={}, messageId={}, streamMessageId={}",
|
||||
event.getTaskId(), message.getMessageId(), message.getStreamMessageId());
|
||||
try {
|
||||
appService.handleIndexTask(event.getTaskId());
|
||||
LOG.info("文档向量化消费者处理完成: taskId={}, messageId={}, streamMessageId={}",
|
||||
event.getTaskId(), message.getMessageId(), message.getStreamMessageId());
|
||||
} catch (Exception exception) {
|
||||
LOG.error("文档向量化消费者处理失败: taskId={}, messageId={}, streamMessageId={}",
|
||||
event.getTaskId(), message.getMessageId(), message.getStreamMessageId(), exception);
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 向量化消费者需覆盖生产端的所有分片,避免消息落入未订阅分片。
|
||||
*
|
||||
* @return 当前 Redis Stream 分片数
|
||||
*/
|
||||
private int resolveShardCount() {
|
||||
return Math.max(mqProperties.getRedis().getChatPersistShardCount(), 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package tech.easyflow.ai.documentimport.task;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import tech.easyflow.common.mq.core.MQMessage;
|
||||
import tech.easyflow.common.mq.core.MQProducer;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 文档向量化任务消息生产者。
|
||||
*
|
||||
* @author Codex
|
||||
* @since 2026-04-14
|
||||
*/
|
||||
@Service
|
||||
public class DocumentImportIndexTaskProducer {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DocumentImportIndexTaskProducer.class);
|
||||
|
||||
private final MQProducer mqProducer;
|
||||
|
||||
public DocumentImportIndexTaskProducer(MQProducer mqProducer) {
|
||||
this.mqProducer = mqProducer;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送向量化任务消息。
|
||||
*
|
||||
* @param taskId 任务 ID
|
||||
*/
|
||||
public void send(BigInteger taskId) {
|
||||
DocumentImportTaskMessage event = new DocumentImportTaskMessage();
|
||||
event.setTaskId(taskId);
|
||||
event.setOccurredAt(new Date());
|
||||
|
||||
MQMessage message = new MQMessage();
|
||||
message.setMessageId("index-" + taskId);
|
||||
message.setTopic(DocumentImportTaskMqConstants.INDEX_TOPIC);
|
||||
message.setKey(String.valueOf(taskId));
|
||||
message.setCreatedAt(event.getOccurredAt());
|
||||
message.setBody(JSON.toJSONString(event));
|
||||
LOG.info("准备投递文档向量化 MQ 消息: topic={}, taskId={}, messageId={}",
|
||||
message.getTopic(), taskId, message.getMessageId());
|
||||
String recordId = mqProducer.send(message);
|
||||
LOG.info("文档向量化 MQ 消息投递完成: topic={}, taskId={}, messageId={}, recordId={}",
|
||||
message.getTopic(), taskId, message.getMessageId(), recordId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package tech.easyflow.ai.documentimport.task;
|
||||
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 知识库文档解析任务收敛器。
|
||||
*
|
||||
* <p>该调度器只负责轮询运行中的桥接解析任务,不承担提交任务职责。</p>
|
||||
*
|
||||
* @author Codex
|
||||
* @since 2026-04-15
|
||||
*/
|
||||
@Component
|
||||
public class DocumentImportParseMonitor {
|
||||
|
||||
private final KnowledgeDocumentImportTaskAppService appService;
|
||||
|
||||
public DocumentImportParseMonitor(KnowledgeDocumentImportTaskAppService appService) {
|
||||
this.appService = appService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 定时收敛运行中的桥接解析任务状态。
|
||||
*/
|
||||
@Scheduled(
|
||||
fixedDelayString = "${easyflow.ai.document-import.parse-monitor.fixed-delay:3000}",
|
||||
initialDelayString = "${easyflow.ai.document-import.parse-monitor.initial-delay:5000}"
|
||||
)
|
||||
public void reconcileRunningParseTasks() {
|
||||
appService.monitorRunningParseTasks();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package tech.easyflow.ai.documentimport.task;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
import tech.easyflow.common.mq.config.MQProperties;
|
||||
import tech.easyflow.common.mq.core.MQConsumerHandler;
|
||||
import tech.easyflow.common.mq.core.MQMessage;
|
||||
import tech.easyflow.common.mq.core.MQSubscription;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 文档解析任务消费者。
|
||||
*
|
||||
* @author Codex
|
||||
* @since 2026-04-14
|
||||
*/
|
||||
@Component
|
||||
public class DocumentImportParseTaskConsumer implements MQConsumerHandler {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DocumentImportParseTaskConsumer.class);
|
||||
|
||||
private final KnowledgeDocumentImportTaskAppService appService;
|
||||
private final MQProperties mqProperties;
|
||||
|
||||
public DocumentImportParseTaskConsumer(KnowledgeDocumentImportTaskAppService appService,
|
||||
MQProperties mqProperties) {
|
||||
this.appService = appService;
|
||||
this.mqProperties = mqProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MQSubscription subscription() {
|
||||
MQSubscription subscription = new MQSubscription();
|
||||
subscription.setTopic(DocumentImportTaskMqConstants.PARSE_TOPIC);
|
||||
subscription.setConsumerGroup(DocumentImportTaskMqConstants.PARSE_GROUP);
|
||||
subscription.setShardCount(resolveShardCount());
|
||||
return subscription;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(List<MQMessage> messages) {
|
||||
LOG.info("文档解析消费者收到消息批次: count={}", messages == null ? 0 : messages.size());
|
||||
for (MQMessage message : messages) {
|
||||
DocumentImportTaskMessage event = JSON.parseObject(message.getBody(), DocumentImportTaskMessage.class);
|
||||
if (event == null || event.getTaskId() == null) {
|
||||
LOG.warn("文档解析消费者跳过非法消息: streamMessageId={}, messageId={}",
|
||||
message == null ? null : message.getStreamMessageId(),
|
||||
message == null ? null : message.getMessageId());
|
||||
continue;
|
||||
}
|
||||
LOG.info("文档解析消费者开始处理消息: taskId={}, messageId={}, streamMessageId={}",
|
||||
event.getTaskId(), message.getMessageId(), message.getStreamMessageId());
|
||||
try {
|
||||
appService.handleParseTask(event.getTaskId());
|
||||
LOG.info("文档解析消费者处理完成: taskId={}, messageId={}, streamMessageId={}",
|
||||
event.getTaskId(), message.getMessageId(), message.getStreamMessageId());
|
||||
} catch (Exception exception) {
|
||||
LOG.error("文档解析消费者处理失败: taskId={}, messageId={}, streamMessageId={}",
|
||||
event.getTaskId(), message.getMessageId(), message.getStreamMessageId(), exception);
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析消费者需覆盖生产端的所有分片,避免消息落入未订阅分片。
|
||||
*
|
||||
* @return 当前 Redis Stream 分片数
|
||||
*/
|
||||
private int resolveShardCount() {
|
||||
return Math.max(mqProperties.getRedis().getChatPersistShardCount(), 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package tech.easyflow.ai.documentimport.task;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import tech.easyflow.common.mq.core.MQMessage;
|
||||
import tech.easyflow.common.mq.core.MQProducer;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 文档解析任务消息生产者。
|
||||
*
|
||||
* @author Codex
|
||||
* @since 2026-04-14
|
||||
*/
|
||||
@Service
|
||||
public class DocumentImportParseTaskProducer {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DocumentImportParseTaskProducer.class);
|
||||
|
||||
private final MQProducer mqProducer;
|
||||
|
||||
public DocumentImportParseTaskProducer(MQProducer mqProducer) {
|
||||
this.mqProducer = mqProducer;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送解析任务消息。
|
||||
*
|
||||
* @param taskId 任务 ID
|
||||
*/
|
||||
public void send(BigInteger taskId) {
|
||||
DocumentImportTaskMessage event = new DocumentImportTaskMessage();
|
||||
event.setTaskId(taskId);
|
||||
event.setOccurredAt(new Date());
|
||||
|
||||
MQMessage message = new MQMessage();
|
||||
message.setMessageId("parse-" + taskId);
|
||||
message.setTopic(DocumentImportTaskMqConstants.PARSE_TOPIC);
|
||||
message.setKey(String.valueOf(taskId));
|
||||
message.setCreatedAt(event.getOccurredAt());
|
||||
message.setBody(JSON.toJSONString(event));
|
||||
LOG.info("准备投递文档解析 MQ 消息: topic={}, taskId={}, messageId={}",
|
||||
message.getTopic(), taskId, message.getMessageId());
|
||||
String recordId = mqProducer.send(message);
|
||||
LOG.info("文档解析 MQ 消息投递完成: topic={}, taskId={}, messageId={}, recordId={}",
|
||||
message.getTopic(), taskId, message.getMessageId(), recordId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package tech.easyflow.ai.documentimport.task;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 文档导入任务消息。
|
||||
*
|
||||
* @author Codex
|
||||
* @since 2026-04-14
|
||||
*/
|
||||
public class DocumentImportTaskMessage implements Serializable {
|
||||
|
||||
private BigInteger taskId;
|
||||
private Date occurredAt;
|
||||
|
||||
public BigInteger getTaskId() {
|
||||
return taskId;
|
||||
}
|
||||
|
||||
public void setTaskId(BigInteger taskId) {
|
||||
this.taskId = taskId;
|
||||
}
|
||||
|
||||
public Date getOccurredAt() {
|
||||
return occurredAt;
|
||||
}
|
||||
|
||||
public void setOccurredAt(Date occurredAt) {
|
||||
this.occurredAt = occurredAt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package tech.easyflow.ai.documentimport.task;
|
||||
|
||||
/**
|
||||
* 文档导入任务 MQ 常量。
|
||||
*
|
||||
* @author Codex
|
||||
* @since 2026-04-14
|
||||
*/
|
||||
public final class DocumentImportTaskMqConstants {
|
||||
|
||||
private DocumentImportTaskMqConstants() {
|
||||
}
|
||||
|
||||
public static final String PARSE_TOPIC = "knowledge-document-parse";
|
||||
public static final String PARSE_GROUP = "knowledge-document-parse-group";
|
||||
public static final String INDEX_TOPIC = "knowledge-document-index";
|
||||
public static final String INDEX_GROUP = "knowledge-document-index-group";
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package tech.easyflow.ai.documentimport.task;
|
||||
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.support.TransactionSynchronization;
|
||||
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
import tech.easyflow.ai.entity.Document;
|
||||
import tech.easyflow.ai.mapper.DocumentMapper;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigInteger;
|
||||
import java.time.Duration;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 知识库文档任务状态 SSE 推送服务。
|
||||
*
|
||||
* @author Codex
|
||||
* @since 2026-04-15
|
||||
*/
|
||||
@Service
|
||||
public class DocumentImportTaskStatusStreamService {
|
||||
|
||||
private static final long SSE_TIMEOUT_MS = Duration.ofMinutes(30).toMillis();
|
||||
|
||||
private final Map<String, Set<SseEmitter>> knowledgeEmitters = new ConcurrentHashMap<String, Set<SseEmitter>>();
|
||||
|
||||
@Resource
|
||||
private DocumentMapper documentMapper;
|
||||
|
||||
@Resource(name = "sseThreadPool")
|
||||
private ThreadPoolTaskExecutor sseThreadPool;
|
||||
|
||||
/**
|
||||
* 订阅知识库文档任务状态流。
|
||||
*
|
||||
* @param knowledgeId 知识库 ID
|
||||
* @return SSE 连接
|
||||
*/
|
||||
public SseEmitter subscribe(BigInteger knowledgeId) {
|
||||
if (knowledgeId == null) {
|
||||
throw new BusinessException("知识库id不能为空");
|
||||
}
|
||||
String topicKey = toTopicKey(knowledgeId);
|
||||
SseEmitter emitter = new SseEmitter(SSE_TIMEOUT_MS);
|
||||
knowledgeEmitters.computeIfAbsent(topicKey, key -> ConcurrentHashMap.newKeySet()).add(emitter);
|
||||
emitter.onCompletion(() -> removeEmitter(topicKey, emitter));
|
||||
emitter.onTimeout(() -> {
|
||||
removeEmitter(topicKey, emitter);
|
||||
emitter.complete();
|
||||
});
|
||||
emitter.onError(error -> removeEmitter(topicKey, emitter));
|
||||
sendAsync(topicKey, emitter, "connected", buildConnectedPayload(knowledgeId));
|
||||
return emitter;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在事务提交后推送文档任务状态变更。
|
||||
*
|
||||
* @param documentId 文档 ID
|
||||
*/
|
||||
public void publishAfterCommit(BigInteger documentId) {
|
||||
if (documentId == null) {
|
||||
return;
|
||||
}
|
||||
Runnable publishAction = () -> publishNow(documentId);
|
||||
if (TransactionSynchronizationManager.isSynchronizationActive()
|
||||
&& TransactionSynchronizationManager.isActualTransactionActive()) {
|
||||
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
|
||||
@Override
|
||||
public void afterCommit() {
|
||||
publishAction.run();
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
publishAction.run();
|
||||
}
|
||||
|
||||
private void publishNow(BigInteger documentId) {
|
||||
Document document = documentMapper.selectOneById(documentId);
|
||||
if (document == null || document.getCollectionId() == null) {
|
||||
return;
|
||||
}
|
||||
String topicKey = toTopicKey(document.getCollectionId());
|
||||
Set<SseEmitter> emitters = knowledgeEmitters.get(topicKey);
|
||||
if (emitters == null || emitters.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Map<String, Object> payload = buildDocumentPayload(document);
|
||||
for (SseEmitter emitter : emitters) {
|
||||
sendAsync(topicKey, emitter, "document-status", payload);
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, Object> buildConnectedPayload(BigInteger knowledgeId) {
|
||||
Map<String, Object> payload = new LinkedHashMap<String, Object>();
|
||||
payload.put("knowledgeId", knowledgeId.toString());
|
||||
payload.put("type", "connected");
|
||||
return payload;
|
||||
}
|
||||
|
||||
private Map<String, Object> buildDocumentPayload(Document document) {
|
||||
Map<String, Object> payload = new LinkedHashMap<String, Object>();
|
||||
payload.put("type", "document-status");
|
||||
payload.put("knowledgeId", document.getCollectionId() == null ? null : document.getCollectionId().toString());
|
||||
payload.put("documentId", document.getId() == null ? null : document.getId().toString());
|
||||
payload.put("processStatus", document.getProcessStatus());
|
||||
payload.put("progressPercent", document.getProgressPercent());
|
||||
payload.put("totalChunks", document.getTotalChunks());
|
||||
payload.put("completedChunks", document.getCompletedChunks());
|
||||
payload.put("failedChunks", document.getFailedChunks());
|
||||
payload.put("lastTaskError", document.getLastTaskError());
|
||||
payload.put("taskModifiedAt", document.getTaskModifiedAt());
|
||||
return payload;
|
||||
}
|
||||
|
||||
private void sendAsync(String topicKey, SseEmitter emitter, String eventName, Map<String, Object> payload) {
|
||||
sseThreadPool.execute(() -> {
|
||||
try {
|
||||
emitter.send(
|
||||
SseEmitter.event()
|
||||
.name(eventName)
|
||||
.data(payload, MediaType.APPLICATION_JSON)
|
||||
);
|
||||
} catch (Exception e) {
|
||||
removeEmitter(topicKey, emitter);
|
||||
try {
|
||||
emitter.completeWithError(e);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void removeEmitter(String topicKey, SseEmitter emitter) {
|
||||
Set<SseEmitter> emitters = knowledgeEmitters.get(topicKey);
|
||||
if (emitters == null) {
|
||||
return;
|
||||
}
|
||||
emitters.remove(emitter);
|
||||
if (emitters.isEmpty()) {
|
||||
knowledgeEmitters.remove(topicKey);
|
||||
}
|
||||
}
|
||||
|
||||
private String toTopicKey(BigInteger knowledgeId) {
|
||||
return String.valueOf(knowledgeId);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,40 @@
|
||||
package tech.easyflow.ai.dto;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* API 分享授权请求。
|
||||
*/
|
||||
public class KnowledgeShareApiGrantRequest implements Serializable {
|
||||
|
||||
private BigInteger apiKeyId;
|
||||
private BigInteger knowledgeId;
|
||||
private Set<String> actionScopes;
|
||||
|
||||
public BigInteger getApiKeyId() {
|
||||
return apiKeyId;
|
||||
}
|
||||
|
||||
public void setApiKeyId(BigInteger apiKeyId) {
|
||||
this.apiKeyId = apiKeyId;
|
||||
}
|
||||
|
||||
public BigInteger getKnowledgeId() {
|
||||
return knowledgeId;
|
||||
}
|
||||
|
||||
public void setKnowledgeId(BigInteger knowledgeId) {
|
||||
this.knowledgeId = knowledgeId;
|
||||
}
|
||||
|
||||
public Set<String> getActionScopes() {
|
||||
return actionScopes;
|
||||
}
|
||||
|
||||
public void setActionScopes(Set<String> actionScopes) {
|
||||
this.actionScopes = actionScopes;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package tech.easyflow.ai.dto;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigInteger;
|
||||
|
||||
/**
|
||||
* 分享页受限配置更新请求。
|
||||
*/
|
||||
public class KnowledgeShareLimitedConfigRequest implements Serializable {
|
||||
|
||||
private BigInteger knowledgeId;
|
||||
private BigInteger vectorEmbedModelId;
|
||||
private BigInteger rerankModelId;
|
||||
private Boolean rerankEnable;
|
||||
private Integer docRecallMaxNum;
|
||||
private Double simThreshold;
|
||||
private Boolean rebuildVectors;
|
||||
|
||||
public BigInteger getKnowledgeId() {
|
||||
return knowledgeId;
|
||||
}
|
||||
|
||||
public void setKnowledgeId(BigInteger knowledgeId) {
|
||||
this.knowledgeId = knowledgeId;
|
||||
}
|
||||
|
||||
public BigInteger getVectorEmbedModelId() {
|
||||
return vectorEmbedModelId;
|
||||
}
|
||||
|
||||
public void setVectorEmbedModelId(BigInteger vectorEmbedModelId) {
|
||||
this.vectorEmbedModelId = vectorEmbedModelId;
|
||||
}
|
||||
|
||||
public BigInteger getRerankModelId() {
|
||||
return rerankModelId;
|
||||
}
|
||||
|
||||
public void setRerankModelId(BigInteger rerankModelId) {
|
||||
this.rerankModelId = rerankModelId;
|
||||
}
|
||||
|
||||
public Boolean getRerankEnable() {
|
||||
return rerankEnable;
|
||||
}
|
||||
|
||||
public void setRerankEnable(Boolean rerankEnable) {
|
||||
this.rerankEnable = rerankEnable;
|
||||
}
|
||||
|
||||
public Integer getDocRecallMaxNum() {
|
||||
return docRecallMaxNum;
|
||||
}
|
||||
|
||||
public void setDocRecallMaxNum(Integer docRecallMaxNum) {
|
||||
this.docRecallMaxNum = docRecallMaxNum;
|
||||
}
|
||||
|
||||
public Double getSimThreshold() {
|
||||
return simThreshold;
|
||||
}
|
||||
|
||||
public void setSimThreshold(Double simThreshold) {
|
||||
this.simThreshold = simThreshold;
|
||||
}
|
||||
|
||||
public Boolean getRebuildVectors() {
|
||||
return rebuildVectors;
|
||||
}
|
||||
|
||||
public void setRebuildVectors(Boolean rebuildVectors) {
|
||||
this.rebuildVectors = rebuildVectors;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.stereotype.Component;
|
||||
import tech.easyflow.ai.easyagentsflow.support.PublishedWorkflowDefinitionIds;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
import tech.easyflow.ai.entity.WorkflowExecResult;
|
||||
import tech.easyflow.ai.entity.WorkflowExecStep;
|
||||
@@ -62,7 +63,11 @@ public class ChainEventListenerForSave implements ChainEventListener {
|
||||
log.info("ChainStartEvent: {}", event);
|
||||
ChainDefinition definition = chain.getDefinition();
|
||||
ChainState state = chain.getState();
|
||||
Workflow workflow = workflowService.getById(definition.getId());
|
||||
Workflow workflow = resolveWorkflow(definition);
|
||||
if (workflow == null) {
|
||||
log.error("ChainStartEvent: workflow not found, definitionId={}", definition.getId());
|
||||
return;
|
||||
}
|
||||
String instanceId = state.getInstanceId();
|
||||
WorkflowExecResult existed = workflowExecResultService.getByExecKey(instanceId);
|
||||
if (existed != null) {
|
||||
@@ -176,4 +181,26 @@ public class ChainEventListenerForSave implements ChainEventListener {
|
||||
ChainState chainState = chain.getChainStateRepository().load(parentInstanceId);
|
||||
return findAncestorState(chainState, chain);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据定义 ID 解析当前执行所对应的工作流。
|
||||
* 已发布快照执行会使用 published 前缀,需要先还原为真实工作流 ID。
|
||||
*/
|
||||
private Workflow resolveWorkflow(ChainDefinition definition) {
|
||||
if (definition == null || StrUtil.isBlank(definition.getId())) {
|
||||
return null;
|
||||
}
|
||||
String definitionId = definition.getId();
|
||||
String workflowId = PublishedWorkflowDefinitionIds.unwrap(definitionId);
|
||||
try {
|
||||
java.math.BigInteger id = new java.math.BigInteger(workflowId);
|
||||
if (PublishedWorkflowDefinitionIds.isPublished(definitionId)) {
|
||||
return workflowService.getPublishedById(id);
|
||||
}
|
||||
return workflowService.getById(id);
|
||||
} catch (NumberFormatException ex) {
|
||||
log.error("Unsupported workflow definition id: {}", definitionId, ex);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,11 @@ import org.springframework.util.StringUtils;
|
||||
import tech.easyflow.ai.easyagentsflow.entity.WorkflowCheckIssue;
|
||||
import tech.easyflow.ai.easyagentsflow.entity.WorkflowCheckResult;
|
||||
import tech.easyflow.ai.easyagentsflow.entity.WorkflowCheckStage;
|
||||
import tech.easyflow.ai.entity.PluginItem;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
import tech.easyflow.ai.plugin.workflow.dependency.WorkflowPluginDependencyService;
|
||||
import tech.easyflow.ai.plugin.workflow.snapshot.WorkflowPluginSnapshotResolver;
|
||||
import tech.easyflow.ai.service.PluginItemService;
|
||||
import tech.easyflow.ai.service.WorkflowService;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.datacenter.execution.model.DatacenterSchemaResponse;
|
||||
@@ -40,6 +44,7 @@ public class WorkflowCheckService {
|
||||
private static final String TYPE_END = "endNode";
|
||||
private static final String TYPE_LOOP = "loopNode";
|
||||
private static final String TYPE_WORKFLOW = "workflow-node";
|
||||
private static final String TYPE_PLUGIN = "plugin-node";
|
||||
|
||||
@Resource
|
||||
private WorkflowService workflowService;
|
||||
@@ -47,6 +52,12 @@ public class WorkflowCheckService {
|
||||
private ChainParser chainParser;
|
||||
@Resource
|
||||
private WorkflowDatacenterContentService workflowDatacenterContentService;
|
||||
@Resource
|
||||
private WorkflowPluginDependencyService workflowPluginDependencyService;
|
||||
@Resource
|
||||
private PluginItemService pluginItemService;
|
||||
@Resource
|
||||
private WorkflowPluginSnapshotResolver workflowPluginSnapshotResolver;
|
||||
|
||||
public WorkflowCheckResult checkWorkflow(BigInteger workflowId, WorkflowCheckStage stage) {
|
||||
if (workflowId == null) {
|
||||
@@ -66,6 +77,9 @@ public class WorkflowCheckService {
|
||||
List<WorkflowCheckIssue> issues = new ArrayList<>();
|
||||
Set<String> issueKeys = new LinkedHashSet<>();
|
||||
ParsedWorkflow parsedWorkflow = parseAndCheckBase(content, issues, issueKeys);
|
||||
if (parsedWorkflow != null) {
|
||||
checkPluginSchemaHashes(parsedWorkflow, issues, issueKeys);
|
||||
}
|
||||
|
||||
if (stage == WorkflowCheckStage.PRE_EXECUTE && parsedWorkflow != null) {
|
||||
runStrictChecks(content, parsedWorkflow, currentWorkflowId, issues, issueKeys);
|
||||
@@ -394,6 +408,10 @@ public class WorkflowCheckService {
|
||||
|
||||
for (NodeView node : parsed.nodes) {
|
||||
if (!TYPE_WORKFLOW.equals(node.type)) {
|
||||
if (!TYPE_PLUGIN.equals(node.type)) {
|
||||
continue;
|
||||
}
|
||||
checkPluginWorkflowReference(node, currentWorkflowIdString, currentContent, contentCache, issues, issueKeys);
|
||||
continue;
|
||||
}
|
||||
String workflowId = getWorkflowIdInNode(node);
|
||||
@@ -510,12 +528,86 @@ public class WorkflowCheckService {
|
||||
refs.add(workflowId);
|
||||
}
|
||||
}
|
||||
refs.addAll(workflowPluginDependencyService.extractWorkflowIdsFromPluginNodes(content));
|
||||
} catch (Exception ignored) {
|
||||
// ignore
|
||||
}
|
||||
return refs;
|
||||
}
|
||||
|
||||
private void checkPluginWorkflowReference(NodeView node,
|
||||
String currentWorkflowIdString,
|
||||
String currentContent,
|
||||
Map<String, String> contentCache,
|
||||
List<WorkflowCheckIssue> issues,
|
||||
Set<String> issueKeys) {
|
||||
String pluginWorkflowId = getWorkflowIdInPluginNode(node);
|
||||
if (!StringUtils.hasText(pluginWorkflowId)) {
|
||||
addIssue(issues, issueKeys, "PLUGIN_WORKFLOW_REF_NOT_FOUND",
|
||||
"插件节点未绑定有效工作流插件", node.id, null, node.name);
|
||||
return;
|
||||
}
|
||||
if (StringUtils.hasText(currentWorkflowIdString) && currentWorkflowIdString.equals(pluginWorkflowId)) {
|
||||
addIssue(issues, issueKeys, "PLUGIN_WORKFLOW_REF_CYCLE",
|
||||
"插件递归引用:工作流不能通过插件引用自身", node.id, null, node.name);
|
||||
return;
|
||||
}
|
||||
String workflowContent = loadWorkflowContent(pluginWorkflowId, currentWorkflowIdString, currentContent, contentCache);
|
||||
if (!StringUtils.hasText(workflowContent)) {
|
||||
addIssue(issues, issueKeys, "PLUGIN_WORKFLOW_REF_NOT_FOUND",
|
||||
"插件绑定工作流不存在: " + pluginWorkflowId, node.id, null, node.name);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkPluginSchemaHashes(ParsedWorkflow parsed,
|
||||
List<WorkflowCheckIssue> issues,
|
||||
Set<String> issueKeys) {
|
||||
for (NodeView node : parsed.nodes) {
|
||||
if (!TYPE_PLUGIN.equals(node.type) || node.data == null) {
|
||||
continue;
|
||||
}
|
||||
String pluginItemId = trimToNull(node.data.getString("pluginId"));
|
||||
if (!StringUtils.hasText(pluginItemId)) {
|
||||
continue;
|
||||
}
|
||||
String workflowId = workflowPluginDependencyService.resolveWorkflowIdByPluginItemId(pluginItemId);
|
||||
if (!StringUtils.hasText(workflowId)) {
|
||||
continue;
|
||||
}
|
||||
String latestSchemaHash = resolveLatestPluginSchemaHash(pluginItemId, workflowId);
|
||||
if (!StringUtils.hasText(latestSchemaHash)) {
|
||||
continue;
|
||||
}
|
||||
String currentSchemaHash = trimToNull(node.data.getString("schemaHash"));
|
||||
if (!latestSchemaHash.equals(currentSchemaHash)) {
|
||||
addIssue(issues, issueKeys, "PLUGIN_SCHEMA_OUTDATED",
|
||||
"当前插件节点绑定工作流的已发布参数契约已更新,请重新选择插件同步节点定义",
|
||||
node.id, null, node.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String getWorkflowIdInPluginNode(NodeView node) {
|
||||
if (node == null || node.data == null) {
|
||||
return null;
|
||||
}
|
||||
return workflowPluginDependencyService.resolveWorkflowIdByPluginItemId(
|
||||
trimToNull(node.data.getString("pluginId"))
|
||||
);
|
||||
}
|
||||
|
||||
private String resolveLatestPluginSchemaHash(String pluginItemId, String workflowId) {
|
||||
PluginItem pluginItem = pluginItemService.getById(pluginItemId);
|
||||
if (pluginItem != null && StringUtils.hasText(pluginItem.getSchemaHash())) {
|
||||
return pluginItem.getSchemaHash();
|
||||
}
|
||||
Workflow workflow = workflowService.getPublishedById(new BigInteger(workflowId));
|
||||
if (workflow == null) {
|
||||
return null;
|
||||
}
|
||||
return workflowPluginSnapshotResolver.resolveSchemaHash(workflow);
|
||||
}
|
||||
|
||||
private String formatCyclePath(LinkedHashSet<String> visiting, String cycleStart) {
|
||||
List<String> chain = new ArrayList<>();
|
||||
boolean started = false;
|
||||
|
||||
@@ -29,6 +29,9 @@ public class Bot extends BotBase {
|
||||
@Column(ignore = true)
|
||||
private String displayPublishStatus;
|
||||
|
||||
@Column(ignore = true)
|
||||
private String createdByName;
|
||||
|
||||
public boolean isAnonymousEnabled() {
|
||||
Map<String, Object> options = getOptions();
|
||||
if (options == null) {
|
||||
@@ -62,4 +65,22 @@ public class Bot extends BotBase {
|
||||
this.displayPublishStatus = displayPublishStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取创建人展示名称。
|
||||
*
|
||||
* @return 创建人展示名称
|
||||
*/
|
||||
public String getCreatedByName() {
|
||||
return createdByName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置创建人展示名称。
|
||||
*
|
||||
* @param createdByName 创建人展示名称
|
||||
*/
|
||||
public void setCreatedByName(String createdByName) {
|
||||
this.createdByName = createdByName;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -55,4 +55,16 @@ public class Document extends DocumentBase {
|
||||
public void setOverlapSize(int overlapSize) {
|
||||
this.overlapSize = overlapSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取列表展示时的优先分块数。
|
||||
*
|
||||
* @return 分块数
|
||||
*/
|
||||
public long getDisplayChunkCount() {
|
||||
if (getTotalChunks() != null && getTotalChunks() > 0) {
|
||||
return getTotalChunks();
|
||||
}
|
||||
return chunkCount == null ? 0L : chunkCount.longValue();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,9 @@ public class DocumentCollection extends DocumentCollectionBase implements Visibi
|
||||
@Column(ignore = true)
|
||||
private String displayPublishStatus;
|
||||
|
||||
@Column(ignore = true)
|
||||
private String createdByName;
|
||||
|
||||
public static final String TYPE_DOCUMENT = "DOCUMENT";
|
||||
public static final String TYPE_FAQ = "FAQ";
|
||||
|
||||
@@ -169,4 +172,22 @@ public class DocumentCollection extends DocumentCollectionBase implements Visibi
|
||||
public void setDisplayPublishStatus(String displayPublishStatus) {
|
||||
this.displayPublishStatus = displayPublishStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取创建人展示名称。
|
||||
*
|
||||
* @return 创建人展示名称
|
||||
*/
|
||||
public String getCreatedByName() {
|
||||
return createdByName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置创建人展示名称。
|
||||
*
|
||||
* @param createdByName 创建人展示名称
|
||||
*/
|
||||
public void setCreatedByName(String createdByName) {
|
||||
this.createdByName = createdByName;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
package tech.easyflow.ai.entity;
|
||||
|
||||
import com.mybatisflex.annotation.Column;
|
||||
import com.mybatisflex.annotation.Id;
|
||||
import com.mybatisflex.annotation.KeyType;
|
||||
import com.mybatisflex.annotation.Table;
|
||||
import com.mybatisflex.core.handler.FastjsonTypeHandler;
|
||||
import tech.easyflow.common.entity.DateEntity;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 知识库文档导入任务。
|
||||
*
|
||||
* @author Codex
|
||||
* @since 2026-04-14
|
||||
*/
|
||||
@Table(value = "tb_document_import_task", comment = "知识库文档导入任务")
|
||||
public class DocumentImportTask extends DateEntity implements Serializable {
|
||||
|
||||
@Id(keyType = KeyType.Generator, value = "snowFlakeId")
|
||||
private BigInteger id;
|
||||
|
||||
@Column(comment = "文档ID")
|
||||
private BigInteger documentId;
|
||||
|
||||
@Column(comment = "知识库ID")
|
||||
private BigInteger knowledgeId;
|
||||
|
||||
@Column(comment = "任务阶段")
|
||||
private String phase;
|
||||
|
||||
@Column(comment = "任务状态")
|
||||
private String status;
|
||||
|
||||
@Column(comment = "底层任务ID")
|
||||
private String providerTaskId;
|
||||
|
||||
@Column(typeHandler = FastjsonTypeHandler.class, comment = "任务载荷")
|
||||
private Map<String, Object> payloadJson;
|
||||
|
||||
@Column(comment = "错误摘要")
|
||||
private String errorSummary;
|
||||
|
||||
@Column(comment = "开始时间")
|
||||
private Date startedAt;
|
||||
|
||||
@Column(comment = "结束时间")
|
||||
private Date finishedAt;
|
||||
|
||||
@Column(comment = "创建时间")
|
||||
private Date created;
|
||||
|
||||
@Column(comment = "创建人")
|
||||
private BigInteger createdBy;
|
||||
|
||||
@Column(comment = "修改时间")
|
||||
private Date modified;
|
||||
|
||||
@Column(comment = "修改人")
|
||||
private BigInteger modifiedBy;
|
||||
|
||||
public BigInteger getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(BigInteger id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public BigInteger getDocumentId() {
|
||||
return documentId;
|
||||
}
|
||||
|
||||
public void setDocumentId(BigInteger documentId) {
|
||||
this.documentId = documentId;
|
||||
}
|
||||
|
||||
public BigInteger getKnowledgeId() {
|
||||
return knowledgeId;
|
||||
}
|
||||
|
||||
public void setKnowledgeId(BigInteger knowledgeId) {
|
||||
this.knowledgeId = knowledgeId;
|
||||
}
|
||||
|
||||
public String getPhase() {
|
||||
return phase;
|
||||
}
|
||||
|
||||
public void setPhase(String phase) {
|
||||
this.phase = phase;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getProviderTaskId() {
|
||||
return providerTaskId;
|
||||
}
|
||||
|
||||
public void setProviderTaskId(String providerTaskId) {
|
||||
this.providerTaskId = providerTaskId;
|
||||
}
|
||||
|
||||
public Map<String, Object> getPayloadJson() {
|
||||
return payloadJson == null ? new LinkedHashMap<String, Object>() : payloadJson;
|
||||
}
|
||||
|
||||
public void setPayloadJson(Map<String, Object> payloadJson) {
|
||||
this.payloadJson = payloadJson == null ? new LinkedHashMap<String, Object>() : payloadJson;
|
||||
}
|
||||
|
||||
public String getErrorSummary() {
|
||||
return errorSummary;
|
||||
}
|
||||
|
||||
public void setErrorSummary(String errorSummary) {
|
||||
this.errorSummary = errorSummary;
|
||||
}
|
||||
|
||||
public Date getStartedAt() {
|
||||
return startedAt;
|
||||
}
|
||||
|
||||
public void setStartedAt(Date startedAt) {
|
||||
this.startedAt = startedAt;
|
||||
}
|
||||
|
||||
public Date getFinishedAt() {
|
||||
return finishedAt;
|
||||
}
|
||||
|
||||
public void setFinishedAt(Date finishedAt) {
|
||||
this.finishedAt = finishedAt;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Date getCreated() {
|
||||
return created;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCreated(Date created) {
|
||||
this.created = created;
|
||||
}
|
||||
|
||||
public BigInteger getCreatedBy() {
|
||||
return createdBy;
|
||||
}
|
||||
|
||||
public void setCreatedBy(BigInteger createdBy) {
|
||||
this.createdBy = createdBy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Date getModified() {
|
||||
return modified;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setModified(Date modified) {
|
||||
this.modified = modified;
|
||||
}
|
||||
|
||||
public BigInteger getModifiedBy() {
|
||||
return modifiedBy;
|
||||
}
|
||||
|
||||
public void setModifiedBy(BigInteger modifiedBy) {
|
||||
this.modifiedBy = modifiedBy;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package tech.easyflow.ai.entity;
|
||||
|
||||
import com.mybatisflex.annotation.Column;
|
||||
import com.mybatisflex.annotation.Table;
|
||||
import tech.easyflow.ai.entity.base.KnowledgeShareBase;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 知识库分享记录。
|
||||
*/
|
||||
@Table("tb_knowledge_share")
|
||||
public class KnowledgeShare extends KnowledgeShareBase {
|
||||
|
||||
@Column(ignore = true)
|
||||
private String shareUrl;
|
||||
|
||||
/**
|
||||
* 解析授权范围。
|
||||
*
|
||||
* @return 授权范围集合
|
||||
*/
|
||||
public Set<String> getPermissionScopeSet() {
|
||||
if (getPermissionSet() == null || getPermissionSet().isBlank()) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
Set<String> scopes = new LinkedHashSet<>();
|
||||
String[] segments = getPermissionSet().split(",");
|
||||
for (String segment : segments) {
|
||||
if (segment == null || segment.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
scopes.add(segment.trim().toUpperCase());
|
||||
}
|
||||
return scopes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入授权范围。
|
||||
*
|
||||
* @param scopes 授权范围集合
|
||||
*/
|
||||
public void setPermissionScopes(Iterable<String> scopes) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
if (scopes != null) {
|
||||
for (String scope : scopes) {
|
||||
if (scope == null || scope.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
if (builder.length() > 0) {
|
||||
builder.append(',');
|
||||
}
|
||||
builder.append(scope.trim().toUpperCase());
|
||||
}
|
||||
}
|
||||
setPermissionSet(builder.toString());
|
||||
}
|
||||
|
||||
public String getShareUrl() {
|
||||
return shareUrl;
|
||||
}
|
||||
|
||||
public void setShareUrl(String shareUrl) {
|
||||
this.shareUrl = shareUrl;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,21 @@ public class Plugin extends PluginBase {
|
||||
@RelationOneToMany(selfField = "id", targetField = "pluginId", targetTable = "tb_plugin_item")
|
||||
private List<PluginItem> tools;
|
||||
|
||||
@com.mybatisflex.annotation.Column(ignore = true)
|
||||
private String workflowTitle;
|
||||
|
||||
@com.mybatisflex.annotation.Column(ignore = true)
|
||||
private Boolean available;
|
||||
|
||||
@com.mybatisflex.annotation.Column(ignore = true)
|
||||
private String reasonCode;
|
||||
|
||||
@com.mybatisflex.annotation.Column(ignore = true)
|
||||
private String reasonMessage;
|
||||
|
||||
@com.mybatisflex.annotation.Column(ignore = true)
|
||||
private String createdByName;
|
||||
|
||||
public String getTitle() {
|
||||
return this.getName();
|
||||
}
|
||||
@@ -30,4 +45,54 @@ public class Plugin extends PluginBase {
|
||||
public void setTools(List<PluginItem> tools) {
|
||||
this.tools = tools;
|
||||
}
|
||||
|
||||
public String getWorkflowTitle() {
|
||||
return workflowTitle;
|
||||
}
|
||||
|
||||
public void setWorkflowTitle(String workflowTitle) {
|
||||
this.workflowTitle = workflowTitle;
|
||||
}
|
||||
|
||||
public Boolean getAvailable() {
|
||||
return available;
|
||||
}
|
||||
|
||||
public void setAvailable(Boolean available) {
|
||||
this.available = available;
|
||||
}
|
||||
|
||||
public String getReasonCode() {
|
||||
return reasonCode;
|
||||
}
|
||||
|
||||
public void setReasonCode(String reasonCode) {
|
||||
this.reasonCode = reasonCode;
|
||||
}
|
||||
|
||||
public String getReasonMessage() {
|
||||
return reasonMessage;
|
||||
}
|
||||
|
||||
public void setReasonMessage(String reasonMessage) {
|
||||
this.reasonMessage = reasonMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取创建人展示名称。
|
||||
*
|
||||
* @return 创建人展示名称
|
||||
*/
|
||||
public String getCreatedByName() {
|
||||
return createdByName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置创建人展示名称。
|
||||
*
|
||||
* @param createdByName 创建人展示名称
|
||||
*/
|
||||
public void setCreatedByName(String createdByName) {
|
||||
this.createdByName = createdByName;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,9 @@ public class Workflow extends WorkflowBase implements VisibilityResource {
|
||||
@Column(ignore = true)
|
||||
private String displayPublishStatus;
|
||||
|
||||
@Column(ignore = true)
|
||||
private String createdByName;
|
||||
|
||||
public Tool toFunction(boolean needEnglishName) {
|
||||
return new WorkflowTool(this, needEnglishName);
|
||||
}
|
||||
@@ -57,4 +60,22 @@ public class Workflow extends WorkflowBase implements VisibilityResource {
|
||||
public void setDisplayPublishStatus(String displayPublishStatus) {
|
||||
this.displayPublishStatus = displayPublishStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取创建人展示名称。
|
||||
*
|
||||
* @return 创建人展示名称
|
||||
*/
|
||||
public String getCreatedByName() {
|
||||
return createdByName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置创建人展示名称。
|
||||
*
|
||||
* @param createdByName 创建人展示名称
|
||||
*/
|
||||
public void setCreatedByName(String createdByName) {
|
||||
this.createdByName = createdByName;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +72,48 @@ public class DocumentBase extends DateEntity implements Serializable {
|
||||
@Column(typeHandler = FastjsonTypeHandler.class, comment = "其他配置项")
|
||||
private Map<String, Object> options;
|
||||
|
||||
/**
|
||||
* 处理状态
|
||||
*/
|
||||
@Column(comment = "处理状态")
|
||||
private String processStatus;
|
||||
|
||||
/**
|
||||
* 总分块数
|
||||
*/
|
||||
@Column(comment = "总分块数")
|
||||
private Integer totalChunks;
|
||||
|
||||
/**
|
||||
* 已完成分块数
|
||||
*/
|
||||
@Column(comment = "已完成分块数")
|
||||
private Integer completedChunks;
|
||||
|
||||
/**
|
||||
* 失败分块数
|
||||
*/
|
||||
@Column(comment = "失败分块数")
|
||||
private Integer failedChunks;
|
||||
|
||||
/**
|
||||
* 处理进度百分比
|
||||
*/
|
||||
@Column(comment = "处理进度百分比")
|
||||
private Integer progressPercent;
|
||||
|
||||
/**
|
||||
* 最近任务错误摘要
|
||||
*/
|
||||
@Column(comment = "最近任务错误摘要")
|
||||
private String lastTaskError;
|
||||
|
||||
/**
|
||||
* 任务状态更新时间
|
||||
*/
|
||||
@Column(comment = "任务状态更新时间")
|
||||
private Date taskModifiedAt;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@@ -176,6 +218,62 @@ public class DocumentBase extends DateEntity implements Serializable {
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
public String getProcessStatus() {
|
||||
return processStatus;
|
||||
}
|
||||
|
||||
public void setProcessStatus(String processStatus) {
|
||||
this.processStatus = processStatus;
|
||||
}
|
||||
|
||||
public Integer getTotalChunks() {
|
||||
return totalChunks;
|
||||
}
|
||||
|
||||
public void setTotalChunks(Integer totalChunks) {
|
||||
this.totalChunks = totalChunks;
|
||||
}
|
||||
|
||||
public Integer getCompletedChunks() {
|
||||
return completedChunks;
|
||||
}
|
||||
|
||||
public void setCompletedChunks(Integer completedChunks) {
|
||||
this.completedChunks = completedChunks;
|
||||
}
|
||||
|
||||
public Integer getFailedChunks() {
|
||||
return failedChunks;
|
||||
}
|
||||
|
||||
public void setFailedChunks(Integer failedChunks) {
|
||||
this.failedChunks = failedChunks;
|
||||
}
|
||||
|
||||
public Integer getProgressPercent() {
|
||||
return progressPercent;
|
||||
}
|
||||
|
||||
public void setProgressPercent(Integer progressPercent) {
|
||||
this.progressPercent = progressPercent;
|
||||
}
|
||||
|
||||
public String getLastTaskError() {
|
||||
return lastTaskError;
|
||||
}
|
||||
|
||||
public void setLastTaskError(String lastTaskError) {
|
||||
this.lastTaskError = lastTaskError;
|
||||
}
|
||||
|
||||
public Date getTaskModifiedAt() {
|
||||
return taskModifiedAt;
|
||||
}
|
||||
|
||||
public void setTaskModifiedAt(Date taskModifiedAt) {
|
||||
this.taskModifiedAt = taskModifiedAt;
|
||||
}
|
||||
|
||||
public Date getCreated() {
|
||||
return created;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
package tech.easyflow.ai.entity.base;
|
||||
|
||||
import com.mybatisflex.annotation.Column;
|
||||
import com.mybatisflex.annotation.Id;
|
||||
import com.mybatisflex.annotation.KeyType;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 知识库分享记录基础字段。
|
||||
*/
|
||||
public class KnowledgeShareBase implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id(keyType = KeyType.Generator, value = "snowFlakeId", comment = "ID")
|
||||
private BigInteger id;
|
||||
|
||||
@Column(comment = "知识库ID")
|
||||
private BigInteger knowledgeId;
|
||||
|
||||
@Column(comment = "分享类型")
|
||||
private String shareType;
|
||||
|
||||
@Column(comment = "分享访问密钥哈希")
|
||||
private String shareKeyHash;
|
||||
|
||||
@Column(comment = "分享状态")
|
||||
private String status;
|
||||
|
||||
@Column(comment = "授权范围")
|
||||
private String permissionSet;
|
||||
|
||||
@Column(comment = "过期时间")
|
||||
private Date expiresAt;
|
||||
|
||||
@Column(tenantId = true, comment = "租户ID")
|
||||
private BigInteger tenantId;
|
||||
|
||||
@Column(comment = "部门ID")
|
||||
private BigInteger deptId;
|
||||
|
||||
@Column(comment = "创建时间")
|
||||
private Date created;
|
||||
|
||||
@Column(comment = "创建人")
|
||||
private BigInteger createdBy;
|
||||
|
||||
@Column(comment = "修改时间")
|
||||
private Date modified;
|
||||
|
||||
@Column(comment = "修改人")
|
||||
private BigInteger modifiedBy;
|
||||
|
||||
public BigInteger getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(BigInteger id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public BigInteger getKnowledgeId() {
|
||||
return knowledgeId;
|
||||
}
|
||||
|
||||
public void setKnowledgeId(BigInteger knowledgeId) {
|
||||
this.knowledgeId = knowledgeId;
|
||||
}
|
||||
|
||||
public String getShareType() {
|
||||
return shareType;
|
||||
}
|
||||
|
||||
public void setShareType(String shareType) {
|
||||
this.shareType = shareType;
|
||||
}
|
||||
|
||||
public String getShareKeyHash() {
|
||||
return shareKeyHash;
|
||||
}
|
||||
|
||||
public void setShareKeyHash(String shareKeyHash) {
|
||||
this.shareKeyHash = shareKeyHash;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getPermissionSet() {
|
||||
return permissionSet;
|
||||
}
|
||||
|
||||
public void setPermissionSet(String permissionSet) {
|
||||
this.permissionSet = permissionSet;
|
||||
}
|
||||
|
||||
public Date getExpiresAt() {
|
||||
return expiresAt;
|
||||
}
|
||||
|
||||
public void setExpiresAt(Date expiresAt) {
|
||||
this.expiresAt = expiresAt;
|
||||
}
|
||||
|
||||
public BigInteger getTenantId() {
|
||||
return tenantId;
|
||||
}
|
||||
|
||||
public void setTenantId(BigInteger tenantId) {
|
||||
this.tenantId = tenantId;
|
||||
}
|
||||
|
||||
public BigInteger getDeptId() {
|
||||
return deptId;
|
||||
}
|
||||
|
||||
public void setDeptId(BigInteger deptId) {
|
||||
this.deptId = deptId;
|
||||
}
|
||||
|
||||
public Date getCreated() {
|
||||
return created;
|
||||
}
|
||||
|
||||
public void setCreated(Date created) {
|
||||
this.created = created;
|
||||
}
|
||||
|
||||
public BigInteger getCreatedBy() {
|
||||
return createdBy;
|
||||
}
|
||||
|
||||
public void setCreatedBy(BigInteger createdBy) {
|
||||
this.createdBy = createdBy;
|
||||
}
|
||||
|
||||
public Date getModified() {
|
||||
return modified;
|
||||
}
|
||||
|
||||
public void setModified(Date modified) {
|
||||
this.modified = modified;
|
||||
}
|
||||
|
||||
public BigInteger getModifiedBy() {
|
||||
return modifiedBy;
|
||||
}
|
||||
|
||||
public void setModifiedBy(BigInteger modifiedBy) {
|
||||
this.modifiedBy = modifiedBy;
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,12 @@ public class PluginBase implements Serializable {
|
||||
@Column(comment = "类型")
|
||||
private Integer type;
|
||||
|
||||
/**
|
||||
* 绑定工作流ID
|
||||
*/
|
||||
@Column(comment = "绑定工作流ID")
|
||||
private BigInteger workflowId;
|
||||
|
||||
/**
|
||||
* 基础URL
|
||||
*/
|
||||
@@ -148,6 +154,14 @@ public class PluginBase implements Serializable {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public BigInteger getWorkflowId() {
|
||||
return workflowId;
|
||||
}
|
||||
|
||||
public void setWorkflowId(BigInteger workflowId) {
|
||||
this.workflowId = workflowId;
|
||||
}
|
||||
|
||||
public String getBaseUrl() {
|
||||
return baseUrl;
|
||||
}
|
||||
|
||||
@@ -90,6 +90,12 @@ public class PluginItemBase implements Serializable {
|
||||
@Column(comment = "英文名称")
|
||||
private String englishName;
|
||||
|
||||
/**
|
||||
* 工作流插件输入输出契约哈希
|
||||
*/
|
||||
@Column(comment = "工作流插件输入输出契约哈希")
|
||||
private String schemaHash;
|
||||
|
||||
public BigInteger getId() {
|
||||
return id;
|
||||
}
|
||||
@@ -194,4 +200,12 @@ public class PluginItemBase implements Serializable {
|
||||
this.englishName = englishName;
|
||||
}
|
||||
|
||||
public String getSchemaHash() {
|
||||
return schemaHash;
|
||||
}
|
||||
|
||||
public void setSchemaHash(String schemaHash) {
|
||||
this.schemaHash = schemaHash;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package tech.easyflow.ai.enums;
|
||||
|
||||
/**
|
||||
* 文档导入任务阶段。
|
||||
*
|
||||
* @author Codex
|
||||
* @since 2026-04-14
|
||||
*/
|
||||
public enum DocumentImportTaskPhase {
|
||||
|
||||
/**
|
||||
* 文档解析阶段。
|
||||
*/
|
||||
PARSE,
|
||||
|
||||
/**
|
||||
* 向量化阶段。
|
||||
*/
|
||||
INDEX
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package tech.easyflow.ai.enums;
|
||||
|
||||
/**
|
||||
* 文档导入任务状态。
|
||||
*
|
||||
* @author Codex
|
||||
* @since 2026-04-14
|
||||
*/
|
||||
public enum DocumentImportTaskStatus {
|
||||
|
||||
/**
|
||||
* 已创建,等待执行。
|
||||
*/
|
||||
PENDING,
|
||||
|
||||
/**
|
||||
* 正在执行。
|
||||
*/
|
||||
RUNNING,
|
||||
|
||||
/**
|
||||
* 执行失败。
|
||||
*/
|
||||
FAILED,
|
||||
|
||||
/**
|
||||
* 执行完成。
|
||||
*/
|
||||
COMPLETED
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package tech.easyflow.ai.enums;
|
||||
|
||||
/**
|
||||
* 文档处理状态。
|
||||
*
|
||||
* @author Codex
|
||||
* @since 2026-04-14
|
||||
*/
|
||||
public enum DocumentProcessStatus {
|
||||
|
||||
/**
|
||||
* 已上传,尚未进入异步处理。
|
||||
*/
|
||||
UPLOADED,
|
||||
|
||||
/**
|
||||
* 解析中。
|
||||
*/
|
||||
PARSING,
|
||||
|
||||
/**
|
||||
* 解析失败。
|
||||
*/
|
||||
PARSE_FAILED,
|
||||
|
||||
/**
|
||||
* 可继续配置分块。
|
||||
*/
|
||||
READY_FOR_SEGMENT,
|
||||
|
||||
/**
|
||||
* 已确认分块,可开始向量化。
|
||||
*/
|
||||
READY_FOR_INDEX,
|
||||
|
||||
/**
|
||||
* 向量化处理中。
|
||||
*/
|
||||
INDEXING,
|
||||
|
||||
/**
|
||||
* 向量化失败。
|
||||
*/
|
||||
INDEX_FAILED,
|
||||
|
||||
/**
|
||||
* 全流程完成。
|
||||
*/
|
||||
COMPLETED;
|
||||
|
||||
/**
|
||||
* 判断当前状态是否属于运行中状态。
|
||||
*
|
||||
* @return 是否运行中
|
||||
*/
|
||||
public boolean isProcessing() {
|
||||
return this == PARSING || this == INDEXING;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package tech.easyflow.ai.enums;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 知识库分享动作范围。
|
||||
*/
|
||||
public enum KnowledgeShareActionScope {
|
||||
|
||||
VIEW,
|
||||
SEARCH,
|
||||
CONTENT_CREATE,
|
||||
CONTENT_UPDATE,
|
||||
CONTENT_DELETE,
|
||||
IMPORT_EXPORT,
|
||||
CONFIG_UPDATE;
|
||||
|
||||
/**
|
||||
* 解析前端提交的动作范围集合。
|
||||
*
|
||||
* @param values 原始动作范围
|
||||
* @return 规范化后的动作范围集合
|
||||
*/
|
||||
public static Set<String> normalize(Iterable<String> values) {
|
||||
Set<String> scopes = new LinkedHashSet<>();
|
||||
if (values == null) {
|
||||
return scopes;
|
||||
}
|
||||
for (String value : values) {
|
||||
if (value == null || value.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
scopes.add(KnowledgeShareActionScope.valueOf(value.trim().toUpperCase()).name());
|
||||
}
|
||||
return scopes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取默认 URL 分享授权范围。
|
||||
*
|
||||
* @return 默认授权范围
|
||||
*/
|
||||
public static Set<String> defaultUrlScopes() {
|
||||
return new LinkedHashSet<>(Arrays.asList(
|
||||
VIEW.name(),
|
||||
SEARCH.name(),
|
||||
CONTENT_CREATE.name(),
|
||||
CONTENT_UPDATE.name(),
|
||||
CONTENT_DELETE.name(),
|
||||
IMPORT_EXPORT.name(),
|
||||
CONFIG_UPDATE.name()
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取默认 API 分享授权范围。
|
||||
*
|
||||
* <p>产品上固定开放查看、检索、新增、更新、导入导出,不提供删除能力。</p>
|
||||
*
|
||||
* @return 默认 API 授权范围
|
||||
*/
|
||||
public static Set<String> defaultApiScopes() {
|
||||
return new LinkedHashSet<>(Arrays.asList(
|
||||
VIEW.name(),
|
||||
SEARCH.name(),
|
||||
CONTENT_CREATE.name(),
|
||||
CONTENT_UPDATE.name(),
|
||||
IMPORT_EXPORT.name()
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package tech.easyflow.ai.enums;
|
||||
|
||||
/**
|
||||
* 知识库分享状态。
|
||||
*/
|
||||
public enum KnowledgeShareStatus {
|
||||
ENABLED,
|
||||
DISABLED,
|
||||
REVOKED
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package tech.easyflow.ai.enums;
|
||||
|
||||
/**
|
||||
* 知识库分享类型。
|
||||
*/
|
||||
public enum KnowledgeShareType {
|
||||
URL
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package tech.easyflow.ai.enums;
|
||||
|
||||
/**
|
||||
* 插件类型枚举。
|
||||
*/
|
||||
public enum PluginType {
|
||||
|
||||
HTTP(1),
|
||||
WORKFLOW(2);
|
||||
|
||||
private final int code;
|
||||
|
||||
PluginType(int code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取类型编码。
|
||||
*
|
||||
* @return 类型编码
|
||||
*/
|
||||
public int getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据编码解析类型,空值或未知值均按 HTTP 处理,兼容历史数据。
|
||||
*
|
||||
* @param code 类型编码
|
||||
* @return 插件类型
|
||||
*/
|
||||
public static PluginType from(Integer code) {
|
||||
if (code != null) {
|
||||
for (PluginType value : values()) {
|
||||
if (value.code == code) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return HTTP;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为工作流插件。
|
||||
*
|
||||
* @param code 类型编码
|
||||
* @return 是否为工作流插件
|
||||
*/
|
||||
public static boolean isWorkflow(Integer code) {
|
||||
return from(code) == WORKFLOW;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package tech.easyflow.ai.mapper;
|
||||
|
||||
import com.mybatisflex.core.BaseMapper;
|
||||
import tech.easyflow.ai.entity.DocumentImportTask;
|
||||
|
||||
/**
|
||||
* 文档导入任务映射层。
|
||||
*
|
||||
* @author Codex
|
||||
* @since 2026-04-14
|
||||
*/
|
||||
public interface DocumentImportTaskMapper extends BaseMapper<DocumentImportTask> {
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package tech.easyflow.ai.mapper;
|
||||
|
||||
import com.mybatisflex.core.BaseMapper;
|
||||
import tech.easyflow.ai.entity.KnowledgeShare;
|
||||
|
||||
/**
|
||||
* 知识库分享记录映射层。
|
||||
*/
|
||||
public interface KnowledgeShareMapper extends BaseMapper<KnowledgeShare> {
|
||||
}
|
||||
|
||||
@@ -4,28 +4,35 @@ import com.easyagents.core.util.StringUtil;
|
||||
import com.easyagents.flow.core.chain.Chain;
|
||||
import com.easyagents.flow.core.chain.Parameter;
|
||||
import com.easyagents.flow.core.node.BaseNode;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import tech.easyflow.ai.utils.DocUtil;
|
||||
import tech.easyflow.common.util.SpringContextUtil;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 工作流文件内容提取节点。
|
||||
*
|
||||
* <p>节点输入为统一文件对象,PDF 交给统一文档解析桥接服务,
|
||||
* 其他类型继续走默认文档读取器。</p>
|
||||
*
|
||||
* @author Codex
|
||||
* @since 2026-04-14
|
||||
*/
|
||||
public class DocNode extends BaseNode {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(DocNode.class);
|
||||
|
||||
/**
|
||||
* 执行文件内容提取。
|
||||
*
|
||||
* @param chain 当前流程链
|
||||
* @return 节点输出
|
||||
*/
|
||||
@Override
|
||||
public Map<String, Object> execute(Chain chain) {
|
||||
Map<String, Object> map = chain.getState().resolveParameters(this);
|
||||
Map<String, Object> res = new HashMap<>();
|
||||
String url = map.get("fileUrl").toString();
|
||||
byte[] bytes = DocUtil.downloadFile(url);
|
||||
ReaderManager manager = SpringContextUtil.getBean(ReaderManager.class);
|
||||
String docContent = manager.getReader().read(DocUtil.getFileNameByUrl(url), new ByteArrayInputStream(bytes));
|
||||
DocNodeFileContentExtractor extractor = SpringContextUtil.getBean(DocNodeFileContentExtractor.class);
|
||||
String docContent = extractor.extract(map.get("file"));
|
||||
|
||||
String key = "content";
|
||||
List<Parameter> outputDefs = getOutputDefs();
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
package tech.easyflow.ai.node;
|
||||
|
||||
import cn.hutool.http.HttpUtil;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.stereotype.Component;
|
||||
import tech.easyflow.ai.document.model.DocumentParseScenario;
|
||||
import tech.easyflow.ai.document.model.DocumentParsedResult;
|
||||
import tech.easyflow.ai.document.model.DocumentSourceRef;
|
||||
import tech.easyflow.ai.document.service.DocumentParseBridgeService;
|
||||
import tech.easyflow.ai.utils.DocUtil;
|
||||
import tech.easyflow.common.filestorage.FileStorageService;
|
||||
import tech.easyflow.common.util.StringUtil;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* {@link DocNode} 文件内容提取器。
|
||||
*
|
||||
* <p>负责把工作流运行态中的文件对象转换为统一文档源,并根据文件类型选择
|
||||
* 统一文档解析桥接服务或默认读取器。</p>
|
||||
*
|
||||
* @author Codex
|
||||
* @since 2026-04-14
|
||||
*/
|
||||
@Component
|
||||
public class DocNodeFileContentExtractor {
|
||||
|
||||
private final DocumentParseBridgeService documentParseBridgeService;
|
||||
private final FileStorageService fileStorageService;
|
||||
private final ReaderManager readerManager;
|
||||
|
||||
/**
|
||||
* 创建文件内容提取器。
|
||||
*
|
||||
* @param documentParseBridgeService 统一文档解析桥接服务
|
||||
* @param fileStorageService 文件存储服务
|
||||
* @param readerManager 默认读取器管理器
|
||||
*/
|
||||
public DocNodeFileContentExtractor(DocumentParseBridgeService documentParseBridgeService,
|
||||
@Qualifier("default") FileStorageService fileStorageService,
|
||||
ReaderManager readerManager) {
|
||||
this.documentParseBridgeService = documentParseBridgeService;
|
||||
this.fileStorageService = fileStorageService;
|
||||
this.readerManager = readerManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取文件文本内容。
|
||||
*
|
||||
* @param fileValue 工作流运行态中的文件对象
|
||||
* @return 可供下游节点直接消费的文本
|
||||
*/
|
||||
public String extract(Object fileValue) {
|
||||
DocumentSourceRef sourceRef = toDocumentSourceRef(fileValue);
|
||||
validateSourceRef(sourceRef);
|
||||
if (isPdf(sourceRef)) {
|
||||
return extractPdfContent(sourceRef);
|
||||
}
|
||||
return extractDefaultContent(sourceRef);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将运行时文件值转换为统一文档源。
|
||||
*
|
||||
* @param fileValue 运行时文件值
|
||||
* @return 文档源
|
||||
*/
|
||||
DocumentSourceRef toDocumentSourceRef(Object fileValue) {
|
||||
if (fileValue instanceof DocumentSourceRef sourceRef) {
|
||||
return sourceRef;
|
||||
}
|
||||
if (!(fileValue instanceof Map<?, ?> fileMap)) {
|
||||
throw new BusinessException("文件输入格式不正确,必须为文件对象");
|
||||
}
|
||||
DocumentSourceRef sourceRef = new DocumentSourceRef();
|
||||
sourceRef.setFileName(asText(fileMap.get("fileName")));
|
||||
sourceRef.setFilePath(asText(fileMap.get("filePath")));
|
||||
sourceRef.setContentType(asText(fileMap.get("contentType")));
|
||||
sourceRef.setUrl(asText(fileMap.get("url")));
|
||||
sourceRef.setSize(asLong(fileMap.get("size")));
|
||||
return sourceRef;
|
||||
}
|
||||
|
||||
private void validateSourceRef(DocumentSourceRef sourceRef) {
|
||||
if (sourceRef == null) {
|
||||
throw new BusinessException("文件输入不能为空");
|
||||
}
|
||||
if (!StringUtil.hasText(sourceRef.getFileName())) {
|
||||
throw new BusinessException("文件输入缺少 fileName");
|
||||
}
|
||||
if (!StringUtil.hasText(sourceRef.getFilePath())) {
|
||||
throw new BusinessException("文件输入缺少 filePath");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isPdf(DocumentSourceRef sourceRef) {
|
||||
if (StringUtil.hasText(sourceRef.getContentType())
|
||||
&& sourceRef.getContentType().toLowerCase().contains("pdf")) {
|
||||
return true;
|
||||
}
|
||||
String fileName = sourceRef.getFileName();
|
||||
if (!StringUtil.hasText(fileName) || !fileName.contains(".")) {
|
||||
return false;
|
||||
}
|
||||
return "pdf".equals(DocUtil.normalizeSuffix(DocUtil.getSuffix(fileName)));
|
||||
}
|
||||
|
||||
private String extractPdfContent(DocumentSourceRef sourceRef) {
|
||||
DocumentParsedResult parsedResult = documentParseBridgeService.parse(sourceRef, DocumentParseScenario.WORKFLOW_TEXT);
|
||||
String preferredText = parsedResult == null ? null : parsedResult.getPreferredText();
|
||||
if (StringUtil.hasText(preferredText)) {
|
||||
return preferredText;
|
||||
}
|
||||
if (parsedResult != null && StringUtil.hasText(parsedResult.getMarkdown())) {
|
||||
return parsedResult.getMarkdown();
|
||||
}
|
||||
if (parsedResult != null && StringUtil.hasText(parsedResult.getPlainText())) {
|
||||
return parsedResult.getPlainText();
|
||||
}
|
||||
throw new BusinessException("PDF 文档解析结果为空");
|
||||
}
|
||||
|
||||
private String extractDefaultContent(DocumentSourceRef sourceRef) {
|
||||
try (InputStream inputStream = openInputStream(sourceRef)) {
|
||||
return readerManager.getReader().read(sourceRef.getFileName(), inputStream);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("读取文件内容失败: " + sourceRef.getFilePath(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private InputStream openInputStream(DocumentSourceRef sourceRef) throws IOException {
|
||||
String filePath = sourceRef.getFilePath();
|
||||
if (StringUtil.hasText(filePath) && isRemoteUrl(filePath)) {
|
||||
byte[] bytes = HttpUtil.downloadBytes(filePath);
|
||||
return new java.io.ByteArrayInputStream(bytes);
|
||||
}
|
||||
if (StringUtil.hasText(filePath)) {
|
||||
return fileStorageService.readStream(filePath);
|
||||
}
|
||||
if (StringUtil.hasText(sourceRef.getUrl())) {
|
||||
byte[] bytes = HttpUtil.downloadBytes(sourceRef.getUrl());
|
||||
return new java.io.ByteArrayInputStream(bytes);
|
||||
}
|
||||
throw new IOException("文件输入缺少可读取路径");
|
||||
}
|
||||
|
||||
private boolean isRemoteUrl(String value) {
|
||||
return value.startsWith("http://") || value.startsWith("https://");
|
||||
}
|
||||
|
||||
private String asText(Object value) {
|
||||
return value == null ? null : String.valueOf(value).trim();
|
||||
}
|
||||
|
||||
private Long asLong(Object value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value instanceof Number number) {
|
||||
return number.longValue();
|
||||
}
|
||||
if (value instanceof String text && StringUtil.hasText(text)) {
|
||||
return Long.parseLong(text.trim());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,23 @@ import com.easyagents.core.model.chat.tool.Tool;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.easyagents.flow.core.chain.Chain;
|
||||
import com.easyagents.flow.core.node.BaseNode;
|
||||
import tech.easyflow.ai.entity.Plugin;
|
||||
import tech.easyflow.ai.entity.PluginItem;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
import tech.easyflow.ai.enums.PluginType;
|
||||
import tech.easyflow.ai.plugin.workflow.availability.WorkflowPluginAvailabilityDecision;
|
||||
import tech.easyflow.ai.plugin.workflow.availability.WorkflowPluginAvailabilityService;
|
||||
import tech.easyflow.ai.plugin.workflow.snapshot.WorkflowPluginSnapshotResolver;
|
||||
import tech.easyflow.ai.service.PluginService;
|
||||
import tech.easyflow.ai.service.PluginItemService;
|
||||
import tech.easyflow.ai.service.WorkflowService;
|
||||
import tech.easyflow.ai.utils.WorkFlowUtil;
|
||||
import tech.easyflow.common.constant.Constants;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.util.SpringContextUtil;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -32,6 +44,11 @@ public class PluginToolNode extends BaseNode {
|
||||
if (tool == null) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
PluginService pluginService = SpringContextUtil.getBean(PluginService.class);
|
||||
Plugin plugin = pluginService.getById(tool.getPluginId());
|
||||
if (plugin != null && PluginType.isWorkflow(plugin.getType())) {
|
||||
return executeWorkflowPlugin(chain, map, plugin);
|
||||
}
|
||||
Tool function = tool.toFunction();
|
||||
if (function == null) {
|
||||
return Collections.emptyMap();
|
||||
@@ -49,6 +66,43 @@ public class PluginToolNode extends BaseNode {
|
||||
return JSON.parseObject(JSON.toJSONString(result), Map.class);
|
||||
}
|
||||
|
||||
private Map<String, Object> executeWorkflowPlugin(Chain chain, Map<String, Object> map, Plugin plugin) {
|
||||
WorkflowPluginAvailabilityService availabilityService =
|
||||
SpringContextUtil.getBean(WorkflowPluginAvailabilityService.class);
|
||||
LoginAccount operator = WorkFlowUtil.getOperator(chain);
|
||||
WorkflowPluginAvailabilityDecision decision = availabilityService.evaluate(plugin, operator);
|
||||
if (!decision.isAvailable()) {
|
||||
return buildSkippedResult(decision);
|
||||
}
|
||||
WorkflowService workflowService = SpringContextUtil.getBean(WorkflowService.class);
|
||||
Workflow workflow = workflowService.getPublishedById(plugin.getWorkflowId());
|
||||
if (workflow == null) {
|
||||
return buildSkippedResult(decision);
|
||||
}
|
||||
WorkflowPluginSnapshotResolver snapshotResolver = SpringContextUtil.getBean(WorkflowPluginSnapshotResolver.class);
|
||||
Map<String, Object> workflowVariables = new LinkedHashMap<>();
|
||||
workflowVariables.put(Constants.LOGIN_USER_KEY, operator);
|
||||
if (map != null && !map.isEmpty()) {
|
||||
workflowVariables.putAll(map);
|
||||
}
|
||||
Object result = snapshotResolver.buildWorkflowTool(workflow).invoke(workflowVariables);
|
||||
if (result == null) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
if (result instanceof Map<?, ?> resultMap) {
|
||||
return (Map<String, Object>) resultMap;
|
||||
}
|
||||
return JSON.parseObject(JSON.toJSONString(result), Map.class);
|
||||
}
|
||||
|
||||
private Map<String, Object> buildSkippedResult(WorkflowPluginAvailabilityDecision decision) {
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("skipped", true);
|
||||
result.put("reasonCode", decision.getReasonCode());
|
||||
result.put("reasonMessage", decision.getReasonMessage());
|
||||
return result;
|
||||
}
|
||||
|
||||
public BigInteger getPluginId() {
|
||||
return pluginId;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package tech.easyflow.ai.plugin.workflow.availability;
|
||||
|
||||
/**
|
||||
* 工作流插件实时可用性判定结果。
|
||||
*/
|
||||
public class WorkflowPluginAvailabilityDecision {
|
||||
|
||||
private boolean visible;
|
||||
|
||||
private boolean available;
|
||||
|
||||
private boolean snapshotPresent;
|
||||
|
||||
private String reasonCode;
|
||||
|
||||
private String reasonMessage;
|
||||
|
||||
private String workflowTitle;
|
||||
|
||||
public boolean isVisible() {
|
||||
return visible;
|
||||
}
|
||||
|
||||
public void setVisible(boolean visible) {
|
||||
this.visible = visible;
|
||||
}
|
||||
|
||||
public boolean isAvailable() {
|
||||
return available;
|
||||
}
|
||||
|
||||
public void setAvailable(boolean available) {
|
||||
this.available = available;
|
||||
}
|
||||
|
||||
public boolean isSnapshotPresent() {
|
||||
return snapshotPresent;
|
||||
}
|
||||
|
||||
public void setSnapshotPresent(boolean snapshotPresent) {
|
||||
this.snapshotPresent = snapshotPresent;
|
||||
}
|
||||
|
||||
public String getReasonCode() {
|
||||
return reasonCode;
|
||||
}
|
||||
|
||||
public void setReasonCode(String reasonCode) {
|
||||
this.reasonCode = reasonCode;
|
||||
}
|
||||
|
||||
public String getReasonMessage() {
|
||||
return reasonMessage;
|
||||
}
|
||||
|
||||
public void setReasonMessage(String reasonMessage) {
|
||||
this.reasonMessage = reasonMessage;
|
||||
}
|
||||
|
||||
public String getWorkflowTitle() {
|
||||
return workflowTitle;
|
||||
}
|
||||
|
||||
public void setWorkflowTitle(String workflowTitle) {
|
||||
this.workflowTitle = workflowTitle;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package tech.easyflow.ai.plugin.workflow.availability;
|
||||
|
||||
import tech.easyflow.ai.entity.Plugin;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
|
||||
/**
|
||||
* 工作流插件可用性判定服务。
|
||||
*/
|
||||
public interface WorkflowPluginAvailabilityService {
|
||||
|
||||
/**
|
||||
* 计算当前登录用户视角下的工作流插件可见性与可用性。
|
||||
*
|
||||
* @param plugin 插件
|
||||
* @return 判定结果
|
||||
*/
|
||||
WorkflowPluginAvailabilityDecision evaluateForCurrentUser(Plugin plugin);
|
||||
|
||||
WorkflowPluginAvailabilityDecision evaluate(Plugin plugin, LoginAccount loginAccount);
|
||||
|
||||
/**
|
||||
* 判断当前用户是否可在管理页继续看到不可用插件。
|
||||
*
|
||||
* @param plugin 插件
|
||||
* @return 是否允许在管理视角保留展示
|
||||
*/
|
||||
boolean canViewUnavailableInManagement(Plugin plugin);
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package tech.easyflow.ai.plugin.workflow.availability;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import tech.easyflow.ai.entity.Plugin;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
import tech.easyflow.ai.enums.PluginType;
|
||||
import tech.easyflow.ai.enums.PublishStatus;
|
||||
import tech.easyflow.ai.plugin.workflow.snapshot.WorkflowPluginSnapshotResolver;
|
||||
import tech.easyflow.ai.service.WorkflowService;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceAction;
|
||||
import tech.easyflow.system.service.CategoryPermissionService;
|
||||
import tech.easyflow.system.service.ResourceAccessService;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 工作流插件实时可用性判定实现。
|
||||
*/
|
||||
@Service
|
||||
public class WorkflowPluginAvailabilityServiceImpl implements WorkflowPluginAvailabilityService {
|
||||
|
||||
private final WorkflowService workflowService;
|
||||
private final ResourceAccessService resourceAccessService;
|
||||
private final CategoryPermissionService categoryPermissionService;
|
||||
private final WorkflowPluginSnapshotResolver workflowPluginSnapshotResolver;
|
||||
|
||||
public WorkflowPluginAvailabilityServiceImpl(WorkflowService workflowService,
|
||||
ResourceAccessService resourceAccessService,
|
||||
CategoryPermissionService categoryPermissionService,
|
||||
WorkflowPluginSnapshotResolver workflowPluginSnapshotResolver) {
|
||||
this.workflowService = workflowService;
|
||||
this.resourceAccessService = resourceAccessService;
|
||||
this.categoryPermissionService = categoryPermissionService;
|
||||
this.workflowPluginSnapshotResolver = workflowPluginSnapshotResolver;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public WorkflowPluginAvailabilityDecision evaluateForCurrentUser(Plugin plugin) {
|
||||
return evaluate(plugin, SaTokenUtil.getLoginAccount());
|
||||
}
|
||||
|
||||
@Override
|
||||
public WorkflowPluginAvailabilityDecision evaluate(Plugin plugin, LoginAccount loginAccount) {
|
||||
WorkflowPluginAvailabilityDecision decision = new WorkflowPluginAvailabilityDecision();
|
||||
decision.setVisible(true);
|
||||
decision.setAvailable(true);
|
||||
if (plugin == null || !PluginType.isWorkflow(plugin.getType())) {
|
||||
return decision;
|
||||
}
|
||||
BigInteger workflowId = plugin.getWorkflowId();
|
||||
if (workflowId == null) {
|
||||
return unavailable(decision, "WORKFLOW_BINDING_MISSING", "当前插件未绑定工作流");
|
||||
}
|
||||
|
||||
Workflow workflow = workflowService.getById(workflowId);
|
||||
if (workflow == null) {
|
||||
return unavailable(decision, "WORKFLOW_NOT_FOUND", "当前插件绑定的工作流不存在");
|
||||
}
|
||||
decision.setWorkflowTitle(workflow.getTitle());
|
||||
|
||||
Map<String, Object> snapshot = workflow.getPublishedSnapshotJson();
|
||||
boolean snapshotPresent = !CollectionUtils.isEmpty(snapshot);
|
||||
decision.setSnapshotPresent(snapshotPresent);
|
||||
if (!snapshotPresent) {
|
||||
return unavailable(decision, "WORKFLOW_SNAPSHOT_MISSING", "当前插件绑定工作流没有可用发布快照");
|
||||
}
|
||||
|
||||
PublishStatus publishStatus = PublishStatus.from(workflow.getPublishStatus());
|
||||
if (!publishStatus.isExternallyVisible()) {
|
||||
return unavailable(decision, "WORKFLOW_OFFLINE", "当前节点绑定工作流已下线");
|
||||
}
|
||||
Workflow publishedWorkflow = workflowService.toPublishedView(workflow);
|
||||
if (!workflowPluginSnapshotResolver.isSupportedForWorkflowPlugin(publishedWorkflow)) {
|
||||
return unavailable(decision, "WORKFLOW_MULTI_END_UNSUPPORTED", "当前节点绑定工作流包含多个结束节点,暂不支持作为插件使用");
|
||||
}
|
||||
|
||||
if (!resourceAccessService.canAccess(loginAccount, CategoryResourceType.WORKFLOW, workflow, ResourceAction.USE)) {
|
||||
return unavailable(decision, "WORKFLOW_NO_PERMISSION", "当前用户无权使用目标工作流");
|
||||
}
|
||||
return decision;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public boolean canViewUnavailableInManagement(Plugin plugin) {
|
||||
if (plugin == null) {
|
||||
return false;
|
||||
}
|
||||
if (categoryPermissionService.isCurrentSuperAdmin()) {
|
||||
return true;
|
||||
}
|
||||
LoginAccount loginAccount = SaTokenUtil.getLoginAccount();
|
||||
if (loginAccount == null || loginAccount.getId() == null || plugin.getCreatedBy() == null) {
|
||||
return false;
|
||||
}
|
||||
return loginAccount.getId().equals(BigInteger.valueOf(plugin.getCreatedBy()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记为不可见且不可用。
|
||||
*
|
||||
* @param decision 判定结果
|
||||
* @param reasonCode 原因编码
|
||||
* @param reasonMessage 原因说明
|
||||
* @return 判定结果
|
||||
*/
|
||||
private WorkflowPluginAvailabilityDecision unavailable(WorkflowPluginAvailabilityDecision decision,
|
||||
String reasonCode,
|
||||
String reasonMessage) {
|
||||
decision.setVisible(false);
|
||||
decision.setAvailable(false);
|
||||
decision.setReasonCode(reasonCode);
|
||||
decision.setReasonMessage(reasonMessage);
|
||||
return decision;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package tech.easyflow.ai.plugin.workflow.binding;
|
||||
|
||||
import tech.easyflow.ai.entity.Plugin;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
/**
|
||||
* 工作流插件绑定服务。
|
||||
*/
|
||||
public interface WorkflowPluginBindingService {
|
||||
|
||||
/**
|
||||
* 创建工作流插件并生成系统维护工具。
|
||||
*
|
||||
* @param plugin 插件
|
||||
* @return 已保存插件
|
||||
*/
|
||||
Plugin saveWorkflowPlugin(Plugin plugin);
|
||||
|
||||
/**
|
||||
* 更新工作流插件并刷新系统维护工具。
|
||||
*
|
||||
* @param plugin 插件
|
||||
* @return 是否更新成功
|
||||
*/
|
||||
boolean updateWorkflowPlugin(Plugin plugin);
|
||||
|
||||
/**
|
||||
* 同步某个工作流关联的所有工作流插件。
|
||||
*
|
||||
* @param workflowId 工作流 ID
|
||||
*/
|
||||
void syncByWorkflowId(BigInteger workflowId);
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
package tech.easyflow.ai.plugin.workflow.binding;
|
||||
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
import tech.easyflow.ai.entity.Plugin;
|
||||
import tech.easyflow.ai.entity.PluginItem;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
import tech.easyflow.ai.enums.PluginType;
|
||||
import tech.easyflow.ai.enums.PublishStatus;
|
||||
import tech.easyflow.ai.mapper.PluginItemMapper;
|
||||
import tech.easyflow.ai.mapper.PluginMapper;
|
||||
import tech.easyflow.ai.plugin.workflow.dependency.WorkflowPluginDependencyService;
|
||||
import tech.easyflow.ai.plugin.workflow.snapshot.WorkflowPluginSnapshotResolver;
|
||||
import tech.easyflow.ai.service.WorkflowService;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceAction;
|
||||
import tech.easyflow.system.service.ResourceAccessService;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 工作流插件绑定服务实现。
|
||||
*/
|
||||
@Service
|
||||
public class WorkflowPluginBindingServiceImpl implements WorkflowPluginBindingService {
|
||||
|
||||
private final PluginMapper pluginMapper;
|
||||
private final PluginItemMapper pluginItemMapper;
|
||||
private final WorkflowService workflowService;
|
||||
private final ResourceAccessService resourceAccessService;
|
||||
private final WorkflowPluginSnapshotResolver workflowPluginSnapshotResolver;
|
||||
private final WorkflowPluginDependencyService workflowPluginDependencyService;
|
||||
|
||||
public WorkflowPluginBindingServiceImpl(PluginMapper pluginMapper,
|
||||
PluginItemMapper pluginItemMapper,
|
||||
WorkflowService workflowService,
|
||||
ResourceAccessService resourceAccessService,
|
||||
WorkflowPluginSnapshotResolver workflowPluginSnapshotResolver,
|
||||
WorkflowPluginDependencyService workflowPluginDependencyService) {
|
||||
this.pluginMapper = pluginMapper;
|
||||
this.pluginItemMapper = pluginItemMapper;
|
||||
this.workflowService = workflowService;
|
||||
this.resourceAccessService = resourceAccessService;
|
||||
this.workflowPluginSnapshotResolver = workflowPluginSnapshotResolver;
|
||||
this.workflowPluginDependencyService = workflowPluginDependencyService;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Plugin saveWorkflowPlugin(Plugin plugin) {
|
||||
LoginAccount loginAccount = requireLogin();
|
||||
Workflow workflow = requirePublishedWorkflow(plugin.getWorkflowId(), "无权限绑定工作流");
|
||||
normalizeWorkflowPlugin(plugin, loginAccount);
|
||||
int insert = pluginMapper.insert(plugin);
|
||||
if (insert <= 0) {
|
||||
throw new BusinessException("保存失败");
|
||||
}
|
||||
PluginItem pluginItem = new PluginItem();
|
||||
pluginItem.setCreated(new Date());
|
||||
workflowPluginSnapshotResolver.syncPluginItemFromPublishedWorkflow(plugin, pluginItem, workflow.getId());
|
||||
if (pluginItemMapper.insert(pluginItem) <= 0) {
|
||||
throw new BusinessException("保存工作流插件工具失败");
|
||||
}
|
||||
return plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean updateWorkflowPlugin(Plugin plugin) {
|
||||
if (plugin.getId() == null) {
|
||||
throw new BusinessException("插件ID不能为空");
|
||||
}
|
||||
Plugin existed = pluginMapper.selectOneById(plugin.getId());
|
||||
if (existed == null) {
|
||||
throw new BusinessException("插件不存在");
|
||||
}
|
||||
if (PluginType.from(existed.getType()) != PluginType.WORKFLOW) {
|
||||
throw new BusinessException("暂不支持在现有 HTTP 插件与工作流插件之间切换类型");
|
||||
}
|
||||
|
||||
Workflow workflow = requirePublishedWorkflow(plugin.getWorkflowId(), "无权限绑定工作流");
|
||||
if (workflowPluginDependencyService.containsPluginReferenceTransitivelyInPublishedSnapshot(workflow.getId(), existed.getId())) {
|
||||
throw new BusinessException("目标工作流已通过子流程或插件链路引用当前插件,无法形成递归绑定");
|
||||
}
|
||||
normalizeWorkflowPlugin(plugin, null);
|
||||
int updated = pluginMapper.update(plugin);
|
||||
if (updated <= 0) {
|
||||
throw new BusinessException("更新失败");
|
||||
}
|
||||
syncSinglePlugin(existed.getId(), workflow.getId());
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void syncByWorkflowId(BigInteger workflowId) {
|
||||
if (workflowId == null) {
|
||||
return;
|
||||
}
|
||||
QueryWrapper wrapper = QueryWrapper.create()
|
||||
.eq(Plugin::getWorkflowId, workflowId)
|
||||
.eq(Plugin::getType, PluginType.WORKFLOW.getCode());
|
||||
List<Plugin> plugins = pluginMapper.selectListByQuery(wrapper);
|
||||
for (Plugin plugin : plugins) {
|
||||
syncSinglePlugin(plugin.getId(), workflowId);
|
||||
}
|
||||
}
|
||||
|
||||
private void syncSinglePlugin(BigInteger pluginId, BigInteger workflowId) {
|
||||
Plugin plugin = pluginMapper.selectOneById(pluginId);
|
||||
if (plugin == null) {
|
||||
return;
|
||||
}
|
||||
Workflow workflow = requirePublishedWorkflowForSync(workflowId);
|
||||
PluginItem pluginItem = getOrCreateSystemTool(pluginId);
|
||||
workflowPluginSnapshotResolver.syncPluginItemFromPublishedWorkflow(plugin, pluginItem, workflow.getId());
|
||||
if (pluginItem.getId() == null) {
|
||||
pluginItem.setCreated(new Date());
|
||||
if (pluginItemMapper.insert(pluginItem) <= 0) {
|
||||
throw new BusinessException("同步工作流插件工具失败");
|
||||
}
|
||||
} else if (pluginItemMapper.update(pluginItem) <= 0) {
|
||||
throw new BusinessException("同步工作流插件工具失败");
|
||||
}
|
||||
}
|
||||
|
||||
private PluginItem getOrCreateSystemTool(BigInteger pluginId) {
|
||||
QueryWrapper wrapper = QueryWrapper.create().eq(PluginItem::getPluginId, pluginId);
|
||||
List<PluginItem> pluginItems = pluginItemMapper.selectListByQuery(wrapper);
|
||||
if (pluginItems == null || pluginItems.isEmpty()) {
|
||||
PluginItem pluginItem = new PluginItem();
|
||||
pluginItem.setPluginId(pluginId);
|
||||
return pluginItem;
|
||||
}
|
||||
PluginItem pluginItem = pluginItems.get(0);
|
||||
if (pluginItems.size() > 1) {
|
||||
for (int i = 1; i < pluginItems.size(); i++) {
|
||||
pluginItemMapper.deleteById(pluginItems.get(i).getId());
|
||||
}
|
||||
}
|
||||
return pluginItem;
|
||||
}
|
||||
|
||||
private Workflow requirePublishedWorkflow(BigInteger workflowId, String denyMessage) {
|
||||
if (workflowId == null) {
|
||||
throw new BusinessException("请选择已发布工作流");
|
||||
}
|
||||
Workflow workflow = workflowService.getById(workflowId);
|
||||
if (workflow == null) {
|
||||
throw new BusinessException("工作流不存在");
|
||||
}
|
||||
resourceAccessService.assertAccess(CategoryResourceType.WORKFLOW, workflow, ResourceAction.USE, denyMessage);
|
||||
PublishStatus publishStatus = PublishStatus.from(workflow.getPublishStatus());
|
||||
if (publishStatus != PublishStatus.PUBLISHED) {
|
||||
throw new BusinessException("仅已发布工作流可被封装为插件");
|
||||
}
|
||||
if (workflow.getPublishedSnapshotJson() == null || workflow.getPublishedSnapshotJson().isEmpty()) {
|
||||
throw new BusinessException("目标工作流缺少已发布快照");
|
||||
}
|
||||
workflowPluginSnapshotResolver.assertSupportedForWorkflowPlugin(workflowService.toPublishedView(workflow));
|
||||
return workflow;
|
||||
}
|
||||
|
||||
private Workflow requirePublishedWorkflowForSync(BigInteger workflowId) {
|
||||
if (workflowId == null) {
|
||||
throw new BusinessException("目标工作流不存在");
|
||||
}
|
||||
Workflow workflow = workflowService.getById(workflowId);
|
||||
if (workflow == null) {
|
||||
throw new BusinessException("目标工作流不存在");
|
||||
}
|
||||
PublishStatus publishStatus = PublishStatus.from(workflow.getPublishStatus());
|
||||
if (publishStatus != PublishStatus.PUBLISHED) {
|
||||
throw new BusinessException("仅已发布工作流可同步到工作流插件");
|
||||
}
|
||||
if (workflow.getPublishedSnapshotJson() == null || workflow.getPublishedSnapshotJson().isEmpty()) {
|
||||
throw new BusinessException("目标工作流缺少已发布快照");
|
||||
}
|
||||
workflowPluginSnapshotResolver.assertSupportedForWorkflowPlugin(workflowService.toPublishedView(workflow));
|
||||
return workflow;
|
||||
}
|
||||
|
||||
private void normalizeWorkflowPlugin(Plugin plugin, LoginAccount loginAccount) {
|
||||
plugin.setType(PluginType.WORKFLOW.getCode());
|
||||
plugin.setBaseUrl(null);
|
||||
plugin.setAuthType(null);
|
||||
plugin.setPosition(null);
|
||||
plugin.setHeaders(null);
|
||||
plugin.setTokenKey(null);
|
||||
plugin.setTokenValue(null);
|
||||
if (plugin.getCreated() == null) {
|
||||
plugin.setCreated(new Date());
|
||||
}
|
||||
if (loginAccount != null) {
|
||||
plugin.setCreatedBy(loginAccount.getId().longValue());
|
||||
plugin.setDeptId(loginAccount.getDeptId() == null ? null : loginAccount.getDeptId().longValue());
|
||||
plugin.setTenantId(loginAccount.getTenantId() == null ? null : loginAccount.getTenantId().longValue());
|
||||
}
|
||||
if (!StringUtils.hasText(plugin.getAlias())) {
|
||||
plugin.setAlias(null);
|
||||
}
|
||||
}
|
||||
|
||||
private LoginAccount requireLogin() {
|
||||
LoginAccount loginAccount = SaTokenUtil.getLoginAccount();
|
||||
if (loginAccount == null || loginAccount.getId() == null) {
|
||||
throw new BusinessException("当前未登录");
|
||||
}
|
||||
return loginAccount;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package tech.easyflow.ai.plugin.workflow.dependency;
|
||||
|
||||
import tech.easyflow.ai.entity.Plugin;
|
||||
import tech.easyflow.ai.vo.OfflineImpactBindingVo;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 工作流插件依赖分析服务。
|
||||
*/
|
||||
public interface WorkflowPluginDependencyService {
|
||||
|
||||
/**
|
||||
* 查询被某个工作流引用的插件列表。
|
||||
*
|
||||
* @param workflowId 工作流 ID
|
||||
* @return 插件引用列表
|
||||
*/
|
||||
List<OfflineImpactBindingVo> listPluginsByWorkflowId(BigInteger workflowId);
|
||||
|
||||
/**
|
||||
* 解析工作流内容中通过插件间接引用到的工作流 ID。
|
||||
*
|
||||
* @param content 工作流内容
|
||||
* @return 引用到的工作流 ID 集合
|
||||
*/
|
||||
Set<String> extractWorkflowIdsFromPluginNodes(String content);
|
||||
|
||||
/**
|
||||
* 判断工作流内容是否引用了指定插件。
|
||||
*
|
||||
* @param content 工作流内容
|
||||
* @param pluginId 插件 ID
|
||||
* @return 是否引用
|
||||
*/
|
||||
boolean containsPluginReference(String content, BigInteger pluginId);
|
||||
|
||||
/**
|
||||
* 判断某个工作流是否经由子流程/工作流插件链路递归引用了指定插件。
|
||||
*
|
||||
* @param workflowId 工作流 ID
|
||||
* @param pluginId 插件 ID
|
||||
* @return 是否存在递归引用
|
||||
*/
|
||||
boolean containsPluginReferenceTransitively(BigInteger workflowId, BigInteger pluginId);
|
||||
|
||||
/**
|
||||
* 判断某个工作流的已发布快照是否经由子流程/工作流插件链路递归引用了指定插件。
|
||||
*
|
||||
* @param workflowId 工作流 ID
|
||||
* @param pluginId 插件 ID
|
||||
* @return 是否存在递归引用
|
||||
*/
|
||||
boolean containsPluginReferenceTransitivelyInPublishedSnapshot(BigInteger workflowId, BigInteger pluginId);
|
||||
|
||||
/**
|
||||
* 查询工作流插件。
|
||||
*
|
||||
* @param pluginId 插件 ID
|
||||
* @return 插件
|
||||
*/
|
||||
Plugin getWorkflowPlugin(BigInteger pluginId);
|
||||
|
||||
/**
|
||||
* 根据插件工具 ID 解析目标工作流 ID。
|
||||
*
|
||||
* @param pluginItemId 插件工具 ID
|
||||
* @return 工作流 ID
|
||||
*/
|
||||
String resolveWorkflowIdByPluginItemId(String pluginItemId);
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
package tech.easyflow.ai.plugin.workflow.dependency;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
import tech.easyflow.ai.entity.Plugin;
|
||||
import tech.easyflow.ai.entity.PluginItem;
|
||||
import tech.easyflow.ai.enums.PluginType;
|
||||
import tech.easyflow.ai.mapper.PluginItemMapper;
|
||||
import tech.easyflow.ai.mapper.PluginMapper;
|
||||
import tech.easyflow.ai.service.WorkflowService;
|
||||
import tech.easyflow.ai.vo.OfflineImpactBindingVo;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 工作流插件依赖分析实现。
|
||||
*/
|
||||
@Service
|
||||
public class WorkflowPluginDependencyServiceImpl implements WorkflowPluginDependencyService {
|
||||
|
||||
private static final String TYPE_PLUGIN = "plugin-node";
|
||||
private static final String TYPE_WORKFLOW = "workflow-node";
|
||||
|
||||
private final PluginMapper pluginMapper;
|
||||
private final PluginItemMapper pluginItemMapper;
|
||||
private final WorkflowService workflowService;
|
||||
|
||||
public WorkflowPluginDependencyServiceImpl(PluginMapper pluginMapper,
|
||||
PluginItemMapper pluginItemMapper,
|
||||
WorkflowService workflowService) {
|
||||
this.pluginMapper = pluginMapper;
|
||||
this.pluginItemMapper = pluginItemMapper;
|
||||
this.workflowService = workflowService;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public List<OfflineImpactBindingVo> listPluginsByWorkflowId(BigInteger workflowId) {
|
||||
if (workflowId == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
QueryWrapper wrapper = QueryWrapper.create().eq(Plugin::getWorkflowId, workflowId);
|
||||
List<Plugin> plugins = pluginMapper.selectListByQuery(wrapper);
|
||||
if (plugins == null || plugins.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<OfflineImpactBindingVo> result = new ArrayList<>(plugins.size());
|
||||
for (Plugin plugin : plugins) {
|
||||
if (!PluginType.isWorkflow(plugin.getType())) {
|
||||
continue;
|
||||
}
|
||||
OfflineImpactBindingVo vo = new OfflineImpactBindingVo();
|
||||
vo.setId(plugin.getId());
|
||||
vo.setTitle(plugin.getName());
|
||||
result.add(vo);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Set<String> extractWorkflowIdsFromPluginNodes(String content) {
|
||||
if (!StringUtils.hasText(content)) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
Set<String> workflowIds = new LinkedHashSet<>();
|
||||
try {
|
||||
Object parsed = JSON.parse(content);
|
||||
if (!(parsed instanceof JSONObject root)) {
|
||||
return workflowIds;
|
||||
}
|
||||
JSONArray nodes = root.getJSONArray("nodes");
|
||||
if (nodes == null) {
|
||||
return workflowIds;
|
||||
}
|
||||
for (int i = 0; i < nodes.size(); i++) {
|
||||
JSONObject node = nodes.getJSONObject(i);
|
||||
if (node == null || !TYPE_PLUGIN.equals(node.getString("type"))) {
|
||||
continue;
|
||||
}
|
||||
JSONObject data = node.getJSONObject("data");
|
||||
String pluginItemId = data == null ? null : data.getString("pluginId");
|
||||
if (!StringUtils.hasText(pluginItemId)) {
|
||||
continue;
|
||||
}
|
||||
Plugin plugin = getPluginByPluginItemId(pluginItemId);
|
||||
if (plugin == null || !PluginType.isWorkflow(plugin.getType()) || plugin.getWorkflowId() == null) {
|
||||
continue;
|
||||
}
|
||||
workflowIds.add(String.valueOf(plugin.getWorkflowId()));
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// ignore
|
||||
}
|
||||
return workflowIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public boolean containsPluginReference(String content, BigInteger pluginId) {
|
||||
if (!StringUtils.hasText(content) || pluginId == null) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
Object parsed = JSON.parse(content);
|
||||
if (!(parsed instanceof JSONObject root)) {
|
||||
return false;
|
||||
}
|
||||
JSONArray nodes = root.getJSONArray("nodes");
|
||||
if (nodes == null) {
|
||||
return false;
|
||||
}
|
||||
String expected = String.valueOf(pluginId);
|
||||
for (int i = 0; i < nodes.size(); i++) {
|
||||
JSONObject node = nodes.getJSONObject(i);
|
||||
if (node == null || !TYPE_PLUGIN.equals(node.getString("type"))) {
|
||||
continue;
|
||||
}
|
||||
JSONObject data = node.getJSONObject("data");
|
||||
String pluginItemId = data == null ? null : data.getString("pluginId");
|
||||
Plugin plugin = getPluginByPluginItemId(pluginItemId);
|
||||
if (plugin != null && expected.equals(String.valueOf(plugin.getId()))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// ignore
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public boolean containsPluginReferenceTransitively(BigInteger workflowId, BigInteger pluginId) {
|
||||
if (workflowId == null || pluginId == null) {
|
||||
return false;
|
||||
}
|
||||
return containsPluginReferenceTransitively(
|
||||
String.valueOf(workflowId),
|
||||
pluginId,
|
||||
false,
|
||||
new LinkedHashSet<>(),
|
||||
new HashMap<>()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public boolean containsPluginReferenceTransitivelyInPublishedSnapshot(BigInteger workflowId, BigInteger pluginId) {
|
||||
if (workflowId == null || pluginId == null) {
|
||||
return false;
|
||||
}
|
||||
return containsPluginReferenceTransitively(
|
||||
String.valueOf(workflowId),
|
||||
pluginId,
|
||||
true,
|
||||
new LinkedHashSet<>(),
|
||||
new HashMap<>()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Plugin getWorkflowPlugin(BigInteger pluginId) {
|
||||
if (pluginId == null) {
|
||||
return null;
|
||||
}
|
||||
Plugin plugin = pluginMapper.selectOneById(pluginId);
|
||||
if (plugin == null || !PluginType.isWorkflow(plugin.getType())) {
|
||||
return null;
|
||||
}
|
||||
return plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public String resolveWorkflowIdByPluginItemId(String pluginItemId) {
|
||||
Plugin plugin = getPluginByPluginItemId(pluginItemId);
|
||||
if (plugin == null || !PluginType.isWorkflow(plugin.getType()) || plugin.getWorkflowId() == null) {
|
||||
return null;
|
||||
}
|
||||
return String.valueOf(plugin.getWorkflowId());
|
||||
}
|
||||
|
||||
private Plugin getPluginByPluginItemId(String pluginItemId) {
|
||||
PluginItem pluginItem = pluginItemMapper.selectOneById(pluginItemId);
|
||||
if (pluginItem == null || pluginItem.getPluginId() == null) {
|
||||
return null;
|
||||
}
|
||||
return pluginMapper.selectOneById(pluginItem.getPluginId());
|
||||
}
|
||||
|
||||
private boolean containsPluginReferenceTransitively(String workflowId,
|
||||
BigInteger pluginId,
|
||||
boolean publishedOnly,
|
||||
Set<String> visitingWorkflowIds,
|
||||
Map<String, Boolean> cache) {
|
||||
if (!StringUtils.hasText(workflowId)) {
|
||||
return false;
|
||||
}
|
||||
Boolean cached = cache.get(workflowId);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
if (!visitingWorkflowIds.add(workflowId)) {
|
||||
return false;
|
||||
}
|
||||
boolean result = false;
|
||||
try {
|
||||
String workflowContent = resolveWorkflowContent(workflowId, publishedOnly);
|
||||
if (!StringUtils.hasText(workflowContent)) {
|
||||
cache.put(workflowId, false);
|
||||
return false;
|
||||
}
|
||||
result = containsPluginReferenceTransitivelyInContent(
|
||||
workflowContent,
|
||||
pluginId,
|
||||
publishedOnly,
|
||||
visitingWorkflowIds,
|
||||
cache
|
||||
);
|
||||
cache.put(workflowId, result);
|
||||
return result;
|
||||
} finally {
|
||||
visitingWorkflowIds.remove(workflowId);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean containsPluginReferenceTransitivelyInContent(String content,
|
||||
BigInteger pluginId,
|
||||
boolean publishedOnly,
|
||||
Set<String> visitingWorkflowIds,
|
||||
Map<String, Boolean> cache) {
|
||||
if (!StringUtils.hasText(content) || pluginId == null) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
Object parsed = JSON.parse(content);
|
||||
if (!(parsed instanceof JSONObject root)) {
|
||||
return false;
|
||||
}
|
||||
JSONArray nodes = root.getJSONArray("nodes");
|
||||
if (nodes == null || nodes.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
String expectedPluginId = String.valueOf(pluginId);
|
||||
for (int i = 0; i < nodes.size(); i++) {
|
||||
JSONObject node = nodes.getJSONObject(i);
|
||||
if (node == null) {
|
||||
continue;
|
||||
}
|
||||
String nodeType = node.getString("type");
|
||||
JSONObject data = node.getJSONObject("data");
|
||||
if (TYPE_PLUGIN.equals(nodeType)) {
|
||||
String pluginItemId = data == null ? null : data.getString("pluginId");
|
||||
Plugin plugin = getPluginByPluginItemId(pluginItemId);
|
||||
if (plugin == null) {
|
||||
continue;
|
||||
}
|
||||
if (expectedPluginId.equals(String.valueOf(plugin.getId()))) {
|
||||
return true;
|
||||
}
|
||||
if (PluginType.isWorkflow(plugin.getType())
|
||||
&& plugin.getWorkflowId() != null
|
||||
&& containsPluginReferenceTransitively(
|
||||
String.valueOf(plugin.getWorkflowId()),
|
||||
pluginId,
|
||||
publishedOnly,
|
||||
visitingWorkflowIds,
|
||||
cache
|
||||
)) {
|
||||
return true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (TYPE_WORKFLOW.equals(nodeType)) {
|
||||
String refWorkflowId = data == null ? null : data.getString("workflowId");
|
||||
if (containsPluginReferenceTransitively(
|
||||
refWorkflowId,
|
||||
pluginId,
|
||||
publishedOnly,
|
||||
visitingWorkflowIds,
|
||||
cache
|
||||
)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// ignore
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private String resolveWorkflowContent(String workflowId, boolean publishedOnly) {
|
||||
tech.easyflow.ai.entity.Workflow workflow = publishedOnly
|
||||
? getPublishedWorkflow(workflowId)
|
||||
: workflowService.getById(workflowId);
|
||||
return workflow == null ? null : workflow.getContent();
|
||||
}
|
||||
|
||||
private tech.easyflow.ai.entity.Workflow getPublishedWorkflow(String workflowId) {
|
||||
try {
|
||||
return workflowService.getPublishedById(new BigInteger(workflowId));
|
||||
} catch (Exception ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
package tech.easyflow.ai.plugin.workflow.snapshot;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.easyagents.flow.core.chain.ChainDefinition;
|
||||
import com.easyagents.flow.core.chain.Node;
|
||||
import com.easyagents.flow.core.node.EndNode;
|
||||
import org.springframework.stereotype.Service;
|
||||
import tech.easyflow.ai.easyagentsflow.support.PublishedWorkflowDefinitionIds;
|
||||
import tech.easyflow.ai.easyagentsflow.service.WorkflowDatacenterContentService;
|
||||
import tech.easyflow.ai.easyagents.tool.WorkflowTool;
|
||||
import tech.easyflow.ai.entity.Plugin;
|
||||
import tech.easyflow.ai.entity.PluginItem;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
import tech.easyflow.ai.service.WorkflowService;
|
||||
import com.easyagents.flow.core.parser.ChainParser;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
|
||||
import java.util.List;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
/**
|
||||
* 工作流插件快照解析服务。
|
||||
*/
|
||||
@Service
|
||||
public class WorkflowPluginSnapshotResolver {
|
||||
|
||||
private final WorkflowService workflowService;
|
||||
private final ChainParser chainParser;
|
||||
private final WorkflowDatacenterContentService workflowDatacenterContentService;
|
||||
|
||||
public WorkflowPluginSnapshotResolver(WorkflowService workflowService,
|
||||
ChainParser chainParser,
|
||||
WorkflowDatacenterContentService workflowDatacenterContentService) {
|
||||
this.workflowService = workflowService;
|
||||
this.chainParser = chainParser;
|
||||
this.workflowDatacenterContentService = workflowDatacenterContentService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用已发布视图刷新工作流插件工具定义。
|
||||
*
|
||||
* @param plugin 插件
|
||||
* @param pluginItem 插件工具
|
||||
* @param workflowId 工作流 ID
|
||||
*/
|
||||
public void syncPluginItemFromPublishedWorkflow(Plugin plugin, PluginItem pluginItem, java.math.BigInteger workflowId) {
|
||||
Workflow workflow = workflowService.getPublishedById(workflowId);
|
||||
if (workflow == null) {
|
||||
throw new BusinessException("工作流不存在");
|
||||
}
|
||||
assertSupportedForWorkflowPlugin(workflow);
|
||||
ChainDefinition definition = parseDefinition(workflow);
|
||||
pluginItem.setPluginId(plugin.getId());
|
||||
pluginItem.setName(plugin.getName());
|
||||
pluginItem.setEnglishName(workflow.getEnglishName());
|
||||
pluginItem.setDescription(resolveDescription(plugin, workflow));
|
||||
pluginItem.setBasePath(null);
|
||||
pluginItem.setRequestMethod("WORKFLOW");
|
||||
JSONArray inputDefinitions = resolveInputDefinitions(definition);
|
||||
JSONArray outputDefinitions = resolveOutputDefinitions(definition);
|
||||
pluginItem.setInputData(JSON.toJSONString(inputDefinitions));
|
||||
pluginItem.setOutputData(JSON.toJSONString(outputDefinitions));
|
||||
pluginItem.setSchemaHash(resolveSchemaHash(inputDefinitions, outputDefinitions));
|
||||
pluginItem.setServiceStatus(1);
|
||||
pluginItem.setStatus(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建工作流插件运行工具。
|
||||
*
|
||||
* @param workflow 工作流已发布视图
|
||||
* @return 工作流工具
|
||||
*/
|
||||
public WorkflowTool buildWorkflowTool(Workflow workflow) {
|
||||
return new WorkflowTool(
|
||||
workflow,
|
||||
false,
|
||||
PublishedWorkflowDefinitionIds.published(String.valueOf(workflow.getId()))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析输入参数定义。
|
||||
*
|
||||
* @param definition 流程定义
|
||||
* @return 输入参数数组
|
||||
*/
|
||||
public JSONArray resolveInputDefinitions(ChainDefinition definition) {
|
||||
JSONArray inputs = JSON.parseArray(JSON.toJSONString(definition.getStartParameters()));
|
||||
markWorkflowPluginInput(inputs);
|
||||
return inputs == null ? new JSONArray() : inputs;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析输出参数定义。
|
||||
*
|
||||
* @param definition 流程定义
|
||||
* @return 输出参数数组
|
||||
*/
|
||||
public JSONArray resolveOutputDefinitions(ChainDefinition definition) {
|
||||
JSONArray outputs = new JSONArray();
|
||||
List<Node> nodes = definition.getNodes();
|
||||
if (nodes == null) {
|
||||
return outputs;
|
||||
}
|
||||
for (Node node : nodes) {
|
||||
if (node instanceof EndNode endNode) {
|
||||
outputs = JSON.parseArray(JSON.toJSONString(endNode.getOutputDefs()));
|
||||
}
|
||||
}
|
||||
return outputs == null ? new JSONArray() : outputs;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析工作流插件契约哈希。
|
||||
*
|
||||
* @param workflow 工作流
|
||||
* @return 哈希值
|
||||
*/
|
||||
public String resolveSchemaHash(Workflow workflow) {
|
||||
ChainDefinition definition = parseDefinition(workflow);
|
||||
return resolveSchemaHash(resolveInputDefinitions(definition), resolveOutputDefinitions(definition));
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析工作流插件契约哈希。
|
||||
*
|
||||
* @param inputDefinitions 输入定义
|
||||
* @param outputDefinitions 输出定义
|
||||
* @return 哈希值
|
||||
*/
|
||||
public String resolveSchemaHash(JSONArray inputDefinitions, JSONArray outputDefinitions) {
|
||||
JSONObject payload = new JSONObject();
|
||||
payload.put("inputs", inputDefinitions == null ? new JSONArray() : inputDefinitions);
|
||||
payload.put("outputs", outputDefinitions == null ? new JSONArray() : outputDefinitions);
|
||||
return sha256Hex(JSON.toJSONString(payload));
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验工作流是否支持被封装为工作流插件。
|
||||
*
|
||||
* @param workflow 工作流
|
||||
*/
|
||||
public void assertSupportedForWorkflowPlugin(Workflow workflow) {
|
||||
ChainDefinition definition = parseDefinition(workflow);
|
||||
int endNodeCount = countEndNodes(definition);
|
||||
// 一期先收敛为“单结束节点才能封装为插件”,后续若要支持多结束节点,
|
||||
// 需要先补齐统一输出契约、父流程节点 schema 同步和结果展示策略。
|
||||
if (endNodeCount != 1) {
|
||||
throw new BusinessException("工作流插件仅支持单一结束节点,当前工作流不可封装为插件");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断工作流是否为单结束节点结构。
|
||||
*
|
||||
* @param workflow 工作流
|
||||
* @return 单结束节点返回 true
|
||||
*/
|
||||
public boolean isSupportedForWorkflowPlugin(Workflow workflow) {
|
||||
ChainDefinition definition = parseDefinition(workflow);
|
||||
return countEndNodes(definition) == 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析已发布工作流定义。
|
||||
*
|
||||
* @param workflow 工作流已发布视图
|
||||
* @return 流程定义
|
||||
*/
|
||||
public ChainDefinition parseDefinition(Workflow workflow) {
|
||||
String preparedContent = workflowDatacenterContentService.prepareContent(workflow.getContent());
|
||||
return chainParser.parse(preparedContent);
|
||||
}
|
||||
|
||||
private String resolveDescription(Plugin plugin, Workflow workflow) {
|
||||
if (plugin.getDescription() != null && !plugin.getDescription().isBlank()) {
|
||||
return plugin.getDescription();
|
||||
}
|
||||
return workflow.getDescription();
|
||||
}
|
||||
|
||||
private void markWorkflowPluginInput(JSONArray parameters) {
|
||||
if (parameters == null) {
|
||||
return;
|
||||
}
|
||||
for (Object parameter : parameters) {
|
||||
if (!(parameter instanceof com.alibaba.fastjson2.JSONObject obj)) {
|
||||
continue;
|
||||
}
|
||||
obj.put("refType", "ref");
|
||||
JSONArray children = obj.getJSONArray("children");
|
||||
if (children != null) {
|
||||
markWorkflowPluginInput(children);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int countEndNodes(ChainDefinition definition) {
|
||||
List<Node> nodes = definition == null ? null : definition.getNodes();
|
||||
if (nodes == null || nodes.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
int count = 0;
|
||||
for (Node node : nodes) {
|
||||
if (node instanceof EndNode) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private String sha256Hex(String source) {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
byte[] bytes = digest.digest(source.getBytes(StandardCharsets.UTF_8));
|
||||
StringBuilder builder = new StringBuilder(bytes.length * 2);
|
||||
for (byte current : bytes) {
|
||||
builder.append(Character.forDigit((current >> 4) & 0xF, 16));
|
||||
builder.append(Character.forDigit(current & 0xF, 16));
|
||||
}
|
||||
return builder.toString();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IllegalStateException("SHA-256 algorithm unavailable", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,8 @@ import org.springframework.stereotype.Component;
|
||||
import tech.easyflow.ai.entity.BotWorkflow;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
import tech.easyflow.ai.enums.PublishStatus;
|
||||
import tech.easyflow.ai.plugin.workflow.binding.WorkflowPluginBindingService;
|
||||
import tech.easyflow.ai.plugin.workflow.snapshot.WorkflowPluginSnapshotResolver;
|
||||
import tech.easyflow.ai.service.BotWorkflowService;
|
||||
import tech.easyflow.ai.service.ResourceOfflineImpactService;
|
||||
import tech.easyflow.ai.service.WorkflowService;
|
||||
@@ -31,18 +33,24 @@ public class WorkflowApprovalSubjectHandler extends AbstractAiResourceLifecycleH
|
||||
private final ResourceAccessService resourceAccessService;
|
||||
private final BotWorkflowService botWorkflowService;
|
||||
private final ResourceOfflineImpactService resourceOfflineImpactService;
|
||||
private final WorkflowPluginBindingService workflowPluginBindingService;
|
||||
private final WorkflowPluginSnapshotResolver workflowPluginSnapshotResolver;
|
||||
|
||||
public WorkflowApprovalSubjectHandler(WorkflowService workflowService,
|
||||
ResourceAccessService resourceAccessService,
|
||||
ApprovalInstanceService approvalInstanceService,
|
||||
BotWorkflowService botWorkflowService,
|
||||
ResourceOfflineImpactService resourceOfflineImpactService,
|
||||
WorkflowPluginBindingService workflowPluginBindingService,
|
||||
WorkflowPluginSnapshotResolver workflowPluginSnapshotResolver,
|
||||
ObjectMapper objectMapper) {
|
||||
super(approvalInstanceService, objectMapper);
|
||||
this.workflowService = workflowService;
|
||||
this.resourceAccessService = resourceAccessService;
|
||||
this.botWorkflowService = botWorkflowService;
|
||||
this.resourceOfflineImpactService = resourceOfflineImpactService;
|
||||
this.workflowPluginBindingService = workflowPluginBindingService;
|
||||
this.workflowPluginSnapshotResolver = workflowPluginSnapshotResolver;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -116,6 +124,16 @@ public class WorkflowApprovalSubjectHandler extends AbstractAiResourceLifecycleH
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<String, Object> buildPublishSnapshot(Workflow resource, PublishStatus currentStatus) {
|
||||
Map<String, Object> snapshot = super.buildPublishSnapshot(resource, currentStatus);
|
||||
OfflineImpactCheckVo impact = resourceOfflineImpactService.checkWorkflowImpact(resource.getId());
|
||||
if (impact.isHasPluginBindings()) {
|
||||
workflowPluginSnapshotResolver.assertSupportedForWorkflowPlugin(resource);
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void persistResourceState(BigInteger resourceId, PublishStatus publishStatus, BigInteger currentApprovalInstanceId) {
|
||||
Workflow update = new Workflow();
|
||||
@@ -135,6 +153,7 @@ public class WorkflowApprovalSubjectHandler extends AbstractAiResourceLifecycleH
|
||||
update.setPublishedAt(new java.util.Date());
|
||||
update.setPublishedBy(operatorId);
|
||||
workflowService.updateById(update);
|
||||
workflowPluginBindingService.syncByWorkflowId(resourceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -162,6 +181,9 @@ public class WorkflowApprovalSubjectHandler extends AbstractAiResourceLifecycleH
|
||||
if (impact.isHasBotBindings()) {
|
||||
snapshot.put("botBindings", impact.getBotBindings());
|
||||
}
|
||||
if (impact.isHasPluginBindings()) {
|
||||
snapshot.put("pluginBindings", impact.getPluginBindings());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -9,6 +9,7 @@ public class KnowledgeRetrievalRequest {
|
||||
private BigInteger knowledgeId;
|
||||
private String query;
|
||||
private Integer limit;
|
||||
private Double minSimilarity;
|
||||
private RetrievalMode retrievalMode = RetrievalMode.HYBRID;
|
||||
private String callerType;
|
||||
private String callerId;
|
||||
@@ -37,6 +38,24 @@ public class KnowledgeRetrievalRequest {
|
||||
this.limit = limit;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回检索时使用的最小相似度阈值。
|
||||
*
|
||||
* @return 最小相似度阈值
|
||||
*/
|
||||
public Double getMinSimilarity() {
|
||||
return minSimilarity;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置检索时使用的最小相似度阈值。
|
||||
*
|
||||
* @param minSimilarity 最小相似度阈值
|
||||
*/
|
||||
public void setMinSimilarity(Double minSimilarity) {
|
||||
this.minSimilarity = minSimilarity;
|
||||
}
|
||||
|
||||
public RetrievalMode getRetrievalMode() {
|
||||
return retrievalMode;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package tech.easyflow.ai.service;
|
||||
|
||||
import com.mybatisflex.core.service.IService;
|
||||
import tech.easyflow.ai.entity.DocumentImportTask;
|
||||
|
||||
/**
|
||||
* 文档导入任务服务。
|
||||
*
|
||||
* @author Codex
|
||||
* @since 2026-04-14
|
||||
*/
|
||||
public interface DocumentImportTaskService extends IService<DocumentImportTask> {
|
||||
}
|
||||
@@ -32,4 +32,16 @@ public interface DocumentService extends IService<Document> {
|
||||
Result<DocumentImportDtos.PreviewResponse> previewImport(DocumentImportDtos.PreviewRequest request);
|
||||
|
||||
Result<DocumentImportDtos.CommitResponse> commitImport(DocumentImportDtos.CommitRequest request);
|
||||
|
||||
Result<DocumentImportDtos.TaskCreateResponse> createImportTask(DocumentImportDtos.TaskCreateRequest request);
|
||||
|
||||
Result<DocumentImportDtos.TaskDetailResponse> getImportTaskDetail(BigInteger taskId);
|
||||
|
||||
Result<DocumentImportDtos.PreviewResponse> previewImportTask(DocumentImportDtos.PreviewRequest request);
|
||||
|
||||
Result<DocumentImportDtos.TaskStartIndexResponse> startIndexTask(DocumentImportDtos.TaskStartIndexRequest request);
|
||||
|
||||
Result<DocumentImportDtos.TaskStartIndexResponse> retryParseTask(DocumentImportDtos.TaskRetryRequest request);
|
||||
|
||||
Result<DocumentImportDtos.TaskStartIndexResponse> retryIndexTask(DocumentImportDtos.TaskRetryRequest request);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package tech.easyflow.ai.service;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
/**
|
||||
* 知识库向量重建服务。
|
||||
*/
|
||||
public interface KnowledgeEmbeddingService {
|
||||
|
||||
/**
|
||||
* 按知识库重建向量数据。
|
||||
*
|
||||
* @param knowledgeId 知识库 ID
|
||||
*/
|
||||
void rebuildKnowledgeVectors(BigInteger knowledgeId);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package tech.easyflow.ai.service;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 知识库分享审计服务。
|
||||
*/
|
||||
public interface KnowledgeShareAuditService {
|
||||
|
||||
/**
|
||||
* 记录审计日志。
|
||||
*
|
||||
* @param accountId 操作人
|
||||
* @param actionName 操作名称
|
||||
* @param actionType 操作类型
|
||||
* @param actionUrl 操作地址
|
||||
* @param detail 扩展上下文
|
||||
*/
|
||||
void log(BigInteger accountId, String actionName, String actionType, String actionUrl, Map<String, Object> detail);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package tech.easyflow.ai.service;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 知识库分享权限服务。
|
||||
*/
|
||||
public interface KnowledgeSharePermissionService {
|
||||
|
||||
/**
|
||||
* 为系统访问令牌授予知识库 API 分享权限。
|
||||
*
|
||||
* @param apiKeyId 系统访问令牌 ID
|
||||
* @param knowledgeId 知识库 ID
|
||||
* @param actionScopes 动作范围
|
||||
*/
|
||||
void grantApiShare(BigInteger apiKeyId, BigInteger knowledgeId, Set<String> actionScopes);
|
||||
|
||||
/**
|
||||
* 按访问令牌维度开启或关闭知识库 API 分享授权。
|
||||
*
|
||||
* <p>产品固定为全量知识库的非删除范围,不向前端暴露动作粒度。</p>
|
||||
*
|
||||
* @param apiKeyId 系统访问令牌 ID
|
||||
* @param enabled 是否启用知识库分享授权
|
||||
*/
|
||||
void replaceApiShareEnabled(BigInteger apiKeyId, boolean enabled);
|
||||
|
||||
/**
|
||||
* 断言当前令牌具备知识库分享权限。
|
||||
*
|
||||
* @param apiKeyId 系统访问令牌 ID
|
||||
* @param requestUri 请求地址
|
||||
* @param knowledgeId 知识库 ID
|
||||
* @param actionScope 动作范围
|
||||
*/
|
||||
void assertApiShare(BigInteger apiKeyId, String requestUri, BigInteger knowledgeId, String actionScope);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package tech.easyflow.ai.service;
|
||||
|
||||
import com.mybatisflex.core.service.IService;
|
||||
import tech.easyflow.ai.entity.KnowledgeShare;
|
||||
import tech.easyflow.ai.vo.KnowledgeShareAuthContext;
|
||||
import tech.easyflow.ai.vo.KnowledgeShareUrlCreateResult;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 知识库分享服务。
|
||||
*/
|
||||
public interface KnowledgeShareService extends IService<KnowledgeShare> {
|
||||
|
||||
/**
|
||||
* 创建 URL 分享。
|
||||
*
|
||||
* @param knowledgeId 目标知识库
|
||||
* @param tenantId 租户 ID
|
||||
* @param deptId 部门 ID
|
||||
* @param operatorId 操作人
|
||||
* @param baseUrl 分享页基础 URL
|
||||
* @param permissionScopes 授权范围
|
||||
* @return 创建结果
|
||||
*/
|
||||
KnowledgeShareUrlCreateResult createUrlShare(
|
||||
BigInteger knowledgeId,
|
||||
BigInteger tenantId,
|
||||
BigInteger deptId,
|
||||
BigInteger operatorId,
|
||||
String baseUrl,
|
||||
Set<String> permissionScopes
|
||||
);
|
||||
|
||||
/**
|
||||
* 校验 URL 分享并返回上下文。
|
||||
*
|
||||
* @param shareKey 分享访问密钥
|
||||
* @return 鉴权上下文
|
||||
*/
|
||||
KnowledgeShareAuthContext validateUrlShare(String shareKey);
|
||||
|
||||
/**
|
||||
* 断言 URL 分享允许当前动作。
|
||||
*
|
||||
* @param shareKey 分享访问密钥
|
||||
* @param knowledgeId 知识库 ID
|
||||
* @param actionScope 动作范围
|
||||
* @return 鉴权上下文
|
||||
*/
|
||||
KnowledgeShareAuthContext assertUrlShareAccess(
|
||||
String shareKey,
|
||||
BigInteger knowledgeId,
|
||||
String actionScope
|
||||
);
|
||||
}
|
||||
@@ -28,4 +28,12 @@ public interface PluginItemService extends IService<PluginItem> {
|
||||
Result pluginToolTest(String inputData, BigInteger pluginToolId);
|
||||
|
||||
List<PluginItem> getByPluginId(String id);
|
||||
|
||||
/**
|
||||
* 获取某个插件的系统维护工具。
|
||||
*
|
||||
* @param pluginId 插件 ID
|
||||
* @return 工具
|
||||
*/
|
||||
PluginItem getSingleByPluginId(BigInteger pluginId);
|
||||
}
|
||||
|
||||
@@ -23,4 +23,22 @@ public interface PluginService extends IService<Plugin> {
|
||||
Result pageByCategory(Long pageNumber, Long pageSize, int category);
|
||||
|
||||
boolean updatePlugin(Plugin plugin);
|
||||
|
||||
/**
|
||||
* 按当前用户视角过滤并补充工作流插件可用性信息。
|
||||
*
|
||||
* @param plugins 插件列表
|
||||
* @param managementView 是否为管理视角
|
||||
* @param availableOnly 是否仅保留当前可用插件
|
||||
* @return 过滤后的插件列表
|
||||
*/
|
||||
List<Plugin> preparePluginsForCurrentUser(List<Plugin> plugins, boolean managementView, boolean availableOnly);
|
||||
|
||||
/**
|
||||
* 补充单个插件的工作流可用性信息。
|
||||
*
|
||||
* @param plugin 插件
|
||||
* @return 原插件
|
||||
*/
|
||||
Plugin preparePluginForCurrentUser(Plugin plugin);
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import tech.easyflow.ai.easyagents.memory.RuntimeChatMemory;
|
||||
import tech.easyflow.ai.easyagents.tool.WorkflowTool;
|
||||
import tech.easyflow.ai.easyagentsflow.support.PublishedWorkflowDefinitionIds;
|
||||
import tech.easyflow.ai.entity.*;
|
||||
import tech.easyflow.ai.enums.PluginType;
|
||||
import tech.easyflow.ai.enums.PublishStatus;
|
||||
import tech.easyflow.ai.mapper.BotMapper;
|
||||
import tech.easyflow.ai.service.*;
|
||||
@@ -117,6 +118,8 @@ public class BotServiceImpl extends ServiceImpl<BotMapper, Bot> implements BotSe
|
||||
@Resource
|
||||
private BotPluginService botPluginService;
|
||||
@Resource
|
||||
private PluginService pluginService;
|
||||
@Resource
|
||||
private PluginItemService pluginItemService;
|
||||
@Resource
|
||||
private BotMcpService botMcpService;
|
||||
@@ -508,6 +511,12 @@ public class BotServiceImpl extends ServiceImpl<BotMapper, Bot> implements BotSe
|
||||
List<PluginItem> pluginItems = pluginItemService.getMapper().selectListWithRelationsByQuery(queryTool);
|
||||
if (pluginItems != null && !pluginItems.isEmpty()) {
|
||||
for (PluginItem pluginItem : pluginItems) {
|
||||
if (pluginItem.getPluginId() != null) {
|
||||
Plugin plugin = pluginService.getById(pluginItem.getPluginId());
|
||||
if (plugin != null && PluginType.isWorkflow(plugin.getType())) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
functionList.add(pluginItem.toFunction());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,9 +30,11 @@ import tech.easyflow.ai.entity.DocumentChunk;
|
||||
import tech.easyflow.ai.entity.DocumentCollection;
|
||||
import tech.easyflow.ai.entity.FaqItem;
|
||||
import tech.easyflow.ai.entity.Model;
|
||||
import tech.easyflow.ai.enums.DocumentProcessStatus;
|
||||
import tech.easyflow.ai.enums.PublishStatus;
|
||||
import tech.easyflow.ai.mapper.DocumentChunkMapper;
|
||||
import tech.easyflow.ai.mapper.DocumentCollectionMapper;
|
||||
import tech.easyflow.ai.mapper.DocumentMapper;
|
||||
import tech.easyflow.ai.mapper.FaqItemMapper;
|
||||
import tech.easyflow.ai.rag.KnowledgeRetrievalRequest;
|
||||
import tech.easyflow.ai.service.DocumentCollectionService;
|
||||
@@ -71,6 +73,8 @@ public class DocumentCollectionServiceImpl extends ServiceImpl<DocumentCollectio
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DocumentCollectionServiceImpl.class);
|
||||
private static final int MAX_FAQ_IMAGES_IN_PROMPT = 3;
|
||||
private static final int INTERNAL_RECALL_MULTIPLIER = 5;
|
||||
private static final int MAX_INTERNAL_RECALL_LIMIT = 100;
|
||||
|
||||
@Resource
|
||||
private ModelService llmService;
|
||||
@@ -81,6 +85,9 @@ public class DocumentCollectionServiceImpl extends ServiceImpl<DocumentCollectio
|
||||
@Autowired
|
||||
private DocumentChunkMapper documentChunkMapper;
|
||||
|
||||
@Autowired
|
||||
private DocumentMapper documentMapper;
|
||||
|
||||
@Autowired
|
||||
private FaqItemMapper faqItemMapper;
|
||||
|
||||
@@ -111,24 +118,27 @@ public class DocumentCollectionServiceImpl extends ServiceImpl<DocumentCollectio
|
||||
throw new BusinessException("知识库不存在");
|
||||
}
|
||||
|
||||
int docRecallMaxNum = readIntegerOption(documentCollection, KEY_DOC_RECALL_MAX_NUM, 5);
|
||||
float minSimilarity = readFloatOption(documentCollection, KEY_SIMILARITY_THRESHOLD, 0.6F);
|
||||
int docRecallMaxNum = resolveDocRecallMaxNum(request, documentCollection);
|
||||
int internalRecallLimit = resolveInternalRecallLimit(docRecallMaxNum);
|
||||
float minSimilarity = resolveMinSimilarity(request, documentCollection);
|
||||
|
||||
RagQuery ragQuery = new RagQuery();
|
||||
ragQuery.setQuery(keyword);
|
||||
ragQuery.setRetrievalMode(retrievalMode);
|
||||
ragQuery.setTopK(docRecallMaxNum);
|
||||
ragQuery.setTopK(internalRecallLimit);
|
||||
ragQuery.setMinScore((double) minSimilarity);
|
||||
|
||||
RagRetrievalExecutor retrievalExecutor = new RagRetrievalExecutor(
|
||||
buildVectorRetriever(documentCollection, docRecallMaxNum, retrievalMode == RetrievalMode.VECTOR ? minSimilarity : null),
|
||||
buildKeywordRetriever(documentCollection, docRecallMaxNum),
|
||||
buildVectorRetriever(documentCollection, internalRecallLimit, retrievalMode == RetrievalMode.VECTOR ? minSimilarity : null),
|
||||
buildKeywordRetriever(documentCollection, internalRecallLimit),
|
||||
new RrfFusionStrategy()
|
||||
);
|
||||
|
||||
RagRetrievalResult retrievalResult = retrievalExecutor.retrieve(ragQuery);
|
||||
List<Document> searchDocuments = toDocuments(retrievalResult.getHits());
|
||||
fillSearchContent(documentCollection, searchDocuments);
|
||||
List<Document> searchDocuments = prepareSearchDocuments(
|
||||
documentCollection,
|
||||
toDocuments(retrievalResult.getHits())
|
||||
);
|
||||
if (searchDocuments.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
@@ -138,7 +148,10 @@ public class DocumentCollectionServiceImpl extends ServiceImpl<DocumentCollectio
|
||||
if (rerankModel != null) {
|
||||
try {
|
||||
RagRetrievalResult rerankResult = retrievalExecutor.rerank(keyword, toRagHits(searchDocuments), rerankModel, docRecallMaxNum);
|
||||
searchDocuments = toDocuments(rerankResult.getHits());
|
||||
searchDocuments = prepareSearchDocuments(
|
||||
documentCollection,
|
||||
toDocuments(rerankResult.getHits())
|
||||
);
|
||||
reranked = true;
|
||||
} catch (RerankException e) {
|
||||
LOG.warn("Rerank failed for collectionId={}, modelId={}, fallback to retrieved results. message={}",
|
||||
@@ -320,6 +333,84 @@ public class DocumentCollectionServiceImpl extends ServiceImpl<DocumentCollectio
|
||||
return !reranked && retrievalMode == RetrievalMode.VECTOR;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析本次查询使用的召回上限,优先采用请求参数,其次回退到知识库默认配置。
|
||||
*
|
||||
* @param request 查询请求
|
||||
* @param documentCollection 知识库实体
|
||||
* @return 规范化后的召回上限
|
||||
*/
|
||||
private int resolveDocRecallMaxNum(KnowledgeRetrievalRequest request, DocumentCollection documentCollection) {
|
||||
Integer requestLimit = request == null ? null : request.getLimit();
|
||||
if (requestLimit != null) {
|
||||
return Math.max(requestLimit, 1);
|
||||
}
|
||||
return readIntegerOption(documentCollection, KEY_DOC_RECALL_MAX_NUM, 5);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析本次查询使用的最小相似度阈值,优先采用请求参数,其次回退到知识库默认配置。
|
||||
*
|
||||
* @param request 查询请求
|
||||
* @param documentCollection 知识库实体
|
||||
* @return 规范化后的最小相似度
|
||||
*/
|
||||
private float resolveMinSimilarity(KnowledgeRetrievalRequest request, DocumentCollection documentCollection) {
|
||||
Double requestMinSimilarity = request == null ? null : request.getMinSimilarity();
|
||||
if (requestMinSimilarity != null) {
|
||||
double normalizedValue = Math.max(0D, Math.min(requestMinSimilarity, 1D));
|
||||
return (float) normalizedValue;
|
||||
}
|
||||
return readFloatOption(documentCollection, KEY_SIMILARITY_THRESHOLD, 0.6F);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算内部候选召回上限。
|
||||
*
|
||||
* @param docRecallMaxNum 业务召回上限
|
||||
* @return 内部候选集上限
|
||||
*/
|
||||
private int resolveInternalRecallLimit(int docRecallMaxNum) {
|
||||
int normalizedLimit = Math.max(docRecallMaxNum, 1);
|
||||
return Math.min(normalizedLimit * INTERNAL_RECALL_MULTIPLIER, MAX_INTERNAL_RECALL_LIMIT);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在重排前过滤掉未完成文档对应的 chunk 命中,避免进行中的文档占用正式召回名额。
|
||||
*
|
||||
* @param documentCollection 知识库
|
||||
* @param searchDocuments 当前召回结果
|
||||
* @return 仅保留完成态文档命中的结果
|
||||
*/
|
||||
private List<Document> prepareSearchDocuments(DocumentCollection documentCollection, List<Document> searchDocuments) {
|
||||
if (searchDocuments == null || searchDocuments.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
if (documentCollection == null) {
|
||||
return searchDocuments;
|
||||
}
|
||||
if (documentCollection.isFaqCollection()) {
|
||||
fillSearchContent(documentCollection, searchDocuments);
|
||||
return searchDocuments;
|
||||
}
|
||||
DocumentHitSnapshot hitSnapshot = loadDocumentHitSnapshot(documentCollection, searchDocuments);
|
||||
if (hitSnapshot.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
return searchDocuments.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.filter(item -> {
|
||||
String content = hitSnapshot.findChunkContent(item.getId());
|
||||
if (!StringUtil.hasText(content)) {
|
||||
return false;
|
||||
}
|
||||
item.setContent(content);
|
||||
return true;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public DocumentCollection getDetail(String idOrAlias) {
|
||||
DocumentCollection knowledge = null;
|
||||
@@ -418,18 +509,93 @@ public class DocumentCollectionServiceImpl extends ServiceImpl<DocumentCollectio
|
||||
return;
|
||||
}
|
||||
|
||||
QueryWrapper queryWrapper = QueryWrapper.create();
|
||||
queryWrapper.in(DocumentChunk::getId, ids);
|
||||
queryWrapper.eq(DocumentChunk::getDocumentCollectionId, documentCollection.getId());
|
||||
Map<String, DocumentChunk> chunkMap = documentChunkMapper.selectListByQuery(queryWrapper).stream()
|
||||
.collect(Collectors.toMap(item -> item.getId().toString(), item -> item, (a, b) -> a));
|
||||
searchDocuments.removeIf(item -> !chunkMap.containsKey(String.valueOf(item.getId())));
|
||||
DocumentHitSnapshot hitSnapshot = loadDocumentHitSnapshot(documentCollection, searchDocuments);
|
||||
searchDocuments.forEach(item -> {
|
||||
DocumentChunk documentChunk = chunkMap.get(String.valueOf(item.getId()));
|
||||
if (documentChunk != null && !StringUtil.noText(documentChunk.getContent())) {
|
||||
item.setContent(documentChunk.getContent());
|
||||
}
|
||||
item.setContent(hitSnapshot.findChunkContent(item.getId()));
|
||||
});
|
||||
searchDocuments.removeIf(item -> !StringUtil.hasText(item.getContent()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量加载命中 chunk 及其完成态父文档,供过滤和内容填充复用。
|
||||
*
|
||||
* @param documentCollection 知识库
|
||||
* @param searchDocuments 检索命中
|
||||
* @return 命中快照
|
||||
*/
|
||||
private DocumentHitSnapshot loadDocumentHitSnapshot(DocumentCollection documentCollection, List<Document> searchDocuments) {
|
||||
if (documentCollection == null || searchDocuments == null || searchDocuments.isEmpty()) {
|
||||
return DocumentHitSnapshot.empty();
|
||||
}
|
||||
List<Serializable> chunkIds = searchDocuments.stream()
|
||||
.map(Document::getId)
|
||||
.filter(Objects::nonNull)
|
||||
.map(item -> (Serializable) item)
|
||||
.collect(Collectors.toList());
|
||||
if (chunkIds.isEmpty()) {
|
||||
return DocumentHitSnapshot.empty();
|
||||
}
|
||||
|
||||
QueryWrapper chunkWrapper = QueryWrapper.create();
|
||||
chunkWrapper.in(DocumentChunk::getId, chunkIds);
|
||||
chunkWrapper.eq(DocumentChunk::getDocumentCollectionId, documentCollection.getId());
|
||||
Map<String, DocumentChunk> chunkMap = documentChunkMapper.selectListByQuery(chunkWrapper).stream()
|
||||
.collect(Collectors.toMap(item -> item.getId().toString(), item -> item, (a, b) -> a));
|
||||
if (chunkMap.isEmpty()) {
|
||||
return DocumentHitSnapshot.empty();
|
||||
}
|
||||
|
||||
List<Serializable> documentIds = chunkMap.values().stream()
|
||||
.map(DocumentChunk::getDocumentId)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.map(item -> (Serializable) item)
|
||||
.collect(Collectors.toList());
|
||||
if (documentIds.isEmpty()) {
|
||||
return DocumentHitSnapshot.empty();
|
||||
}
|
||||
|
||||
QueryWrapper documentWrapper = QueryWrapper.create();
|
||||
documentWrapper.in(tech.easyflow.ai.entity.Document::getId, documentIds);
|
||||
documentWrapper.eq(tech.easyflow.ai.entity.Document::getCollectionId, documentCollection.getId());
|
||||
documentWrapper.eq(tech.easyflow.ai.entity.Document::getProcessStatus, DocumentProcessStatus.COMPLETED.name());
|
||||
Map<String, tech.easyflow.ai.entity.Document> documentMap = documentMapper.selectListByQuery(documentWrapper).stream()
|
||||
.collect(Collectors.toMap(item -> item.getId().toString(), item -> item, (a, b) -> a));
|
||||
return new DocumentHitSnapshot(chunkMap, documentMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* 文档检索命中的批量快照,避免过滤和填充阶段重复查询。
|
||||
*/
|
||||
private static class DocumentHitSnapshot {
|
||||
|
||||
private final Map<String, DocumentChunk> chunkMap;
|
||||
private final Map<String, tech.easyflow.ai.entity.Document> documentMap;
|
||||
|
||||
private DocumentHitSnapshot(Map<String, DocumentChunk> chunkMap,
|
||||
Map<String, tech.easyflow.ai.entity.Document> documentMap) {
|
||||
this.chunkMap = chunkMap == null ? Collections.emptyMap() : chunkMap;
|
||||
this.documentMap = documentMap == null ? Collections.emptyMap() : documentMap;
|
||||
}
|
||||
|
||||
private static DocumentHitSnapshot empty() {
|
||||
return new DocumentHitSnapshot(Collections.emptyMap(), Collections.emptyMap());
|
||||
}
|
||||
|
||||
private boolean isEmpty() {
|
||||
return chunkMap.isEmpty() || documentMap.isEmpty();
|
||||
}
|
||||
|
||||
private String findChunkContent(Object chunkId) {
|
||||
DocumentChunk documentChunk = chunkMap.get(String.valueOf(chunkId));
|
||||
if (documentChunk == null || documentChunk.getDocumentId() == null) {
|
||||
return null;
|
||||
}
|
||||
if (!documentMap.containsKey(String.valueOf(documentChunk.getDocumentId()))) {
|
||||
return null;
|
||||
}
|
||||
return StringUtil.noText(documentChunk.getContent()) ? null : documentChunk.getContent();
|
||||
}
|
||||
}
|
||||
|
||||
private String buildFaqPromptContent(FaqItem faqItem, List<Map<String, String>> images) {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package tech.easyflow.ai.service.impl;
|
||||
|
||||
import com.mybatisflex.spring.service.impl.ServiceImpl;
|
||||
import org.springframework.stereotype.Service;
|
||||
import tech.easyflow.ai.entity.DocumentImportTask;
|
||||
import tech.easyflow.ai.mapper.DocumentImportTaskMapper;
|
||||
import tech.easyflow.ai.service.DocumentImportTaskService;
|
||||
|
||||
/**
|
||||
* 文档导入任务服务实现。
|
||||
*
|
||||
* @author Codex
|
||||
* @since 2026-04-14
|
||||
*/
|
||||
@Service
|
||||
public class DocumentImportTaskServiceImpl extends ServiceImpl<DocumentImportTaskMapper, DocumentImportTask>
|
||||
implements DocumentImportTaskService {
|
||||
}
|
||||
@@ -34,7 +34,9 @@ import tech.easyflow.ai.config.SearcherFactory;
|
||||
import tech.easyflow.ai.documentimport.DocumentImportDtos;
|
||||
import tech.easyflow.ai.documentimport.DocumentImportKeys;
|
||||
import tech.easyflow.ai.documentimport.DocumentImportPreviewService;
|
||||
import tech.easyflow.ai.documentimport.task.KnowledgeDocumentImportTaskAppService;
|
||||
import tech.easyflow.ai.entity.*;
|
||||
import tech.easyflow.ai.enums.DocumentProcessStatus;
|
||||
import tech.easyflow.ai.mapper.DocumentChunkMapper;
|
||||
import tech.easyflow.ai.mapper.DocumentMapper;
|
||||
import tech.easyflow.ai.service.DocumentChunkService;
|
||||
@@ -69,6 +71,7 @@ import static tech.easyflow.ai.entity.table.DocumentTableDef.DOCUMENT;
|
||||
@Service("AiService")
|
||||
public class DocumentServiceImpl extends ServiceImpl<DocumentMapper, Document> implements DocumentService {
|
||||
protected Logger Log = LoggerFactory.getLogger(DocumentServiceImpl.class);
|
||||
private static final String SOURCE_RANGES_KEY = "sourceRanges";
|
||||
|
||||
@Resource
|
||||
private DocumentMapper documentMapper;
|
||||
@@ -97,6 +100,9 @@ public class DocumentServiceImpl extends ServiceImpl<DocumentMapper, Document> i
|
||||
@Autowired
|
||||
private DocumentImportPreviewService documentImportPreviewService;
|
||||
|
||||
@Autowired
|
||||
private KnowledgeDocumentImportTaskAppService importTaskAppService;
|
||||
|
||||
@Override
|
||||
public Page<Document> getDocumentList(String knowledgeId, int pageSize, int pageNum, String fileName) {
|
||||
QueryWrapper queryWrapper=QueryWrapper.create()
|
||||
@@ -130,6 +136,13 @@ public class DocumentServiceImpl extends ServiceImpl<DocumentMapper, Document> i
|
||||
// 查询该文档对应哪些分割的字段,先删除
|
||||
QueryWrapper queryWrapperDocument = QueryWrapper.create().eq(Document::getId, id);
|
||||
Document oneByQuery = documentMapper.selectOneByQuery(queryWrapperDocument);
|
||||
if (oneByQuery == null) {
|
||||
return false;
|
||||
}
|
||||
if (DocumentProcessStatus.PARSING.name().equals(oneByQuery.getProcessStatus())
|
||||
|| DocumentProcessStatus.INDEXING.name().equals(oneByQuery.getProcessStatus())) {
|
||||
throw new BusinessException("文档处理中,暂不允许删除");
|
||||
}
|
||||
DocumentCollection knowledge = knowledgeService.getById(oneByQuery.getCollectionId());
|
||||
if (knowledge == null) {
|
||||
return false;
|
||||
@@ -214,6 +227,12 @@ public class DocumentServiceImpl extends ServiceImpl<DocumentMapper, Document> i
|
||||
aiDocument.setChunkSize(documentCollectionSplitParams.getChunkSize());
|
||||
aiDocument.setOverlapSize(documentCollectionSplitParams.getOverlapSize());
|
||||
aiDocument.setTitle(fileOriginName);
|
||||
aiDocument.setProcessStatus(DocumentProcessStatus.COMPLETED.name());
|
||||
aiDocument.setTotalChunks(previewList.size());
|
||||
aiDocument.setCompletedChunks(previewList.size());
|
||||
aiDocument.setFailedChunks(0);
|
||||
aiDocument.setProgressPercent(100);
|
||||
aiDocument.setTaskModifiedAt(new Date());
|
||||
Map<String, Object> res = new HashMap<>();
|
||||
|
||||
List<DocumentChunk> documentChunks = null;
|
||||
@@ -334,10 +353,11 @@ public class DocumentServiceImpl extends ServiceImpl<DocumentMapper, Document> i
|
||||
item.setPreviewSessionId(sessionId);
|
||||
item.setFilePath(file.getFilePath());
|
||||
item.setFileName(file.getFileName());
|
||||
item.setNormalizedContent(session.getAnalysis() == null ? null : session.getAnalysis().getNormalizedContent());
|
||||
item.setStrategyCode(session.getStrategyConfig().getStrategyCode());
|
||||
item.setStrategyLabel(ragIngestionService.toStrategyLabel(session.getStrategyConfig().getStrategyCode()));
|
||||
item.setAnalysis(session.getAnalysis());
|
||||
item.setChunks(session.getPreviewChunks());
|
||||
item.setChunks(toPreviewChunkResults(session.getPreviewChunks()));
|
||||
item.setTotalChunks(session.getPreviewChunks().size());
|
||||
item.setTotalWarnings(countWarnings(session.getPreviewChunks()));
|
||||
items.add(item);
|
||||
@@ -398,6 +418,12 @@ public class DocumentServiceImpl extends ServiceImpl<DocumentMapper, Document> i
|
||||
document.setModified(new Date());
|
||||
document.setCreatedBy(BigInteger.valueOf(StpUtil.getLoginIdAsLong()));
|
||||
document.setModifiedBy(BigInteger.valueOf(StpUtil.getLoginIdAsLong()));
|
||||
document.setProcessStatus(DocumentProcessStatus.COMPLETED.name());
|
||||
document.setTotalChunks(session.getDocumentChunks().size());
|
||||
document.setCompletedChunks(session.getDocumentChunks().size());
|
||||
document.setFailedChunks(0);
|
||||
document.setProgressPercent(100);
|
||||
document.setTaskModifiedAt(new Date());
|
||||
for (DocumentChunk chunk : session.getDocumentChunks()) {
|
||||
chunk.setDocumentId(document.getId());
|
||||
chunk.setDocumentCollectionId(document.getCollectionId());
|
||||
@@ -430,6 +456,7 @@ public class DocumentServiceImpl extends ServiceImpl<DocumentMapper, Document> i
|
||||
|
||||
DocumentImportDtos.PreviewSession session = new DocumentImportDtos.PreviewSession();
|
||||
session.setKnowledgeId(knowledge.getId());
|
||||
session.setDocumentId(document.getId());
|
||||
session.setFilePath(fileRequest.getFilePath());
|
||||
session.setFileName(fileRequest.getFileName());
|
||||
session.setSourceFormat(analysis.getSourceFormat());
|
||||
@@ -656,6 +683,55 @@ public class DocumentServiceImpl extends ServiceImpl<DocumentMapper, Document> i
|
||||
return total;
|
||||
}
|
||||
|
||||
private List<DocumentImportDtos.PreviewChunkResult> toPreviewChunkResults(List<RagChunk> chunks) {
|
||||
List<DocumentImportDtos.PreviewChunkResult> result = new ArrayList<>();
|
||||
if (chunks == null) {
|
||||
return result;
|
||||
}
|
||||
for (RagChunk chunk : chunks) {
|
||||
DocumentImportDtos.PreviewChunkResult item = new DocumentImportDtos.PreviewChunkResult();
|
||||
item.setAnswer(chunk.getAnswer());
|
||||
item.setCharCount(chunk.getCharCount());
|
||||
item.setChunkId(chunk.getChunkId());
|
||||
item.setChunkType(chunk.getChunkType());
|
||||
item.setContent(chunk.getContent());
|
||||
item.setHeadingPath(chunk.getHeadingPath() == null ? new ArrayList<>() : new ArrayList<>(chunk.getHeadingPath()));
|
||||
item.setPartNo(chunk.getPartNo());
|
||||
item.setPartTotal(chunk.getPartTotal());
|
||||
item.setQuestion(chunk.getQuestion());
|
||||
item.setSourceLabel(chunk.getSourceLabel());
|
||||
item.setTokenEstimate(chunk.getTokenEstimate());
|
||||
item.setWarnings(chunk.getWarnings() == null ? new ArrayList<>() : new ArrayList<>(chunk.getWarnings()));
|
||||
item.setSourceRanges(copySourceRanges(chunk));
|
||||
result.add(item);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private List<DocumentImportDtos.PreviewSourceRange> copySourceRanges(RagChunk chunk) {
|
||||
List<DocumentImportDtos.PreviewSourceRange> result = new ArrayList<>();
|
||||
if (chunk == null || chunk.getOptions() == null) {
|
||||
return result;
|
||||
}
|
||||
Object rawRanges = chunk.getOptions().get(SOURCE_RANGES_KEY);
|
||||
if (!(rawRanges instanceof List<?> rangeList)) {
|
||||
return result;
|
||||
}
|
||||
for (Object item : rangeList) {
|
||||
if (!(item instanceof Map<?, ?> rangeMap)) {
|
||||
continue;
|
||||
}
|
||||
DocumentImportDtos.PreviewSourceRange range = new DocumentImportDtos.PreviewSourceRange();
|
||||
range.setStart(asInteger(rangeMap.get("start"), null));
|
||||
range.setEnd(asInteger(rangeMap.get("end"), null));
|
||||
if (range.getStart() != null && range.getEnd() != null) {
|
||||
result.add(range);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private StoreExecutionContext prepareStoreContext(Document entity) {
|
||||
DocumentCollection knowledge = knowledgeService.getById(entity.getCollectionId());
|
||||
if (knowledge == null) {
|
||||
@@ -882,4 +958,34 @@ public class DocumentServiceImpl extends ServiceImpl<DocumentMapper, Document> i
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<DocumentImportDtos.TaskCreateResponse> createImportTask(DocumentImportDtos.TaskCreateRequest request) {
|
||||
return importTaskAppService.createImportTask(request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<DocumentImportDtos.TaskDetailResponse> getImportTaskDetail(BigInteger taskId) {
|
||||
return importTaskAppService.getImportTaskDetail(taskId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<DocumentImportDtos.PreviewResponse> previewImportTask(DocumentImportDtos.PreviewRequest request) {
|
||||
return importTaskAppService.previewImportTask(request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<DocumentImportDtos.TaskStartIndexResponse> startIndexTask(DocumentImportDtos.TaskStartIndexRequest request) {
|
||||
return importTaskAppService.startIndexTask(request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<DocumentImportDtos.TaskStartIndexResponse> retryParseTask(DocumentImportDtos.TaskRetryRequest request) {
|
||||
return importTaskAppService.retryParseTask(request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<DocumentImportDtos.TaskStartIndexResponse> retryIndexTask(DocumentImportDtos.TaskRetryRequest request) {
|
||||
return importTaskAppService.retryIndexTask(request);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
package tech.easyflow.ai.service.impl;
|
||||
|
||||
import com.easyagents.core.model.embedding.EmbeddingModel;
|
||||
import com.easyagents.core.store.DocumentStore;
|
||||
import com.easyagents.core.store.StoreOptions;
|
||||
import com.easyagents.core.store.StoreResult;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
import tech.easyflow.ai.entity.DocumentChunk;
|
||||
import tech.easyflow.ai.entity.DocumentCollection;
|
||||
import tech.easyflow.ai.entity.FaqItem;
|
||||
import tech.easyflow.ai.entity.Model;
|
||||
import tech.easyflow.ai.service.DocumentChunkService;
|
||||
import tech.easyflow.ai.service.DocumentCollectionService;
|
||||
import tech.easyflow.ai.service.FaqItemService;
|
||||
import tech.easyflow.ai.service.KnowledgeEmbeddingService;
|
||||
import tech.easyflow.ai.service.ModelService;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 知识库向量重建服务实现。
|
||||
*/
|
||||
@Service
|
||||
public class KnowledgeEmbeddingServiceImpl implements KnowledgeEmbeddingService {
|
||||
|
||||
@Resource
|
||||
private DocumentCollectionService documentCollectionService;
|
||||
@Resource
|
||||
private DocumentChunkService documentChunkService;
|
||||
@Resource
|
||||
private FaqItemService faqItemService;
|
||||
@Resource
|
||||
private ModelService modelService;
|
||||
|
||||
@Override
|
||||
public void rebuildKnowledgeVectors(BigInteger knowledgeId) {
|
||||
DocumentCollection knowledge = documentCollectionService.getById(knowledgeId);
|
||||
if (knowledge == null) {
|
||||
throw new BusinessException("知识库不存在");
|
||||
}
|
||||
DocumentStore documentStore = knowledge.toDocumentStore();
|
||||
if (documentStore == null) {
|
||||
throw new BusinessException("知识库没有配置向量库");
|
||||
}
|
||||
Model model = modelService.getModelInstance(knowledge.getVectorEmbedModelId());
|
||||
if (model == null) {
|
||||
throw new BusinessException("知识库没有配置向量模型");
|
||||
}
|
||||
EmbeddingModel embeddingModel = model.toEmbeddingModel();
|
||||
documentStore.setEmbeddingModel(embeddingModel);
|
||||
StoreOptions storeOptions = StoreOptions.ofCollectionName(knowledge.getVectorStoreCollection());
|
||||
storeOptions.setIndexName(knowledge.getVectorStoreCollection());
|
||||
|
||||
if (knowledge.isFaqCollection()) {
|
||||
rebuildFaqVectors(knowledge, documentStore, storeOptions, embeddingModel);
|
||||
return;
|
||||
}
|
||||
rebuildDocumentVectors(knowledge, documentStore, storeOptions, embeddingModel);
|
||||
}
|
||||
|
||||
private void rebuildDocumentVectors(
|
||||
DocumentCollection knowledge,
|
||||
DocumentStore documentStore,
|
||||
StoreOptions storeOptions,
|
||||
EmbeddingModel embeddingModel
|
||||
) {
|
||||
QueryWrapper wrapper = QueryWrapper.create()
|
||||
.eq(DocumentChunk::getDocumentCollectionId, knowledge.getId())
|
||||
.orderBy("sorting asc");
|
||||
List<DocumentChunk> chunks = documentChunkService.list(wrapper);
|
||||
List<BigInteger> ids = new ArrayList<>();
|
||||
List<com.easyagents.core.document.Document> documents = new ArrayList<>();
|
||||
for (DocumentChunk chunk : chunks) {
|
||||
ids.add(chunk.getId());
|
||||
com.easyagents.core.document.Document document = com.easyagents.core.document.Document.of(chunk.getContent());
|
||||
document.setId(chunk.getId());
|
||||
documents.add(document);
|
||||
}
|
||||
rewriteStore(documentStore, storeOptions, ids, documents);
|
||||
updateKnowledgeEmbeddingState(knowledge, embeddingModel);
|
||||
}
|
||||
|
||||
private void rebuildFaqVectors(
|
||||
DocumentCollection knowledge,
|
||||
DocumentStore documentStore,
|
||||
StoreOptions storeOptions,
|
||||
EmbeddingModel embeddingModel
|
||||
) {
|
||||
QueryWrapper wrapper = QueryWrapper.create()
|
||||
.eq(FaqItem::getCollectionId, knowledge.getId())
|
||||
.orderBy("order_no asc");
|
||||
List<FaqItem> faqItems = faqItemService.list(wrapper);
|
||||
List<BigInteger> ids = new ArrayList<>();
|
||||
List<com.easyagents.core.document.Document> documents = new ArrayList<>();
|
||||
for (FaqItem faqItem : faqItems) {
|
||||
ids.add(faqItem.getId());
|
||||
StringBuilder content = new StringBuilder();
|
||||
content.append("问题:").append(faqItem.getQuestion());
|
||||
if (faqItem.getAnswerText() != null && !faqItem.getAnswerText().isBlank()) {
|
||||
content.append("\n答案:").append(faqItem.getAnswerText());
|
||||
}
|
||||
com.easyagents.core.document.Document document =
|
||||
com.easyagents.core.document.Document.of(content.toString());
|
||||
document.setId(faqItem.getId());
|
||||
Map<String, Object> metadata = new HashMap<>();
|
||||
metadata.put("question", faqItem.getQuestion());
|
||||
metadata.put("answerText", faqItem.getAnswerText());
|
||||
metadata.put("categoryId", faqItem.getCategoryId());
|
||||
document.setMetadataMap(metadata);
|
||||
documents.add(document);
|
||||
}
|
||||
rewriteStore(documentStore, storeOptions, ids, documents);
|
||||
updateKnowledgeEmbeddingState(knowledge, embeddingModel);
|
||||
}
|
||||
|
||||
private void rewriteStore(
|
||||
DocumentStore documentStore,
|
||||
StoreOptions storeOptions,
|
||||
List<BigInteger> ids,
|
||||
List<com.easyagents.core.document.Document> documents
|
||||
) {
|
||||
if (!ids.isEmpty()) {
|
||||
documentStore.delete(ids, storeOptions);
|
||||
}
|
||||
if (documents.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
StoreResult result = documentStore.store(documents, storeOptions);
|
||||
if (result == null || !result.isSuccess()) {
|
||||
throw new BusinessException("知识库向量重建失败");
|
||||
}
|
||||
}
|
||||
|
||||
private void updateKnowledgeEmbeddingState(DocumentCollection knowledge, EmbeddingModel embeddingModel) {
|
||||
DocumentCollection update = new DocumentCollection();
|
||||
update.setId(knowledge.getId());
|
||||
Map<String, Object> options = knowledge.getOptions() == null
|
||||
? new HashMap<>()
|
||||
: new HashMap<>(knowledge.getOptions());
|
||||
options.put(DocumentCollection.KEY_CAN_UPDATE_EMBEDDING_MODEL, false);
|
||||
update.setOptions(options);
|
||||
if (knowledge.getDimensionOfVectorModel() == null) {
|
||||
update.setDimensionOfVectorModel(Model.getEmbeddingDimension(embeddingModel));
|
||||
}
|
||||
documentCollectionService.updateById(update);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package tech.easyflow.ai.service.impl;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
import tech.easyflow.ai.service.KnowledgeShareAuditService;
|
||||
import tech.easyflow.common.util.RequestUtil;
|
||||
import tech.easyflow.system.entity.SysLog;
|
||||
import tech.easyflow.system.service.SysLogService;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 知识库分享审计服务实现。
|
||||
*/
|
||||
@Service
|
||||
public class KnowledgeShareAuditServiceImpl implements KnowledgeShareAuditService {
|
||||
|
||||
@Resource
|
||||
private SysLogService sysLogService;
|
||||
|
||||
@Override
|
||||
public void log(BigInteger accountId, String actionName, String actionType, String actionUrl, Map<String, Object> detail) {
|
||||
SysLog log = new SysLog();
|
||||
log.setAccountId(accountId);
|
||||
log.setActionName(actionName);
|
||||
log.setActionType(actionType);
|
||||
log.setActionUrl(actionUrl);
|
||||
log.setActionBody(detail == null ? null : JSON.toJSONString(detail));
|
||||
log.setCreated(new Date());
|
||||
log.setStatus(0);
|
||||
ServletRequestAttributes attributes =
|
||||
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
if (attributes != null) {
|
||||
HttpServletRequest request = attributes.getRequest();
|
||||
log.setActionIp(RequestUtil.getIpAddress(request));
|
||||
log.setActionParams(request.getQueryString());
|
||||
}
|
||||
sysLogService.save(log);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
package tech.easyflow.ai.service.impl;
|
||||
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import tech.easyflow.ai.enums.KnowledgeShareActionScope;
|
||||
import tech.easyflow.ai.service.KnowledgeSharePermissionService;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.system.entity.SysApiKey;
|
||||
import tech.easyflow.system.entity.SysApiKeyResource;
|
||||
import tech.easyflow.system.entity.SysApiKeyResourceMapping;
|
||||
import tech.easyflow.system.service.SysApiKeyResourceMappingService;
|
||||
import tech.easyflow.system.service.SysApiKeyResourceService;
|
||||
import tech.easyflow.system.service.SysApiKeyService;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 知识库分享权限服务实现。
|
||||
*/
|
||||
@Service
|
||||
public class KnowledgeSharePermissionServiceImpl implements KnowledgeSharePermissionService {
|
||||
|
||||
public static final String RESOURCE_TYPE_KNOWLEDGE = "KNOWLEDGE";
|
||||
|
||||
private static final Map<String, List<String>> URI_SCOPE_MAPPING = new LinkedHashMap<>();
|
||||
|
||||
static {
|
||||
URI_SCOPE_MAPPING.put(KnowledgeShareActionScope.VIEW.name(), List.of(
|
||||
"/public-api/knowledge-share/detail",
|
||||
"/public-api/knowledge-share/document/page",
|
||||
"/public-api/knowledge-share/document/download",
|
||||
"/public-api/knowledge-share/document/import/task/detail",
|
||||
"/public-api/knowledge-share/documentChunk/page",
|
||||
"/public-api/knowledge-share/faq/page",
|
||||
"/public-api/knowledge-share/faq/detail"
|
||||
));
|
||||
URI_SCOPE_MAPPING.put(KnowledgeShareActionScope.SEARCH.name(), List.of(
|
||||
"/public-api/knowledge-share/search"
|
||||
));
|
||||
URI_SCOPE_MAPPING.put(KnowledgeShareActionScope.CONTENT_CREATE.name(), List.of(
|
||||
"/public-api/knowledge-share/document/import/analyze",
|
||||
"/public-api/knowledge-share/document/import/preview",
|
||||
"/public-api/knowledge-share/document/import/commit",
|
||||
"/public-api/knowledge-share/document/import/task/create",
|
||||
"/public-api/knowledge-share/document/import/task/preview",
|
||||
"/public-api/knowledge-share/document/import/task/startIndex",
|
||||
"/public-api/knowledge-share/document/import/task/retryParse",
|
||||
"/public-api/knowledge-share/document/import/task/retryIndex",
|
||||
"/public-api/knowledge-share/faq/save"
|
||||
));
|
||||
URI_SCOPE_MAPPING.put(KnowledgeShareActionScope.CONTENT_UPDATE.name(), List.of(
|
||||
"/public-api/knowledge-share/documentChunk/update",
|
||||
"/public-api/knowledge-share/faq/update"
|
||||
));
|
||||
URI_SCOPE_MAPPING.put(KnowledgeShareActionScope.CONTENT_DELETE.name(), List.of(
|
||||
"/public-api/knowledge-share/document/remove",
|
||||
"/public-api/knowledge-share/documentChunk/remove",
|
||||
"/public-api/knowledge-share/faq/remove"
|
||||
));
|
||||
URI_SCOPE_MAPPING.put(KnowledgeShareActionScope.IMPORT_EXPORT.name(), List.of(
|
||||
"/public-api/knowledge-share/faq/importExcel",
|
||||
"/public-api/knowledge-share/faq/exportExcel",
|
||||
"/public-api/knowledge-share/faq/downloadImportTemplate"
|
||||
));
|
||||
}
|
||||
|
||||
@Resource
|
||||
private SysApiKeyService sysApiKeyService;
|
||||
@Resource
|
||||
private SysApiKeyResourceService resourceService;
|
||||
@Resource
|
||||
private SysApiKeyResourceMappingService mappingService;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void grantApiShare(BigInteger apiKeyId, BigInteger knowledgeId, Set<String> actionScopes) {
|
||||
if (apiKeyId == null) {
|
||||
throw new BusinessException("系统访问令牌不能为空");
|
||||
}
|
||||
if (knowledgeId == null) {
|
||||
throw new BusinessException("知识库不能为空");
|
||||
}
|
||||
SysApiKey apiKey = sysApiKeyService.getById(apiKeyId);
|
||||
if (apiKey == null) {
|
||||
throw new BusinessException("系统访问令牌不存在");
|
||||
}
|
||||
Set<String> normalizedScopes = KnowledgeShareActionScope.normalize(actionScopes);
|
||||
if (normalizedScopes.isEmpty()) {
|
||||
throw new BusinessException("动作范围不能为空");
|
||||
}
|
||||
|
||||
mappingService.removeScopedMappings(apiKeyId, RESOURCE_TYPE_KNOWLEDGE, knowledgeId);
|
||||
List<SysApiKeyResourceMapping> rows = new ArrayList<>();
|
||||
for (String scope : normalizedScopes) {
|
||||
List<String> uris = URI_SCOPE_MAPPING.get(scope);
|
||||
if (uris == null || uris.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
for (String uri : uris) {
|
||||
SysApiKeyResource resource = ensureResource(uri);
|
||||
SysApiKeyResourceMapping row = new SysApiKeyResourceMapping();
|
||||
row.setApiKeyId(apiKeyId);
|
||||
row.setApiKeyResourceId(resource.getId());
|
||||
row.setResourceType(RESOURCE_TYPE_KNOWLEDGE);
|
||||
row.setResourceTargetId(knowledgeId);
|
||||
row.setActionScope(scope);
|
||||
rows.add(row);
|
||||
}
|
||||
}
|
||||
if (!rows.isEmpty()) {
|
||||
mappingService.saveBatch(rows);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void replaceApiShareEnabled(BigInteger apiKeyId, boolean enabled) {
|
||||
if (apiKeyId == null) {
|
||||
throw new BusinessException("系统访问令牌不能为空");
|
||||
}
|
||||
SysApiKey apiKey = sysApiKeyService.getById(apiKeyId);
|
||||
if (apiKey == null) {
|
||||
throw new BusinessException("系统访问令牌不存在");
|
||||
}
|
||||
mappingService.removeScopedMappings(apiKeyId, RESOURCE_TYPE_KNOWLEDGE);
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<SysApiKeyResourceMapping> rows = new ArrayList<>();
|
||||
for (String scope : KnowledgeShareActionScope.defaultApiScopes()) {
|
||||
List<String> uris = URI_SCOPE_MAPPING.get(scope);
|
||||
if (uris == null || uris.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
for (String uri : uris) {
|
||||
SysApiKeyResource resource = ensureResource(uri);
|
||||
SysApiKeyResourceMapping row = new SysApiKeyResourceMapping();
|
||||
row.setApiKeyId(apiKeyId);
|
||||
row.setApiKeyResourceId(resource.getId());
|
||||
row.setResourceType(RESOURCE_TYPE_KNOWLEDGE);
|
||||
row.setActionScope(scope);
|
||||
rows.add(row);
|
||||
}
|
||||
}
|
||||
if (!rows.isEmpty()) {
|
||||
mappingService.saveBatch(rows);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void assertApiShare(BigInteger apiKeyId, String requestUri, BigInteger knowledgeId, String actionScope) {
|
||||
if (apiKeyId == null || knowledgeId == null) {
|
||||
throw new BusinessException("API 分享鉴权参数不完整");
|
||||
}
|
||||
sysApiKeyService.checkResourceScope(apiKeyId, requestUri, RESOURCE_TYPE_KNOWLEDGE, knowledgeId, actionScope);
|
||||
}
|
||||
|
||||
private SysApiKeyResource ensureResource(String requestInterface) {
|
||||
QueryWrapper wrapper = QueryWrapper.create()
|
||||
.eq(SysApiKeyResource::getRequestInterface, requestInterface);
|
||||
SysApiKeyResource resource = resourceService.getOne(wrapper);
|
||||
if (resource != null) {
|
||||
return resource;
|
||||
}
|
||||
resource = new SysApiKeyResource();
|
||||
resource.setRequestInterface(requestInterface);
|
||||
resource.setTitle("知识库分享接口");
|
||||
resourceService.save(resource);
|
||||
return resource;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
package tech.easyflow.ai.service.impl;
|
||||
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import com.mybatisflex.spring.service.impl.ServiceImpl;
|
||||
import org.springframework.stereotype.Service;
|
||||
import tech.easyflow.ai.constants.KnowledgeShareErrorCode;
|
||||
import tech.easyflow.ai.entity.DocumentCollection;
|
||||
import tech.easyflow.ai.entity.KnowledgeShare;
|
||||
import tech.easyflow.ai.enums.KnowledgeShareStatus;
|
||||
import tech.easyflow.ai.enums.KnowledgeShareType;
|
||||
import tech.easyflow.ai.mapper.KnowledgeShareMapper;
|
||||
import tech.easyflow.ai.service.DocumentCollectionService;
|
||||
import tech.easyflow.ai.service.KnowledgeShareService;
|
||||
import tech.easyflow.ai.vo.KnowledgeShareAuthContext;
|
||||
import tech.easyflow.ai.vo.KnowledgeShareUrlCreateResult;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigInteger;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.Duration;
|
||||
import java.util.Date;
|
||||
import java.util.HexFormat;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 知识库分享服务实现。
|
||||
*/
|
||||
@Service
|
||||
public class KnowledgeShareServiceImpl extends ServiceImpl<KnowledgeShareMapper, KnowledgeShare> implements KnowledgeShareService {
|
||||
|
||||
private static final Duration DEFAULT_EXPIRE_DURATION = Duration.ofMinutes(30);
|
||||
|
||||
@Resource
|
||||
private DocumentCollectionService documentCollectionService;
|
||||
|
||||
@Override
|
||||
public KnowledgeShareUrlCreateResult createUrlShare(
|
||||
BigInteger knowledgeId,
|
||||
BigInteger tenantId,
|
||||
BigInteger deptId,
|
||||
BigInteger operatorId,
|
||||
String baseUrl,
|
||||
Set<String> permissionScopes
|
||||
) {
|
||||
DocumentCollection knowledge = documentCollectionService.getById(knowledgeId);
|
||||
if (knowledge == null) {
|
||||
throw new BusinessException("知识库不存在");
|
||||
}
|
||||
invalidateExistingUrlShares(knowledgeId, operatorId);
|
||||
|
||||
String shareKey = UUID.randomUUID().toString().replace("-", "");
|
||||
Date now = new Date();
|
||||
Date expiresAt = new Date(now.getTime() + DEFAULT_EXPIRE_DURATION.toMillis());
|
||||
|
||||
KnowledgeShare share = new KnowledgeShare();
|
||||
share.setKnowledgeId(knowledgeId);
|
||||
share.setShareType(KnowledgeShareType.URL.name());
|
||||
share.setShareKeyHash(hashShareKey(shareKey));
|
||||
share.setStatus(KnowledgeShareStatus.ENABLED.name());
|
||||
share.setPermissionScopes(permissionScopes);
|
||||
share.setExpiresAt(expiresAt);
|
||||
share.setTenantId(tenantId);
|
||||
share.setDeptId(deptId);
|
||||
share.setCreated(now);
|
||||
share.setCreatedBy(operatorId);
|
||||
share.setModified(now);
|
||||
share.setModifiedBy(operatorId);
|
||||
save(share);
|
||||
|
||||
KnowledgeShareUrlCreateResult result = new KnowledgeShareUrlCreateResult();
|
||||
result.setId(share.getId());
|
||||
result.setShareKey(shareKey);
|
||||
result.setExpiresAt(expiresAt);
|
||||
result.setShareUrl(buildShareUrl(baseUrl, shareKey));
|
||||
return result;
|
||||
}
|
||||
|
||||
private void invalidateExistingUrlShares(BigInteger knowledgeId, BigInteger operatorId) {
|
||||
QueryWrapper wrapper = QueryWrapper.create()
|
||||
.eq(KnowledgeShare::getKnowledgeId, knowledgeId)
|
||||
.eq(KnowledgeShare::getShareType, KnowledgeShareType.URL.name())
|
||||
.eq(KnowledgeShare::getStatus, KnowledgeShareStatus.ENABLED.name());
|
||||
List<KnowledgeShare> activeShares = list(wrapper);
|
||||
if (activeShares == null || activeShares.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Date now = new Date();
|
||||
for (KnowledgeShare activeShare : activeShares) {
|
||||
KnowledgeShare update = new KnowledgeShare();
|
||||
update.setId(activeShare.getId());
|
||||
update.setStatus(KnowledgeShareStatus.DISABLED.name());
|
||||
update.setModified(now);
|
||||
update.setModifiedBy(operatorId);
|
||||
updateById(update);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public KnowledgeShareAuthContext validateUrlShare(String shareKey) {
|
||||
if (shareKey == null || shareKey.isBlank()) {
|
||||
throw invalidShare();
|
||||
}
|
||||
QueryWrapper wrapper = QueryWrapper.create()
|
||||
.eq(KnowledgeShare::getShareKeyHash, hashShareKey(shareKey));
|
||||
KnowledgeShare share = getOne(wrapper);
|
||||
if (share == null) {
|
||||
throw invalidShare();
|
||||
}
|
||||
if (KnowledgeShareStatus.REVOKED.name().equals(share.getStatus())
|
||||
|| KnowledgeShareStatus.DISABLED.name().equals(share.getStatus())) {
|
||||
throw invalidShare();
|
||||
}
|
||||
if (share.getExpiresAt() == null || share.getExpiresAt().before(new Date())) {
|
||||
throw expiredShare();
|
||||
}
|
||||
DocumentCollection knowledge = documentCollectionService.getById(share.getKnowledgeId());
|
||||
if (knowledge == null) {
|
||||
throw invalidShare();
|
||||
}
|
||||
KnowledgeShareAuthContext context = new KnowledgeShareAuthContext();
|
||||
context.setShare(share);
|
||||
context.setKnowledge(knowledge);
|
||||
return context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KnowledgeShareAuthContext assertUrlShareAccess(
|
||||
String shareKey,
|
||||
BigInteger knowledgeId,
|
||||
String actionScope
|
||||
) {
|
||||
KnowledgeShareAuthContext context = validateUrlShare(shareKey);
|
||||
if (knowledgeId != null && context.getKnowledge() != null
|
||||
&& context.getKnowledge().getId() != null
|
||||
&& context.getKnowledge().getId().compareTo(knowledgeId) != 0) {
|
||||
throw invalidShare();
|
||||
}
|
||||
if (actionScope != null && !actionScope.isBlank()
|
||||
&& !context.getShare().getPermissionScopeSet().contains(actionScope.trim().toUpperCase())) {
|
||||
throw forbiddenShare("当前分享不允许执行该操作");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
private String buildShareUrl(String baseUrl, String shareKey) {
|
||||
if (baseUrl == null || baseUrl.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
return baseUrl + (baseUrl.contains("?") ? "&" : "?")
|
||||
+ "shareKey=" + shareKey;
|
||||
}
|
||||
|
||||
private String hashShareKey(String shareKey) {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
byte[] value = digest.digest(shareKey.getBytes(StandardCharsets.UTF_8));
|
||||
return HexFormat.of().formatHex(value);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IllegalStateException("SHA-256 unavailable", e);
|
||||
}
|
||||
}
|
||||
|
||||
private BusinessException expiredShare() {
|
||||
return new BusinessException(KnowledgeShareErrorCode.KNOWLEDGE_SHARE_EXPIRED + ":链接已过期");
|
||||
}
|
||||
|
||||
private BusinessException invalidShare() {
|
||||
return new BusinessException(KnowledgeShareErrorCode.KNOWLEDGE_SHARE_INVALID + ":链接无效");
|
||||
}
|
||||
|
||||
private BusinessException forbiddenShare(String message) {
|
||||
return new BusinessException(KnowledgeShareErrorCode.KNOWLEDGE_SHARE_FORBIDDEN + ":" + message);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user