Compare commits
7 Commits
6da90e2296
...
2689adfa40
| Author | SHA1 | Date | |
|---|---|---|---|
| 2689adfa40 | |||
| a41b50959e | |||
| 855e93ecbf | |||
| ae10383f17 | |||
| 31a755a8bc | |||
| 8cfe5400fe | |||
| 47655a728b |
@@ -36,5 +36,17 @@
|
|||||||
<groupId>tech.easyflow</groupId>
|
<groupId>tech.easyflow</groupId>
|
||||||
<artifactId>easyflow-common-captcha</artifactId>
|
<artifactId>easyflow-common-captcha</artifactId>
|
||||||
</dependency>
|
</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>
|
</dependencies>
|
||||||
</project>
|
</project>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import org.springframework.util.StringUtils;
|
|||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
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.easyagents.listener.PromptChoreChatStreamListener;
|
||||||
import tech.easyflow.ai.entity.*;
|
import tech.easyflow.ai.entity.*;
|
||||||
import tech.easyflow.ai.publish.BotPublishAppService;
|
import tech.easyflow.ai.publish.BotPublishAppService;
|
||||||
@@ -73,6 +74,8 @@ public class BotController extends BaseCurdController<BotService, Bot> {
|
|||||||
private BotPublishAppService botPublishAppService;
|
private BotPublishAppService botPublishAppService;
|
||||||
@Resource
|
@Resource
|
||||||
private AiResourceApprovalStateService aiResourceApprovalStateService;
|
private AiResourceApprovalStateService aiResourceApprovalStateService;
|
||||||
|
@Resource
|
||||||
|
private AiResourceCreatorNameSupport aiResourceCreatorNameSupport;
|
||||||
|
|
||||||
public BotController(BotService service, ModelService modelService, BotWorkflowService botWorkflowService,
|
public BotController(BotService service, ModelService modelService, BotWorkflowService botWorkflowService,
|
||||||
BotDocumentCollectionService botDocumentCollectionService, BotMessageService botMessageService) {
|
BotDocumentCollectionService botDocumentCollectionService, BotMessageService botMessageService) {
|
||||||
@@ -305,6 +308,7 @@ public class BotController extends BaseCurdController<BotService, Bot> {
|
|||||||
applyCategoryPermission(queryWrapper);
|
applyCategoryPermission(queryWrapper);
|
||||||
Page<Bot> result = super.queryPage(page, queryWrapper);
|
Page<Bot> result = super.queryPage(page, queryWrapper);
|
||||||
aiResourceApprovalStateService.fillBotApprovalState(result.getRecords());
|
aiResourceApprovalStateService.fillBotApprovalState(result.getRecords());
|
||||||
|
aiResourceCreatorNameSupport.fillBotCreatorNames(result.getRecords());
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import org.springframework.web.bind.annotation.PostMapping;
|
|||||||
import tech.easyflow.ai.entity.Plugin;
|
import tech.easyflow.ai.entity.Plugin;
|
||||||
import tech.easyflow.ai.entity.BotPlugin;
|
import tech.easyflow.ai.entity.BotPlugin;
|
||||||
import tech.easyflow.ai.entity.PluginItem;
|
import tech.easyflow.ai.entity.PluginItem;
|
||||||
|
import tech.easyflow.ai.enums.PluginType;
|
||||||
import tech.easyflow.common.annotation.UsePermission;
|
import tech.easyflow.common.annotation.UsePermission;
|
||||||
import tech.easyflow.common.domain.Result;
|
import tech.easyflow.common.domain.Result;
|
||||||
import tech.easyflow.common.tree.Tree;
|
import tech.easyflow.common.tree.Tree;
|
||||||
@@ -58,7 +59,14 @@ public class BotPluginController extends BaseCurdController<BotPluginService, Bo
|
|||||||
List<BotPlugin> visibleList = new ArrayList<>();
|
List<BotPlugin> visibleList = new ArrayList<>();
|
||||||
for (BotPlugin relation : botPlugins) {
|
for (BotPlugin relation : botPlugins) {
|
||||||
Plugin plugin = relation.getAiPlugin();
|
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);
|
visibleList.add(relation);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -73,7 +81,13 @@ public class BotPluginController extends BaseCurdController<BotPluginService, Bo
|
|||||||
List<Plugin> plugins = botPluginService.getList(botId);
|
List<Plugin> plugins = botPluginService.getList(botId);
|
||||||
List<Plugin> visibleList = new ArrayList<>();
|
List<Plugin> visibleList = new ArrayList<>();
|
||||||
for (Plugin plugin : plugins) {
|
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);
|
visibleList.add(plugin);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -105,6 +119,9 @@ public class BotPluginController extends BaseCurdController<BotPluginService, Bo
|
|||||||
if (pluginItem.getPluginId() != null) {
|
if (pluginItem.getPluginId() != null) {
|
||||||
Plugin plugin = pluginService.getById(pluginItem.getPluginId());
|
Plugin plugin = pluginService.getById(pluginItem.getPluginId());
|
||||||
if (plugin != null) {
|
if (plugin != null) {
|
||||||
|
if (PluginType.isWorkflow(plugin.getType())) {
|
||||||
|
throw new tech.easyflow.common.web.exceptions.BusinessException("当前版本暂不支持聊天助手绑定工作流插件");
|
||||||
|
}
|
||||||
pluginVisibilityService.assertPluginVisible(plugin.getCreatedBy(), plugin.getId(), "无权限绑定插件");
|
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.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
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.permission.KnowledgeVisibilityQueryHelper;
|
||||||
import tech.easyflow.ai.documentimport.DocumentImportDtos;
|
import tech.easyflow.ai.documentimport.DocumentImportDtos;
|
||||||
import tech.easyflow.ai.dto.KnowledgeSearchResultItem;
|
import tech.easyflow.ai.dto.KnowledgeSearchResultItem;
|
||||||
@@ -76,6 +77,8 @@ public class DocumentCollectionController extends BaseCurdController<DocumentCol
|
|||||||
private KnowledgePublishAppService knowledgePublishAppService;
|
private KnowledgePublishAppService knowledgePublishAppService;
|
||||||
@Resource
|
@Resource
|
||||||
private AiResourceApprovalStateService aiResourceApprovalStateService;
|
private AiResourceApprovalStateService aiResourceApprovalStateService;
|
||||||
|
@Resource
|
||||||
|
private AiResourceCreatorNameSupport aiResourceCreatorNameSupport;
|
||||||
|
|
||||||
public DocumentCollectionController(DocumentCollectionService service, DocumentChunkService chunkService, ModelService llmService) {
|
public DocumentCollectionController(DocumentCollectionService service, DocumentChunkService chunkService, ModelService llmService) {
|
||||||
super(service);
|
super(service);
|
||||||
@@ -143,10 +146,14 @@ public class DocumentCollectionController extends BaseCurdController<DocumentCol
|
|||||||
)
|
)
|
||||||
public Result<List<KnowledgeSearchResultItem>> search(@RequestParam BigInteger knowledgeId,
|
public Result<List<KnowledgeSearchResultItem>> search(@RequestParam BigInteger knowledgeId,
|
||||||
@RequestParam String keyword,
|
@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();
|
KnowledgeRetrievalRequest request = new KnowledgeRetrievalRequest();
|
||||||
request.setKnowledgeId(knowledgeId);
|
request.setKnowledgeId(knowledgeId);
|
||||||
request.setQuery(keyword);
|
request.setQuery(keyword);
|
||||||
|
request.setLimit(docRecallMaxNum);
|
||||||
|
request.setMinSimilarity(simThreshold);
|
||||||
request.setRetrievalMode(KnowledgeRetrievalModes.parse(retrievalMode));
|
request.setRetrievalMode(KnowledgeRetrievalModes.parse(retrievalMode));
|
||||||
request.setCallerType("API");
|
request.setCallerType("API");
|
||||||
request.setCallerId(String.valueOf(knowledgeId));
|
request.setCallerId(String.valueOf(knowledgeId));
|
||||||
@@ -310,6 +317,7 @@ public class DocumentCollectionController extends BaseCurdController<DocumentCol
|
|||||||
applyPublishedOnlyFilter(queryWrapper);
|
applyPublishedOnlyFilter(queryWrapper);
|
||||||
Page<DocumentCollection> result = super.queryPage(page, queryWrapper);
|
Page<DocumentCollection> result = super.queryPage(page, queryWrapper);
|
||||||
aiResourceApprovalStateService.fillKnowledgeApprovalState(result.getRecords());
|
aiResourceApprovalStateService.fillKnowledgeApprovalState(result.getRecords());
|
||||||
|
aiResourceCreatorNameSupport.fillDocumentCollectionCreatorNames(result.getRecords());
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,12 @@ import jakarta.servlet.http.HttpServletResponse;
|
|||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.core.io.ClassPathResource;
|
import org.springframework.core.io.ClassPathResource;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.web.bind.annotation.*;
|
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.DocumentImportDtos;
|
||||||
|
import tech.easyflow.ai.documentimport.task.DocumentImportTaskStatusStreamService;
|
||||||
import tech.easyflow.ai.entity.Document;
|
import tech.easyflow.ai.entity.Document;
|
||||||
import tech.easyflow.ai.entity.DocumentCollection;
|
import tech.easyflow.ai.entity.DocumentCollection;
|
||||||
import tech.easyflow.ai.entity.DocumentCollectionSplitParams;
|
import tech.easyflow.ai.entity.DocumentCollectionSplitParams;
|
||||||
@@ -77,6 +80,9 @@ public class DocumentController extends BaseCurdController<DocumentService, Docu
|
|||||||
@Autowired
|
@Autowired
|
||||||
private ResourceAccessService resourceAccessService;
|
private ResourceAccessService resourceAccessService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private DocumentImportTaskStatusStreamService documentImportTaskStatusStreamService;
|
||||||
|
|
||||||
@Value("${easyflow.storage.local.root:}")
|
@Value("${easyflow.storage.local.root:}")
|
||||||
private String fileUploadPath;
|
private String fileUploadPath;
|
||||||
|
|
||||||
@@ -233,6 +239,79 @@ public class DocumentController extends BaseCurdController<DocumentService, Docu
|
|||||||
return documentService.commitImport(request);
|
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
|
* 更新 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;
|
package tech.easyflow.admin.controller.ai;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import tech.easyflow.ai.entity.PluginCategory;
|
import tech.easyflow.ai.entity.PluginCategory;
|
||||||
import tech.easyflow.ai.entity.PluginCategoryMapping;
|
import tech.easyflow.ai.entity.PluginCategoryMapping;
|
||||||
import tech.easyflow.ai.service.PluginCategoryMappingService;
|
import tech.easyflow.ai.service.PluginCategoryMappingService;
|
||||||
|
import tech.easyflow.common.annotation.UsePermission;
|
||||||
import tech.easyflow.common.domain.Result;
|
import tech.easyflow.common.domain.Result;
|
||||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||||
@@ -21,6 +23,7 @@ import java.util.List;
|
|||||||
*/
|
*/
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/pluginCategoryMapping")
|
@RequestMapping("/api/v1/pluginCategoryMapping")
|
||||||
|
@UsePermission(moduleName = "/api/v1/plugin")
|
||||||
public class PluginCategoryMappingController extends BaseCurdController<PluginCategoryMappingService, PluginCategoryMapping> {
|
public class PluginCategoryMappingController extends BaseCurdController<PluginCategoryMappingService, PluginCategoryMapping> {
|
||||||
public PluginCategoryMappingController(PluginCategoryMappingService service) {
|
public PluginCategoryMappingController(PluginCategoryMappingService service) {
|
||||||
super(service);
|
super(service);
|
||||||
@@ -30,6 +33,7 @@ public class PluginCategoryMappingController extends BaseCurdController<PluginCa
|
|||||||
private PluginCategoryMappingService relationService;
|
private PluginCategoryMappingService relationService;
|
||||||
|
|
||||||
@PostMapping("/updateRelation")
|
@PostMapping("/updateRelation")
|
||||||
|
@SaCheckPermission("/api/v1/plugin/save")
|
||||||
public Result<Boolean> updateRelation(
|
public Result<Boolean> updateRelation(
|
||||||
@JsonBody(value="pluginId") BigInteger pluginId,
|
@JsonBody(value="pluginId") BigInteger pluginId,
|
||||||
@JsonBody(value="categoryIds") ArrayList<BigInteger> categoryIds
|
@JsonBody(value="categoryIds") ArrayList<BigInteger> categoryIds
|
||||||
@@ -42,4 +46,4 @@ public class PluginCategoryMappingController extends BaseCurdController<PluginCa
|
|||||||
){
|
){
|
||||||
return Result.ok(relationService.getPluginCategories(pluginId));
|
return Result.ok(relationService.getPluginCategories(pluginId));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,21 +6,33 @@ import com.mybatisflex.core.query.QueryWrapper;
|
|||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
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.Model;
|
||||||
import tech.easyflow.ai.entity.Plugin;
|
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.ModelService;
|
||||||
import tech.easyflow.ai.service.PluginVisibilityService;
|
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.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.common.web.controller.BaseCurdController;
|
||||||
import tech.easyflow.ai.service.PluginService;
|
import tech.easyflow.ai.service.PluginService;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
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.entity.vo.RoleCategoryAccessSnapshot;
|
||||||
import tech.easyflow.system.service.CategoryPermissionService;
|
import tech.easyflow.system.service.CategoryPermissionService;
|
||||||
|
import tech.easyflow.system.service.ResourceAccessService;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
@@ -47,6 +59,16 @@ public class PluginController extends BaseCurdController<PluginService, Plugin>
|
|||||||
private PluginVisibilityService pluginVisibilityService;
|
private PluginVisibilityService pluginVisibilityService;
|
||||||
@Resource
|
@Resource
|
||||||
private ModelService modelService;
|
private ModelService modelService;
|
||||||
|
@Resource
|
||||||
|
private WorkflowVisibilityQueryHelper workflowVisibilityQueryHelper;
|
||||||
|
@Resource
|
||||||
|
private WorkflowService workflowService;
|
||||||
|
@Resource
|
||||||
|
private ResourceAccessService resourceAccessService;
|
||||||
|
@Resource
|
||||||
|
private WorkflowPluginSnapshotResolver workflowPluginSnapshotResolver;
|
||||||
|
@Resource
|
||||||
|
private AiResourceCreatorNameSupport aiResourceCreatorNameSupport;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Result<?> onSaveOrUpdateBefore(Plugin entity, boolean isSave) {
|
protected Result<?> onSaveOrUpdateBefore(Plugin entity, boolean isSave) {
|
||||||
@@ -79,7 +101,8 @@ public class PluginController extends BaseCurdController<PluginService, Plugin>
|
|||||||
public Result<List<Plugin>> getList(){
|
public Result<List<Plugin>> getList(){
|
||||||
QueryWrapper queryWrapper = QueryWrapper.create().select();
|
QueryWrapper queryWrapper = QueryWrapper.create().select();
|
||||||
applyCategoryPermission(queryWrapper);
|
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")
|
@GetMapping("/pageByCategory")
|
||||||
@@ -97,10 +120,31 @@ public class PluginController extends BaseCurdController<PluginService, Plugin>
|
|||||||
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||||
return Result.ok(queryPage(new Page<>(pageNumber, pageSize), queryWrapper));
|
return Result.ok(queryPage(new Page<>(pageNumber, pageSize), queryWrapper));
|
||||||
} else {
|
} 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")
|
@GetMapping("/modelList")
|
||||||
@SaCheckPermission("/api/v1/plugin/query")
|
@SaCheckPermission("/api/v1/plugin/query")
|
||||||
public Result<List<Model>> modelList(Model entity, Boolean asTree, String sortKey, String sortType) {
|
public Result<List<Model>> modelList(Model entity, Boolean asTree, String sortKey, String sortType) {
|
||||||
@@ -110,14 +154,25 @@ public class PluginController extends BaseCurdController<PluginService, Plugin>
|
|||||||
@Override
|
@Override
|
||||||
protected Page<Plugin> queryPage(Page<Plugin> page, QueryWrapper queryWrapper) {
|
protected Page<Plugin> queryPage(Page<Plugin> page, QueryWrapper queryWrapper) {
|
||||||
applyCategoryPermission(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
|
@Override
|
||||||
public Result<Plugin> detail(String id) {
|
public Result<Plugin> detail(String id) {
|
||||||
Plugin plugin = service.getById(id);
|
Plugin plugin = service.getMapper().selectOneWithRelationsById(id);
|
||||||
if (plugin != null) {
|
if (plugin != null) {
|
||||||
pluginVisibilityService.assertPluginVisible(plugin.getCreatedBy(), plugin.getId(), "无权限访问插件");
|
pluginVisibilityService.assertPluginVisible(plugin.getCreatedBy(), plugin.getId(), "无权限访问插件");
|
||||||
|
pluginService.preparePluginForCurrentUser(plugin);
|
||||||
}
|
}
|
||||||
return Result.ok(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)));
|
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;
|
package tech.easyflow.admin.controller.ai;
|
||||||
|
|
||||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
|
import cn.dev33.satoken.stp.StpUtil;
|
||||||
import cn.hutool.core.util.IdUtil;
|
import cn.hutool.core.util.IdUtil;
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import com.alibaba.fastjson2.JSON;
|
import com.alibaba.fastjson2.JSON;
|
||||||
import com.alibaba.fastjson2.JSONArray;
|
import com.alibaba.fastjson2.JSONArray;
|
||||||
import com.alibaba.fastjson2.JSONObject;
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
import com.easyagents.flow.core.chain.runtime.ChainExecutor;
|
||||||
import com.mybatisflex.core.query.QueryWrapper;
|
import com.mybatisflex.core.query.QueryWrapper;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
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.BotPlugin;
|
||||||
|
import tech.easyflow.ai.entity.Plugin;
|
||||||
import tech.easyflow.ai.entity.PluginItem;
|
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.BotPluginService;
|
||||||
|
import tech.easyflow.ai.service.PluginService;
|
||||||
import tech.easyflow.ai.service.PluginItemService;
|
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.annotation.UsePermission;
|
||||||
import tech.easyflow.common.domain.Result;
|
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.controller.BaseCurdController;
|
||||||
|
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 控制层。
|
* 控制层。
|
||||||
@@ -45,6 +64,18 @@ public class PluginItemController extends BaseCurdController<PluginItemService,
|
|||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private BotPluginService botPluginService;
|
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")
|
@PostMapping("/tool/save")
|
||||||
@SaCheckPermission("/api/v1/plugin/save")
|
@SaCheckPermission("/api/v1/plugin/save")
|
||||||
@@ -87,8 +118,18 @@ public class PluginItemController extends BaseCurdController<PluginItemService,
|
|||||||
if (record == null) {
|
if (record == null) {
|
||||||
return Result.ok(nodeData);
|
return Result.ok(nodeData);
|
||||||
}
|
}
|
||||||
|
Plugin plugin = pluginService.getById(record.getPluginId());
|
||||||
nodeData.put("pluginId", record.getId().toString());
|
nodeData.put("pluginId", record.getId().toString());
|
||||||
nodeData.put("pluginName", record.getName());
|
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 parameters = new JSONArray();
|
||||||
JSONArray outputDefs = new JSONArray();
|
JSONArray outputDefs = new JSONArray();
|
||||||
@@ -104,6 +145,7 @@ public class PluginItemController extends BaseCurdController<PluginItemService,
|
|||||||
handleArray(array);
|
handleArray(array);
|
||||||
outputDefs = array;
|
outputDefs = array;
|
||||||
}
|
}
|
||||||
|
nodeData.put("schemaHash", resolveSchemaHash(record, plugin));
|
||||||
nodeData.put("parameters", parameters);
|
nodeData.put("parameters", parameters);
|
||||||
nodeData.put("outputDefs", outputDefs);
|
nodeData.put("outputDefs", outputDefs);
|
||||||
return Result.ok(nodeData);
|
return Result.ok(nodeData);
|
||||||
@@ -119,6 +161,71 @@ public class PluginItemController extends BaseCurdController<PluginItemService,
|
|||||||
return pluginItemService.pluginToolTest(inputData, pluginToolId);
|
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) {
|
private void handleArray(JSONArray array) {
|
||||||
for (Object o : array) {
|
for (Object o : array) {
|
||||||
JSONObject obj = (JSONObject) o;
|
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
|
@Override
|
||||||
protected Result<?> onRemoveBefore(Collection<Serializable> ids) {
|
protected Result<?> onRemoveBefore(Collection<Serializable> ids) {
|
||||||
|
|
||||||
@@ -144,6 +285,15 @@ public class PluginItemController extends BaseCurdController<PluginItemService,
|
|||||||
if (exists){
|
if (exists){
|
||||||
return Result.fail(1, "此工具还关联着bot,请先取消关联!");
|
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;
|
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.context.request.ServletRequestAttributes;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
import tech.easyflow.admin.controller.ai.support.AiResourceCreatorNameSupport;
|
||||||
import tech.easyflow.ai.permission.WorkflowVisibilityQueryHelper;
|
import tech.easyflow.ai.permission.WorkflowVisibilityQueryHelper;
|
||||||
import tech.easyflow.ai.easyagentsflow.entity.ChainInfo;
|
import tech.easyflow.ai.easyagentsflow.entity.ChainInfo;
|
||||||
import tech.easyflow.ai.easyagentsflow.entity.NodeInfo;
|
import tech.easyflow.ai.easyagentsflow.entity.NodeInfo;
|
||||||
@@ -93,6 +94,8 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
|||||||
private WorkflowPublishAppService workflowPublishAppService;
|
private WorkflowPublishAppService workflowPublishAppService;
|
||||||
@Resource
|
@Resource
|
||||||
private AiResourceApprovalStateService aiResourceApprovalStateService;
|
private AiResourceApprovalStateService aiResourceApprovalStateService;
|
||||||
|
@Resource
|
||||||
|
private AiResourceCreatorNameSupport aiResourceCreatorNameSupport;
|
||||||
|
|
||||||
public WorkflowController(WorkflowService service, ModelService modelService) {
|
public WorkflowController(WorkflowService service, ModelService modelService) {
|
||||||
super(service);
|
super(service);
|
||||||
@@ -120,6 +123,12 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
|||||||
if (workflow == null) {
|
if (workflow == null) {
|
||||||
return Result.fail(1, "工作流不存在");
|
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);
|
Map<String, Object> res = chainExecutor.executeNode(workflowId.toString(), nodeId, variables);
|
||||||
return Result.ok(res);
|
return Result.ok(res);
|
||||||
}
|
}
|
||||||
@@ -428,6 +437,7 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
|||||||
applyPublishedOnlyFilter(queryWrapper);
|
applyPublishedOnlyFilter(queryWrapper);
|
||||||
Page<Workflow> result = super.queryPage(page, queryWrapper);
|
Page<Workflow> result = super.queryPage(page, queryWrapper);
|
||||||
aiResourceApprovalStateService.fillWorkflowApprovalState(result.getRecords());
|
aiResourceApprovalStateService.fillWorkflowApprovalState(result.getRecords());
|
||||||
|
aiResourceCreatorNameSupport.fillWorkflowCreatorNames(result.getRecords());
|
||||||
return result;
|
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.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import tech.easyflow.ai.service.KnowledgeSharePermissionService;
|
||||||
import tech.easyflow.common.domain.Result;
|
import tech.easyflow.common.domain.Result;
|
||||||
import tech.easyflow.common.entity.LoginAccount;
|
import tech.easyflow.common.entity.LoginAccount;
|
||||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||||
@@ -43,6 +44,8 @@ public class SysApiKeyController extends BaseCurdController<SysApiKeyService, Sy
|
|||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private SysApiKeyResourceMappingService sysApiKeyResourceMappingService;
|
private SysApiKeyResourceMappingService sysApiKeyResourceMappingService;
|
||||||
|
@Resource
|
||||||
|
private KnowledgeSharePermissionService knowledgeSharePermissionService;
|
||||||
/**
|
/**
|
||||||
* 添加(保存)数据
|
* 添加(保存)数据
|
||||||
*
|
*
|
||||||
@@ -79,10 +82,20 @@ public class SysApiKeyController extends BaseCurdController<SysApiKeyService, Sy
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onSaveOrUpdateAfter(SysApiKey entity, boolean isSave) {
|
protected void onSaveOrUpdateAfter(SysApiKey entity, boolean isSave) {
|
||||||
if (!isSave && entity.getPermissionIds() != null && !entity.getPermissionIds().isEmpty()) {
|
if (entity.getPermissionIds() != null) {
|
||||||
// 修改的时候绑定授权接口
|
|
||||||
sysApiKeyResourceMappingService.authInterface(entity);
|
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
|
@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);
|
Result<Page<SysApiKey>> pageResult = (Result<Page<SysApiKey>>) super.page(request, sortKey, sortType, pageNumber, pageSize);
|
||||||
Page<SysApiKey> data = pageResult.getData();
|
Page<SysApiKey> data = pageResult.getData();
|
||||||
List<SysApiKey> records = data.getRecords();
|
List<SysApiKey> records = data.getRecords();
|
||||||
records.forEach(record -> {
|
records.forEach(this::fillApiKeyPermissions);
|
||||||
QueryWrapper queryWrapper = QueryWrapper.create().select(SysApiKeyResourceMapping::getApiKeyResourceId).eq(SysApiKeyResourceMapping::getApiKeyId, record.getId());
|
|
||||||
List<BigInteger> resourceIds = sysApiKeyResourceMappingService.listAs(queryWrapper, BigInteger.class);
|
|
||||||
record.setPermissionIds(resourceIds);
|
|
||||||
});
|
|
||||||
return pageResult;
|
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;
|
package tech.easyflow.publicapi.controller;
|
||||||
|
|
||||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
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.ChainDefinition;
|
||||||
import com.easyagents.flow.core.chain.Parameter;
|
import com.easyagents.flow.core.chain.Parameter;
|
||||||
import com.easyagents.flow.core.chain.runtime.ChainExecutor;
|
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.easyagentsflow.service.WorkflowDatacenterContentService;
|
||||||
import tech.easyflow.ai.entity.Workflow;
|
import tech.easyflow.ai.entity.Workflow;
|
||||||
import tech.easyflow.ai.service.WorkflowService;
|
import tech.easyflow.ai.service.WorkflowService;
|
||||||
|
import tech.easyflow.common.constant.Constants;
|
||||||
import tech.easyflow.common.domain.Result;
|
import tech.easyflow.common.domain.Result;
|
||||||
|
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||||
|
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
@@ -75,6 +78,12 @@ public class PublicWorkflowController {
|
|||||||
if (workflow == null) {
|
if (workflow == null) {
|
||||||
return Result.fail(1, "工作流不存在");
|
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);
|
Map<String, Object> res = chainExecutor.executeNode(workflowId.toString(), nodeId, variables);
|
||||||
return Result.ok(res);
|
return Result.ok(res);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,6 +83,12 @@ public class UcWorkflowController extends BaseCurdController<WorkflowService, Wo
|
|||||||
if (workflow == null) {
|
if (workflow == null) {
|
||||||
return Result.fail(1, "工作流不存在");
|
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);
|
Map<String, Object> res = chainExecutor.executeNode(workflowId.toString(), nodeId, variables);
|
||||||
return Result.ok(res);
|
return Result.ok(res);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package tech.easyflow.common.mq.redis;
|
package tech.easyflow.common.mq.redis;
|
||||||
|
|
||||||
import jakarta.annotation.PreDestroy;
|
import jakarta.annotation.PreDestroy;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.context.SmartLifecycle;
|
import org.springframework.context.SmartLifecycle;
|
||||||
import org.springframework.data.domain.Range;
|
import org.springframework.data.domain.Range;
|
||||||
import org.springframework.data.redis.connection.RedisConnection;
|
import org.springframework.data.redis.connection.RedisConnection;
|
||||||
@@ -34,6 +36,8 @@ import java.util.concurrent.TimeUnit;
|
|||||||
|
|
||||||
public class RedisMQConsumerContainer implements MQConsumerContainer, SmartLifecycle {
|
public class RedisMQConsumerContainer implements MQConsumerContainer, SmartLifecycle {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(RedisMQConsumerContainer.class);
|
||||||
|
|
||||||
private final RedisConnectionFactory redisConnectionFactory;
|
private final RedisConnectionFactory redisConnectionFactory;
|
||||||
private final StringRedisTemplate stringRedisTemplate;
|
private final StringRedisTemplate stringRedisTemplate;
|
||||||
private final MQProperties properties;
|
private final MQProperties properties;
|
||||||
@@ -71,6 +75,8 @@ public class RedisMQConsumerContainer implements MQConsumerContainer, SmartLifec
|
|||||||
MQSubscription subscription = handler.subscription();
|
MQSubscription subscription = handler.subscription();
|
||||||
for (int shard = 0; shard < Math.max(subscription.getShardCount(), 1); shard++) {
|
for (int shard = 0; shard < Math.max(subscription.getShardCount(), 1); shard++) {
|
||||||
int currentShard = 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));
|
executorService.submit(() -> consumeLoop(handler, subscription, currentShard));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -106,6 +112,8 @@ public class RedisMQConsumerContainer implements MQConsumerContainer, SmartLifec
|
|||||||
String streamKey = keySupport.streamKey(subscription.getTopic(), shard);
|
String streamKey = keySupport.streamKey(subscription.getTopic(), shard);
|
||||||
String consumerName = subscription.getConsumerGroup() + "-" + shard;
|
String consumerName = subscription.getConsumerGroup() + "-" + shard;
|
||||||
ensureConsumerGroup(streamKey, subscription.getConsumerGroup());
|
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) {
|
while (running) {
|
||||||
try {
|
try {
|
||||||
reclaimPending(streamKey, subscription.getConsumerGroup(), consumerName);
|
reclaimPending(streamKey, subscription.getConsumerGroup(), consumerName);
|
||||||
@@ -123,8 +131,18 @@ public class RedisMQConsumerContainer implements MQConsumerContainer, SmartLifec
|
|||||||
if (messages.isEmpty()) {
|
if (messages.isEmpty()) {
|
||||||
continue;
|
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);
|
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);
|
sleepSilently(1000L);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -192,8 +210,12 @@ public class RedisMQConsumerContainer implements MQConsumerContainer, SmartLifec
|
|||||||
message.setRetryCount(retryCount);
|
message.setRetryCount(retryCount);
|
||||||
message.getHeaders().put("lastError", reason == null ? "" : reason);
|
message.getHeaders().put("lastError", reason == null ? "" : reason);
|
||||||
if (retryCount > properties.getRedis().getMaxRetry()) {
|
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);
|
deadLetterService.deadLetter(message, reason);
|
||||||
} else {
|
} else {
|
||||||
|
LOG.warn("MQ 消息消费失败,准备重试: topic={}, messageId={}, streamKey={}, retryCount={}, reason={}",
|
||||||
|
message.getTopic(), message.getMessageId(), message.getStreamKey(), retryCount, reason);
|
||||||
stringRedisTemplate.opsForStream().add(
|
stringRedisTemplate.opsForStream().add(
|
||||||
org.springframework.data.redis.connection.stream.StreamRecords.string(
|
org.springframework.data.redis.connection.stream.StreamRecords.string(
|
||||||
Map.of("payload", messageConverter.serialize(message))
|
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 {
|
private void handleMessages(MQConsumerHandler handler, String streamKey, String group, List<MQMessage> messages) throws Exception {
|
||||||
try {
|
try {
|
||||||
|
LOG.info("MQ 开始批量处理消息: group={}, streamKey={}, count={}, handler={}",
|
||||||
|
group, streamKey, messages.size(), handler.getClass().getSimpleName());
|
||||||
handler.handle(messages);
|
handler.handle(messages);
|
||||||
acknowledge(streamKey, group, messages);
|
acknowledge(streamKey, group, messages);
|
||||||
|
LOG.info("MQ 批量处理消息完成: group={}, streamKey={}, count={}, handler={}",
|
||||||
|
group, streamKey, messages.size(), handler.getClass().getSimpleName());
|
||||||
return;
|
return;
|
||||||
} catch (Exception batchEx) {
|
} catch (Exception batchEx) {
|
||||||
|
LOG.error("MQ 批量处理消息失败,准备降级单条处理: group={}, streamKey={}, count={}, handler={}",
|
||||||
|
group, streamKey, messages.size(), handler.getClass().getSimpleName(), batchEx);
|
||||||
if (messages.size() == 1) {
|
if (messages.size() == 1) {
|
||||||
retryOrDeadLetter(messages, resolveReason(batchEx));
|
retryOrDeadLetter(messages, resolveReason(batchEx));
|
||||||
acknowledge(streamKey, group, messages);
|
acknowledge(streamKey, group, messages);
|
||||||
@@ -218,7 +246,11 @@ public class RedisMQConsumerContainer implements MQConsumerContainer, SmartLifec
|
|||||||
|
|
||||||
for (MQMessage message : messages) {
|
for (MQMessage message : messages) {
|
||||||
try {
|
try {
|
||||||
|
LOG.info("MQ 开始单条处理消息: group={}, streamKey={}, messageId={}, handler={}",
|
||||||
|
group, streamKey, message.getMessageId(), handler.getClass().getSimpleName());
|
||||||
handler.handle(List.of(message));
|
handler.handle(List.of(message));
|
||||||
|
LOG.info("MQ 单条处理消息完成: group={}, streamKey={}, messageId={}, handler={}",
|
||||||
|
group, streamKey, message.getMessageId(), handler.getClass().getSimpleName());
|
||||||
} catch (Exception singleEx) {
|
} catch (Exception singleEx) {
|
||||||
retryOrDeadLetter(List.of(message), resolveReason(singleEx));
|
retryOrDeadLetter(List.of(message), resolveReason(singleEx));
|
||||||
} finally {
|
} finally {
|
||||||
@@ -240,6 +272,7 @@ public class RedisMQConsumerContainer implements MQConsumerContainer, SmartLifec
|
|||||||
}
|
}
|
||||||
MQAcknowledger acknowledger = records -> stringRedisTemplate.opsForStream().acknowledge(streamKey, group, ids);
|
MQAcknowledger acknowledger = records -> stringRedisTemplate.opsForStream().acknowledge(streamKey, group, ids);
|
||||||
acknowledger.acknowledge(messages);
|
acknowledger.acknowledge(messages);
|
||||||
|
LOG.info("MQ 消息确认完成: group={}, streamKey={}, count={}", group, streamKey, ids.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String resolveReason(Exception exception) {
|
private String resolveReason(Exception exception) {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package tech.easyflow.common.mq.redis;
|
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.RecordId;
|
||||||
import org.springframework.data.redis.connection.stream.StreamRecords;
|
import org.springframework.data.redis.connection.stream.StreamRecords;
|
||||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||||
@@ -15,6 +17,8 @@ import java.util.UUID;
|
|||||||
|
|
||||||
public class RedisMQProducer implements MQProducer {
|
public class RedisMQProducer implements MQProducer {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(RedisMQProducer.class);
|
||||||
|
|
||||||
private final StringRedisTemplate stringRedisTemplate;
|
private final StringRedisTemplate stringRedisTemplate;
|
||||||
private final MQProperties properties;
|
private final MQProperties properties;
|
||||||
private final MQMessageConverter messageConverter;
|
private final MQMessageConverter messageConverter;
|
||||||
@@ -47,12 +51,16 @@ public class RedisMQProducer implements MQProducer {
|
|||||||
int shardCount = Math.max(properties.getRedis().getChatPersistShardCount(), 1);
|
int shardCount = Math.max(properties.getRedis().getChatPersistShardCount(), 1);
|
||||||
int shard = keySupport.resolveShard(message.getKey(), shardCount);
|
int shard = keySupport.resolveShard(message.getKey(), shardCount);
|
||||||
String streamKey = keySupport.streamKey(message.getTopic(), shard);
|
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(
|
RecordId recordId = stringRedisTemplate.opsForStream().add(
|
||||||
StreamRecords.string(Map.of("payload", messageConverter.serialize(message))).withStreamKey(streamKey)
|
StreamRecords.string(Map.of("payload", messageConverter.serialize(message))).withStreamKey(streamKey)
|
||||||
);
|
);
|
||||||
if (recordId == null) {
|
if (recordId == null) {
|
||||||
throw new MQException("MQ 消息投递失败");
|
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();
|
return recordId.getValue();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,14 @@ public class GlobalErrorResolver implements HandlerExceptionResolver {
|
|||||||
} else if (ex instanceof ConstraintViolationException) {
|
} else if (ex instanceof ConstraintViolationException) {
|
||||||
error = Result.fail(400, ex.getMessage());
|
error = Result.fail(400, ex.getMessage());
|
||||||
} else if (ex instanceof BusinessException) {
|
} 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 {
|
} else {
|
||||||
LOG.error(ex.toString(), ex);
|
LOG.error(ex.toString(), ex);
|
||||||
error = Result.fail(1, "错误信息:" + ex.getMessage());
|
error = Result.fail(1, "错误信息:" + ex.getMessage());
|
||||||
|
|||||||
@@ -41,6 +41,10 @@
|
|||||||
<groupId>com.easyagents</groupId>
|
<groupId>com.easyagents</groupId>
|
||||||
<artifactId>easy-agents-support</artifactId>
|
<artifactId>easy-agents-support</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.easyagents</groupId>
|
||||||
|
<artifactId>easy-agents-document-core</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.easyagents</groupId>
|
<groupId>com.easyagents</groupId>
|
||||||
<artifactId>easy-agents-rag-retrieval</artifactId>
|
<artifactId>easy-agents-rag-retrieval</artifactId>
|
||||||
@@ -99,6 +103,10 @@
|
|||||||
<groupId>tech.easyflow</groupId>
|
<groupId>tech.easyflow</groupId>
|
||||||
<artifactId>easyflow-common-chat-protocol</artifactId>
|
<artifactId>easyflow-common-chat-protocol</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>tech.easyflow</groupId>
|
||||||
|
<artifactId>easyflow-common-mq</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.easyagents</groupId>
|
<groupId>com.easyagents</groupId>
|
||||||
|
|||||||
@@ -12,8 +12,9 @@ public class ThreadPoolConfig {
|
|||||||
private static final Logger log = LoggerFactory.getLogger(ThreadPoolConfig.class);
|
private static final Logger log = LoggerFactory.getLogger(ThreadPoolConfig.class);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SSE消息发送专用线程池
|
* 创建 SSE 消息发送线程池。
|
||||||
* 核心原则:IO密集型任务(网络推送),线程数 = CPU核心数 * 2 + 1
|
*
|
||||||
|
* @return SSE 推送线程池
|
||||||
*/
|
*/
|
||||||
@Bean(name = "sseThreadPool")
|
@Bean(name = "sseThreadPool")
|
||||||
public ThreadPoolTaskExecutor sseThreadPool() {
|
public ThreadPoolTaskExecutor sseThreadPool() {
|
||||||
@@ -37,4 +38,29 @@ public class ThreadPoolConfig {
|
|||||||
executor.initialize();
|
executor.initialize();
|
||||||
return executor;
|
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 {
|
public static class PreviewRequest implements Serializable {
|
||||||
private BigInteger knowledgeId;
|
private BigInteger knowledgeId;
|
||||||
|
private BigInteger documentId;
|
||||||
private List<PreviewFileRequest> files = new ArrayList<PreviewFileRequest>();
|
private List<PreviewFileRequest> files = new ArrayList<PreviewFileRequest>();
|
||||||
|
|
||||||
public BigInteger getKnowledgeId() {
|
public BigInteger getKnowledgeId() {
|
||||||
@@ -103,6 +104,14 @@ public final class DocumentImportDtos {
|
|||||||
this.knowledgeId = knowledgeId;
|
this.knowledgeId = knowledgeId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public BigInteger getDocumentId() {
|
||||||
|
return documentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDocumentId(BigInteger documentId) {
|
||||||
|
this.documentId = documentId;
|
||||||
|
}
|
||||||
|
|
||||||
public List<PreviewFileRequest> getFiles() {
|
public List<PreviewFileRequest> getFiles() {
|
||||||
return files;
|
return files;
|
||||||
}
|
}
|
||||||
@@ -114,6 +123,7 @@ public final class DocumentImportDtos {
|
|||||||
|
|
||||||
public static class CommitRequest implements Serializable {
|
public static class CommitRequest implements Serializable {
|
||||||
private BigInteger knowledgeId;
|
private BigInteger knowledgeId;
|
||||||
|
private BigInteger documentId;
|
||||||
private List<String> previewSessionIds = new ArrayList<String>();
|
private List<String> previewSessionIds = new ArrayList<String>();
|
||||||
|
|
||||||
public BigInteger getKnowledgeId() {
|
public BigInteger getKnowledgeId() {
|
||||||
@@ -124,6 +134,14 @@ public final class DocumentImportDtos {
|
|||||||
this.knowledgeId = knowledgeId;
|
this.knowledgeId = knowledgeId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public BigInteger getDocumentId() {
|
||||||
|
return documentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDocumentId(BigInteger documentId) {
|
||||||
|
this.documentId = documentId;
|
||||||
|
}
|
||||||
|
|
||||||
public List<String> getPreviewSessionIds() {
|
public List<String> getPreviewSessionIds() {
|
||||||
return previewSessionIds;
|
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 {
|
public static class PreviewFileResult implements Serializable {
|
||||||
private String previewSessionId;
|
private String previewSessionId;
|
||||||
private String filePath;
|
private String filePath;
|
||||||
private String fileName;
|
private String fileName;
|
||||||
|
private String normalizedContent;
|
||||||
private String strategyCode;
|
private String strategyCode;
|
||||||
private String strategyLabel;
|
private String strategyLabel;
|
||||||
private AnalysisResult analysis;
|
private AnalysisResult analysis;
|
||||||
private Integer totalChunks;
|
private Integer totalChunks;
|
||||||
private Integer totalWarnings;
|
private Integer totalWarnings;
|
||||||
private List<RagChunk> chunks = new ArrayList<RagChunk>();
|
private List<PreviewChunkResult> chunks = new ArrayList<PreviewChunkResult>();
|
||||||
|
|
||||||
public String getPreviewSessionId() {
|
public String getPreviewSessionId() {
|
||||||
return previewSessionId;
|
return previewSessionId;
|
||||||
@@ -276,6 +436,14 @@ public final class DocumentImportDtos {
|
|||||||
this.fileName = fileName;
|
this.fileName = fileName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getNormalizedContent() {
|
||||||
|
return normalizedContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setNormalizedContent(String normalizedContent) {
|
||||||
|
this.normalizedContent = normalizedContent;
|
||||||
|
}
|
||||||
|
|
||||||
public String getStrategyCode() {
|
public String getStrategyCode() {
|
||||||
return strategyCode;
|
return strategyCode;
|
||||||
}
|
}
|
||||||
@@ -316,11 +484,11 @@ public final class DocumentImportDtos {
|
|||||||
this.totalWarnings = totalWarnings;
|
this.totalWarnings = totalWarnings;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<RagChunk> getChunks() {
|
public List<PreviewChunkResult> getChunks() {
|
||||||
return chunks;
|
return chunks;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setChunks(List<RagChunk> chunks) {
|
public void setChunks(List<PreviewChunkResult> chunks) {
|
||||||
this.chunks = chunks;
|
this.chunks = chunks;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -454,6 +622,7 @@ public final class DocumentImportDtos {
|
|||||||
public static class PreviewSession implements Serializable {
|
public static class PreviewSession implements Serializable {
|
||||||
private String sessionId;
|
private String sessionId;
|
||||||
private BigInteger knowledgeId;
|
private BigInteger knowledgeId;
|
||||||
|
private BigInteger documentId;
|
||||||
private String filePath;
|
private String filePath;
|
||||||
private String fileName;
|
private String fileName;
|
||||||
private String sourceFormat;
|
private String sourceFormat;
|
||||||
@@ -480,6 +649,14 @@ public final class DocumentImportDtos {
|
|||||||
this.knowledgeId = knowledgeId;
|
this.knowledgeId = knowledgeId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public BigInteger getDocumentId() {
|
||||||
|
return documentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDocumentId(BigInteger documentId) {
|
||||||
|
this.documentId = documentId;
|
||||||
|
}
|
||||||
|
|
||||||
public String getFilePath() {
|
public String getFilePath() {
|
||||||
return filePath;
|
return filePath;
|
||||||
}
|
}
|
||||||
@@ -552,4 +729,265 @@ public final class DocumentImportDtos {
|
|||||||
this.createdAt = createdAt;
|
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_ANALYSIS_SUMMARY = "splitter.analysisSummary";
|
||||||
public static final String KEY_DOCUMENT_SOURCE_FILE_EXT = "splitter.sourceFileExt";
|
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_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.slf4j.LoggerFactory;
|
||||||
import org.springframework.dao.DuplicateKeyException;
|
import org.springframework.dao.DuplicateKeyException;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
import tech.easyflow.ai.easyagentsflow.support.PublishedWorkflowDefinitionIds;
|
||||||
import tech.easyflow.ai.entity.Workflow;
|
import tech.easyflow.ai.entity.Workflow;
|
||||||
import tech.easyflow.ai.entity.WorkflowExecResult;
|
import tech.easyflow.ai.entity.WorkflowExecResult;
|
||||||
import tech.easyflow.ai.entity.WorkflowExecStep;
|
import tech.easyflow.ai.entity.WorkflowExecStep;
|
||||||
@@ -62,7 +63,11 @@ public class ChainEventListenerForSave implements ChainEventListener {
|
|||||||
log.info("ChainStartEvent: {}", event);
|
log.info("ChainStartEvent: {}", event);
|
||||||
ChainDefinition definition = chain.getDefinition();
|
ChainDefinition definition = chain.getDefinition();
|
||||||
ChainState state = chain.getState();
|
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();
|
String instanceId = state.getInstanceId();
|
||||||
WorkflowExecResult existed = workflowExecResultService.getByExecKey(instanceId);
|
WorkflowExecResult existed = workflowExecResultService.getByExecKey(instanceId);
|
||||||
if (existed != null) {
|
if (existed != null) {
|
||||||
@@ -176,4 +181,26 @@ public class ChainEventListenerForSave implements ChainEventListener {
|
|||||||
ChainState chainState = chain.getChainStateRepository().load(parentInstanceId);
|
ChainState chainState = chain.getChainStateRepository().load(parentInstanceId);
|
||||||
return findAncestorState(chainState, chain);
|
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.WorkflowCheckIssue;
|
||||||
import tech.easyflow.ai.easyagentsflow.entity.WorkflowCheckResult;
|
import tech.easyflow.ai.easyagentsflow.entity.WorkflowCheckResult;
|
||||||
import tech.easyflow.ai.easyagentsflow.entity.WorkflowCheckStage;
|
import tech.easyflow.ai.easyagentsflow.entity.WorkflowCheckStage;
|
||||||
|
import tech.easyflow.ai.entity.PluginItem;
|
||||||
import tech.easyflow.ai.entity.Workflow;
|
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.ai.service.WorkflowService;
|
||||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||||
import tech.easyflow.datacenter.execution.model.DatacenterSchemaResponse;
|
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_END = "endNode";
|
||||||
private static final String TYPE_LOOP = "loopNode";
|
private static final String TYPE_LOOP = "loopNode";
|
||||||
private static final String TYPE_WORKFLOW = "workflow-node";
|
private static final String TYPE_WORKFLOW = "workflow-node";
|
||||||
|
private static final String TYPE_PLUGIN = "plugin-node";
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private WorkflowService workflowService;
|
private WorkflowService workflowService;
|
||||||
@@ -47,6 +52,12 @@ public class WorkflowCheckService {
|
|||||||
private ChainParser chainParser;
|
private ChainParser chainParser;
|
||||||
@Resource
|
@Resource
|
||||||
private WorkflowDatacenterContentService workflowDatacenterContentService;
|
private WorkflowDatacenterContentService workflowDatacenterContentService;
|
||||||
|
@Resource
|
||||||
|
private WorkflowPluginDependencyService workflowPluginDependencyService;
|
||||||
|
@Resource
|
||||||
|
private PluginItemService pluginItemService;
|
||||||
|
@Resource
|
||||||
|
private WorkflowPluginSnapshotResolver workflowPluginSnapshotResolver;
|
||||||
|
|
||||||
public WorkflowCheckResult checkWorkflow(BigInteger workflowId, WorkflowCheckStage stage) {
|
public WorkflowCheckResult checkWorkflow(BigInteger workflowId, WorkflowCheckStage stage) {
|
||||||
if (workflowId == null) {
|
if (workflowId == null) {
|
||||||
@@ -66,6 +77,9 @@ public class WorkflowCheckService {
|
|||||||
List<WorkflowCheckIssue> issues = new ArrayList<>();
|
List<WorkflowCheckIssue> issues = new ArrayList<>();
|
||||||
Set<String> issueKeys = new LinkedHashSet<>();
|
Set<String> issueKeys = new LinkedHashSet<>();
|
||||||
ParsedWorkflow parsedWorkflow = parseAndCheckBase(content, issues, issueKeys);
|
ParsedWorkflow parsedWorkflow = parseAndCheckBase(content, issues, issueKeys);
|
||||||
|
if (parsedWorkflow != null) {
|
||||||
|
checkPluginSchemaHashes(parsedWorkflow, issues, issueKeys);
|
||||||
|
}
|
||||||
|
|
||||||
if (stage == WorkflowCheckStage.PRE_EXECUTE && parsedWorkflow != null) {
|
if (stage == WorkflowCheckStage.PRE_EXECUTE && parsedWorkflow != null) {
|
||||||
runStrictChecks(content, parsedWorkflow, currentWorkflowId, issues, issueKeys);
|
runStrictChecks(content, parsedWorkflow, currentWorkflowId, issues, issueKeys);
|
||||||
@@ -394,6 +408,10 @@ public class WorkflowCheckService {
|
|||||||
|
|
||||||
for (NodeView node : parsed.nodes) {
|
for (NodeView node : parsed.nodes) {
|
||||||
if (!TYPE_WORKFLOW.equals(node.type)) {
|
if (!TYPE_WORKFLOW.equals(node.type)) {
|
||||||
|
if (!TYPE_PLUGIN.equals(node.type)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
checkPluginWorkflowReference(node, currentWorkflowIdString, currentContent, contentCache, issues, issueKeys);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
String workflowId = getWorkflowIdInNode(node);
|
String workflowId = getWorkflowIdInNode(node);
|
||||||
@@ -510,12 +528,86 @@ public class WorkflowCheckService {
|
|||||||
refs.add(workflowId);
|
refs.add(workflowId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
refs.addAll(workflowPluginDependencyService.extractWorkflowIdsFromPluginNodes(content));
|
||||||
} catch (Exception ignored) {
|
} catch (Exception ignored) {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
return refs;
|
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) {
|
private String formatCyclePath(LinkedHashSet<String> visiting, String cycleStart) {
|
||||||
List<String> chain = new ArrayList<>();
|
List<String> chain = new ArrayList<>();
|
||||||
boolean started = false;
|
boolean started = false;
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ public class Bot extends BotBase {
|
|||||||
@Column(ignore = true)
|
@Column(ignore = true)
|
||||||
private String displayPublishStatus;
|
private String displayPublishStatus;
|
||||||
|
|
||||||
|
@Column(ignore = true)
|
||||||
|
private String createdByName;
|
||||||
|
|
||||||
public boolean isAnonymousEnabled() {
|
public boolean isAnonymousEnabled() {
|
||||||
Map<String, Object> options = getOptions();
|
Map<String, Object> options = getOptions();
|
||||||
if (options == null) {
|
if (options == null) {
|
||||||
@@ -62,4 +65,22 @@ public class Bot extends BotBase {
|
|||||||
this.displayPublishStatus = displayPublishStatus;
|
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) {
|
public void setOverlapSize(int overlapSize) {
|
||||||
this.overlapSize = 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)
|
@Column(ignore = true)
|
||||||
private String displayPublishStatus;
|
private String displayPublishStatus;
|
||||||
|
|
||||||
|
@Column(ignore = true)
|
||||||
|
private String createdByName;
|
||||||
|
|
||||||
public static final String TYPE_DOCUMENT = "DOCUMENT";
|
public static final String TYPE_DOCUMENT = "DOCUMENT";
|
||||||
public static final String TYPE_FAQ = "FAQ";
|
public static final String TYPE_FAQ = "FAQ";
|
||||||
|
|
||||||
@@ -169,4 +172,22 @@ public class DocumentCollection extends DocumentCollectionBase implements Visibi
|
|||||||
public void setDisplayPublishStatus(String displayPublishStatus) {
|
public void setDisplayPublishStatus(String displayPublishStatus) {
|
||||||
this.displayPublishStatus = 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")
|
@RelationOneToMany(selfField = "id", targetField = "pluginId", targetTable = "tb_plugin_item")
|
||||||
private List<PluginItem> tools;
|
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() {
|
public String getTitle() {
|
||||||
return this.getName();
|
return this.getName();
|
||||||
}
|
}
|
||||||
@@ -30,4 +45,54 @@ public class Plugin extends PluginBase {
|
|||||||
public void setTools(List<PluginItem> tools) {
|
public void setTools(List<PluginItem> tools) {
|
||||||
this.tools = 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)
|
@Column(ignore = true)
|
||||||
private String displayPublishStatus;
|
private String displayPublishStatus;
|
||||||
|
|
||||||
|
@Column(ignore = true)
|
||||||
|
private String createdByName;
|
||||||
|
|
||||||
public Tool toFunction(boolean needEnglishName) {
|
public Tool toFunction(boolean needEnglishName) {
|
||||||
return new WorkflowTool(this, needEnglishName);
|
return new WorkflowTool(this, needEnglishName);
|
||||||
}
|
}
|
||||||
@@ -57,4 +60,22 @@ public class Workflow extends WorkflowBase implements VisibilityResource {
|
|||||||
public void setDisplayPublishStatus(String displayPublishStatus) {
|
public void setDisplayPublishStatus(String displayPublishStatus) {
|
||||||
this.displayPublishStatus = 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 = "其他配置项")
|
@Column(typeHandler = FastjsonTypeHandler.class, comment = "其他配置项")
|
||||||
private Map<String, Object> options;
|
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;
|
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() {
|
public Date getCreated() {
|
||||||
return created;
|
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 = "类型")
|
@Column(comment = "类型")
|
||||||
private Integer type;
|
private Integer type;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绑定工作流ID
|
||||||
|
*/
|
||||||
|
@Column(comment = "绑定工作流ID")
|
||||||
|
private BigInteger workflowId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 基础URL
|
* 基础URL
|
||||||
*/
|
*/
|
||||||
@@ -148,6 +154,14 @@ public class PluginBase implements Serializable {
|
|||||||
this.type = type;
|
this.type = type;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public BigInteger getWorkflowId() {
|
||||||
|
return workflowId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setWorkflowId(BigInteger workflowId) {
|
||||||
|
this.workflowId = workflowId;
|
||||||
|
}
|
||||||
|
|
||||||
public String getBaseUrl() {
|
public String getBaseUrl() {
|
||||||
return baseUrl;
|
return baseUrl;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,6 +90,12 @@ public class PluginItemBase implements Serializable {
|
|||||||
@Column(comment = "英文名称")
|
@Column(comment = "英文名称")
|
||||||
private String englishName;
|
private String englishName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作流插件输入输出契约哈希
|
||||||
|
*/
|
||||||
|
@Column(comment = "工作流插件输入输出契约哈希")
|
||||||
|
private String schemaHash;
|
||||||
|
|
||||||
public BigInteger getId() {
|
public BigInteger getId() {
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
@@ -194,4 +200,12 @@ public class PluginItemBase implements Serializable {
|
|||||||
this.englishName = englishName;
|
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.Chain;
|
||||||
import com.easyagents.flow.core.chain.Parameter;
|
import com.easyagents.flow.core.chain.Parameter;
|
||||||
import com.easyagents.flow.core.node.BaseNode;
|
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 tech.easyflow.common.util.SpringContextUtil;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作流文件内容提取节点。
|
||||||
|
*
|
||||||
|
* <p>节点输入为统一文件对象,PDF 交给统一文档解析桥接服务,
|
||||||
|
* 其他类型继续走默认文档读取器。</p>
|
||||||
|
*
|
||||||
|
* @author Codex
|
||||||
|
* @since 2026-04-14
|
||||||
|
*/
|
||||||
public class DocNode extends BaseNode {
|
public class DocNode extends BaseNode {
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(DocNode.class);
|
/**
|
||||||
|
* 执行文件内容提取。
|
||||||
|
*
|
||||||
|
* @param chain 当前流程链
|
||||||
|
* @return 节点输出
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public Map<String, Object> execute(Chain chain) {
|
public Map<String, Object> execute(Chain chain) {
|
||||||
Map<String, Object> map = chain.getState().resolveParameters(this);
|
Map<String, Object> map = chain.getState().resolveParameters(this);
|
||||||
Map<String, Object> res = new HashMap<>();
|
Map<String, Object> res = new HashMap<>();
|
||||||
String url = map.get("fileUrl").toString();
|
DocNodeFileContentExtractor extractor = SpringContextUtil.getBean(DocNodeFileContentExtractor.class);
|
||||||
byte[] bytes = DocUtil.downloadFile(url);
|
String docContent = extractor.extract(map.get("file"));
|
||||||
ReaderManager manager = SpringContextUtil.getBean(ReaderManager.class);
|
|
||||||
String docContent = manager.getReader().read(DocUtil.getFileNameByUrl(url), new ByteArrayInputStream(bytes));
|
|
||||||
|
|
||||||
String key = "content";
|
String key = "content";
|
||||||
List<Parameter> outputDefs = getOutputDefs();
|
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.alibaba.fastjson.JSON;
|
||||||
import com.easyagents.flow.core.chain.Chain;
|
import com.easyagents.flow.core.chain.Chain;
|
||||||
import com.easyagents.flow.core.node.BaseNode;
|
import com.easyagents.flow.core.node.BaseNode;
|
||||||
|
import tech.easyflow.ai.entity.Plugin;
|
||||||
import tech.easyflow.ai.entity.PluginItem;
|
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.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 tech.easyflow.common.util.SpringContextUtil;
|
||||||
|
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@@ -32,6 +44,11 @@ public class PluginToolNode extends BaseNode {
|
|||||||
if (tool == null) {
|
if (tool == null) {
|
||||||
return Collections.emptyMap();
|
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();
|
Tool function = tool.toFunction();
|
||||||
if (function == null) {
|
if (function == null) {
|
||||||
return Collections.emptyMap();
|
return Collections.emptyMap();
|
||||||
@@ -49,6 +66,43 @@ public class PluginToolNode extends BaseNode {
|
|||||||
return JSON.parseObject(JSON.toJSONString(result), Map.class);
|
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() {
|
public BigInteger getPluginId() {
|
||||||
return pluginId;
|
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.BotWorkflow;
|
||||||
import tech.easyflow.ai.entity.Workflow;
|
import tech.easyflow.ai.entity.Workflow;
|
||||||
import tech.easyflow.ai.enums.PublishStatus;
|
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.BotWorkflowService;
|
||||||
import tech.easyflow.ai.service.ResourceOfflineImpactService;
|
import tech.easyflow.ai.service.ResourceOfflineImpactService;
|
||||||
import tech.easyflow.ai.service.WorkflowService;
|
import tech.easyflow.ai.service.WorkflowService;
|
||||||
@@ -31,18 +33,24 @@ public class WorkflowApprovalSubjectHandler extends AbstractAiResourceLifecycleH
|
|||||||
private final ResourceAccessService resourceAccessService;
|
private final ResourceAccessService resourceAccessService;
|
||||||
private final BotWorkflowService botWorkflowService;
|
private final BotWorkflowService botWorkflowService;
|
||||||
private final ResourceOfflineImpactService resourceOfflineImpactService;
|
private final ResourceOfflineImpactService resourceOfflineImpactService;
|
||||||
|
private final WorkflowPluginBindingService workflowPluginBindingService;
|
||||||
|
private final WorkflowPluginSnapshotResolver workflowPluginSnapshotResolver;
|
||||||
|
|
||||||
public WorkflowApprovalSubjectHandler(WorkflowService workflowService,
|
public WorkflowApprovalSubjectHandler(WorkflowService workflowService,
|
||||||
ResourceAccessService resourceAccessService,
|
ResourceAccessService resourceAccessService,
|
||||||
ApprovalInstanceService approvalInstanceService,
|
ApprovalInstanceService approvalInstanceService,
|
||||||
BotWorkflowService botWorkflowService,
|
BotWorkflowService botWorkflowService,
|
||||||
ResourceOfflineImpactService resourceOfflineImpactService,
|
ResourceOfflineImpactService resourceOfflineImpactService,
|
||||||
|
WorkflowPluginBindingService workflowPluginBindingService,
|
||||||
|
WorkflowPluginSnapshotResolver workflowPluginSnapshotResolver,
|
||||||
ObjectMapper objectMapper) {
|
ObjectMapper objectMapper) {
|
||||||
super(approvalInstanceService, objectMapper);
|
super(approvalInstanceService, objectMapper);
|
||||||
this.workflowService = workflowService;
|
this.workflowService = workflowService;
|
||||||
this.resourceAccessService = resourceAccessService;
|
this.resourceAccessService = resourceAccessService;
|
||||||
this.botWorkflowService = botWorkflowService;
|
this.botWorkflowService = botWorkflowService;
|
||||||
this.resourceOfflineImpactService = resourceOfflineImpactService;
|
this.resourceOfflineImpactService = resourceOfflineImpactService;
|
||||||
|
this.workflowPluginBindingService = workflowPluginBindingService;
|
||||||
|
this.workflowPluginSnapshotResolver = workflowPluginSnapshotResolver;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -116,6 +124,16 @@ public class WorkflowApprovalSubjectHandler extends AbstractAiResourceLifecycleH
|
|||||||
return snapshot;
|
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
|
@Override
|
||||||
protected void persistResourceState(BigInteger resourceId, PublishStatus publishStatus, BigInteger currentApprovalInstanceId) {
|
protected void persistResourceState(BigInteger resourceId, PublishStatus publishStatus, BigInteger currentApprovalInstanceId) {
|
||||||
Workflow update = new Workflow();
|
Workflow update = new Workflow();
|
||||||
@@ -135,6 +153,7 @@ public class WorkflowApprovalSubjectHandler extends AbstractAiResourceLifecycleH
|
|||||||
update.setPublishedAt(new java.util.Date());
|
update.setPublishedAt(new java.util.Date());
|
||||||
update.setPublishedBy(operatorId);
|
update.setPublishedBy(operatorId);
|
||||||
workflowService.updateById(update);
|
workflowService.updateById(update);
|
||||||
|
workflowPluginBindingService.syncByWorkflowId(resourceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -162,6 +181,9 @@ public class WorkflowApprovalSubjectHandler extends AbstractAiResourceLifecycleH
|
|||||||
if (impact.isHasBotBindings()) {
|
if (impact.isHasBotBindings()) {
|
||||||
snapshot.put("botBindings", impact.getBotBindings());
|
snapshot.put("botBindings", impact.getBotBindings());
|
||||||
}
|
}
|
||||||
|
if (impact.isHasPluginBindings()) {
|
||||||
|
snapshot.put("pluginBindings", impact.getPluginBindings());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ public class KnowledgeRetrievalRequest {
|
|||||||
private BigInteger knowledgeId;
|
private BigInteger knowledgeId;
|
||||||
private String query;
|
private String query;
|
||||||
private Integer limit;
|
private Integer limit;
|
||||||
|
private Double minSimilarity;
|
||||||
private RetrievalMode retrievalMode = RetrievalMode.HYBRID;
|
private RetrievalMode retrievalMode = RetrievalMode.HYBRID;
|
||||||
private String callerType;
|
private String callerType;
|
||||||
private String callerId;
|
private String callerId;
|
||||||
@@ -37,6 +38,24 @@ public class KnowledgeRetrievalRequest {
|
|||||||
this.limit = limit;
|
this.limit = limit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回检索时使用的最小相似度阈值。
|
||||||
|
*
|
||||||
|
* @return 最小相似度阈值
|
||||||
|
*/
|
||||||
|
public Double getMinSimilarity() {
|
||||||
|
return minSimilarity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置检索时使用的最小相似度阈值。
|
||||||
|
*
|
||||||
|
* @param minSimilarity 最小相似度阈值
|
||||||
|
*/
|
||||||
|
public void setMinSimilarity(Double minSimilarity) {
|
||||||
|
this.minSimilarity = minSimilarity;
|
||||||
|
}
|
||||||
|
|
||||||
public RetrievalMode getRetrievalMode() {
|
public RetrievalMode getRetrievalMode() {
|
||||||
return retrievalMode;
|
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.PreviewResponse> previewImport(DocumentImportDtos.PreviewRequest request);
|
||||||
|
|
||||||
Result<DocumentImportDtos.CommitResponse> commitImport(DocumentImportDtos.CommitRequest 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);
|
Result pluginToolTest(String inputData, BigInteger pluginToolId);
|
||||||
|
|
||||||
List<PluginItem> getByPluginId(String id);
|
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);
|
Result pageByCategory(Long pageNumber, Long pageSize, int category);
|
||||||
|
|
||||||
boolean updatePlugin(Plugin plugin);
|
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.easyagents.tool.WorkflowTool;
|
||||||
import tech.easyflow.ai.easyagentsflow.support.PublishedWorkflowDefinitionIds;
|
import tech.easyflow.ai.easyagentsflow.support.PublishedWorkflowDefinitionIds;
|
||||||
import tech.easyflow.ai.entity.*;
|
import tech.easyflow.ai.entity.*;
|
||||||
|
import tech.easyflow.ai.enums.PluginType;
|
||||||
import tech.easyflow.ai.enums.PublishStatus;
|
import tech.easyflow.ai.enums.PublishStatus;
|
||||||
import tech.easyflow.ai.mapper.BotMapper;
|
import tech.easyflow.ai.mapper.BotMapper;
|
||||||
import tech.easyflow.ai.service.*;
|
import tech.easyflow.ai.service.*;
|
||||||
@@ -117,6 +118,8 @@ public class BotServiceImpl extends ServiceImpl<BotMapper, Bot> implements BotSe
|
|||||||
@Resource
|
@Resource
|
||||||
private BotPluginService botPluginService;
|
private BotPluginService botPluginService;
|
||||||
@Resource
|
@Resource
|
||||||
|
private PluginService pluginService;
|
||||||
|
@Resource
|
||||||
private PluginItemService pluginItemService;
|
private PluginItemService pluginItemService;
|
||||||
@Resource
|
@Resource
|
||||||
private BotMcpService botMcpService;
|
private BotMcpService botMcpService;
|
||||||
@@ -508,6 +511,12 @@ public class BotServiceImpl extends ServiceImpl<BotMapper, Bot> implements BotSe
|
|||||||
List<PluginItem> pluginItems = pluginItemService.getMapper().selectListWithRelationsByQuery(queryTool);
|
List<PluginItem> pluginItems = pluginItemService.getMapper().selectListWithRelationsByQuery(queryTool);
|
||||||
if (pluginItems != null && !pluginItems.isEmpty()) {
|
if (pluginItems != null && !pluginItems.isEmpty()) {
|
||||||
for (PluginItem pluginItem : pluginItems) {
|
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());
|
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.DocumentCollection;
|
||||||
import tech.easyflow.ai.entity.FaqItem;
|
import tech.easyflow.ai.entity.FaqItem;
|
||||||
import tech.easyflow.ai.entity.Model;
|
import tech.easyflow.ai.entity.Model;
|
||||||
|
import tech.easyflow.ai.enums.DocumentProcessStatus;
|
||||||
import tech.easyflow.ai.enums.PublishStatus;
|
import tech.easyflow.ai.enums.PublishStatus;
|
||||||
import tech.easyflow.ai.mapper.DocumentChunkMapper;
|
import tech.easyflow.ai.mapper.DocumentChunkMapper;
|
||||||
import tech.easyflow.ai.mapper.DocumentCollectionMapper;
|
import tech.easyflow.ai.mapper.DocumentCollectionMapper;
|
||||||
|
import tech.easyflow.ai.mapper.DocumentMapper;
|
||||||
import tech.easyflow.ai.mapper.FaqItemMapper;
|
import tech.easyflow.ai.mapper.FaqItemMapper;
|
||||||
import tech.easyflow.ai.rag.KnowledgeRetrievalRequest;
|
import tech.easyflow.ai.rag.KnowledgeRetrievalRequest;
|
||||||
import tech.easyflow.ai.service.DocumentCollectionService;
|
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 Logger LOG = LoggerFactory.getLogger(DocumentCollectionServiceImpl.class);
|
||||||
private static final int MAX_FAQ_IMAGES_IN_PROMPT = 3;
|
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
|
@Resource
|
||||||
private ModelService llmService;
|
private ModelService llmService;
|
||||||
@@ -81,6 +85,9 @@ public class DocumentCollectionServiceImpl extends ServiceImpl<DocumentCollectio
|
|||||||
@Autowired
|
@Autowired
|
||||||
private DocumentChunkMapper documentChunkMapper;
|
private DocumentChunkMapper documentChunkMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private DocumentMapper documentMapper;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private FaqItemMapper faqItemMapper;
|
private FaqItemMapper faqItemMapper;
|
||||||
|
|
||||||
@@ -111,24 +118,27 @@ public class DocumentCollectionServiceImpl extends ServiceImpl<DocumentCollectio
|
|||||||
throw new BusinessException("知识库不存在");
|
throw new BusinessException("知识库不存在");
|
||||||
}
|
}
|
||||||
|
|
||||||
int docRecallMaxNum = readIntegerOption(documentCollection, KEY_DOC_RECALL_MAX_NUM, 5);
|
int docRecallMaxNum = resolveDocRecallMaxNum(request, documentCollection);
|
||||||
float minSimilarity = readFloatOption(documentCollection, KEY_SIMILARITY_THRESHOLD, 0.6F);
|
int internalRecallLimit = resolveInternalRecallLimit(docRecallMaxNum);
|
||||||
|
float minSimilarity = resolveMinSimilarity(request, documentCollection);
|
||||||
|
|
||||||
RagQuery ragQuery = new RagQuery();
|
RagQuery ragQuery = new RagQuery();
|
||||||
ragQuery.setQuery(keyword);
|
ragQuery.setQuery(keyword);
|
||||||
ragQuery.setRetrievalMode(retrievalMode);
|
ragQuery.setRetrievalMode(retrievalMode);
|
||||||
ragQuery.setTopK(docRecallMaxNum);
|
ragQuery.setTopK(internalRecallLimit);
|
||||||
ragQuery.setMinScore((double) minSimilarity);
|
ragQuery.setMinScore((double) minSimilarity);
|
||||||
|
|
||||||
RagRetrievalExecutor retrievalExecutor = new RagRetrievalExecutor(
|
RagRetrievalExecutor retrievalExecutor = new RagRetrievalExecutor(
|
||||||
buildVectorRetriever(documentCollection, docRecallMaxNum, retrievalMode == RetrievalMode.VECTOR ? minSimilarity : null),
|
buildVectorRetriever(documentCollection, internalRecallLimit, retrievalMode == RetrievalMode.VECTOR ? minSimilarity : null),
|
||||||
buildKeywordRetriever(documentCollection, docRecallMaxNum),
|
buildKeywordRetriever(documentCollection, internalRecallLimit),
|
||||||
new RrfFusionStrategy()
|
new RrfFusionStrategy()
|
||||||
);
|
);
|
||||||
|
|
||||||
RagRetrievalResult retrievalResult = retrievalExecutor.retrieve(ragQuery);
|
RagRetrievalResult retrievalResult = retrievalExecutor.retrieve(ragQuery);
|
||||||
List<Document> searchDocuments = toDocuments(retrievalResult.getHits());
|
List<Document> searchDocuments = prepareSearchDocuments(
|
||||||
fillSearchContent(documentCollection, searchDocuments);
|
documentCollection,
|
||||||
|
toDocuments(retrievalResult.getHits())
|
||||||
|
);
|
||||||
if (searchDocuments.isEmpty()) {
|
if (searchDocuments.isEmpty()) {
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
@@ -138,7 +148,10 @@ public class DocumentCollectionServiceImpl extends ServiceImpl<DocumentCollectio
|
|||||||
if (rerankModel != null) {
|
if (rerankModel != null) {
|
||||||
try {
|
try {
|
||||||
RagRetrievalResult rerankResult = retrievalExecutor.rerank(keyword, toRagHits(searchDocuments), rerankModel, docRecallMaxNum);
|
RagRetrievalResult rerankResult = retrievalExecutor.rerank(keyword, toRagHits(searchDocuments), rerankModel, docRecallMaxNum);
|
||||||
searchDocuments = toDocuments(rerankResult.getHits());
|
searchDocuments = prepareSearchDocuments(
|
||||||
|
documentCollection,
|
||||||
|
toDocuments(rerankResult.getHits())
|
||||||
|
);
|
||||||
reranked = true;
|
reranked = true;
|
||||||
} catch (RerankException e) {
|
} catch (RerankException e) {
|
||||||
LOG.warn("Rerank failed for collectionId={}, modelId={}, fallback to retrieved results. message={}",
|
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;
|
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
|
@Override
|
||||||
public DocumentCollection getDetail(String idOrAlias) {
|
public DocumentCollection getDetail(String idOrAlias) {
|
||||||
DocumentCollection knowledge = null;
|
DocumentCollection knowledge = null;
|
||||||
@@ -418,18 +509,93 @@ public class DocumentCollectionServiceImpl extends ServiceImpl<DocumentCollectio
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
QueryWrapper queryWrapper = QueryWrapper.create();
|
DocumentHitSnapshot hitSnapshot = loadDocumentHitSnapshot(documentCollection, searchDocuments);
|
||||||
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())));
|
|
||||||
searchDocuments.forEach(item -> {
|
searchDocuments.forEach(item -> {
|
||||||
DocumentChunk documentChunk = chunkMap.get(String.valueOf(item.getId()));
|
item.setContent(hitSnapshot.findChunkContent(item.getId()));
|
||||||
if (documentChunk != null && !StringUtil.noText(documentChunk.getContent())) {
|
|
||||||
item.setContent(documentChunk.getContent());
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
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) {
|
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.DocumentImportDtos;
|
||||||
import tech.easyflow.ai.documentimport.DocumentImportKeys;
|
import tech.easyflow.ai.documentimport.DocumentImportKeys;
|
||||||
import tech.easyflow.ai.documentimport.DocumentImportPreviewService;
|
import tech.easyflow.ai.documentimport.DocumentImportPreviewService;
|
||||||
|
import tech.easyflow.ai.documentimport.task.KnowledgeDocumentImportTaskAppService;
|
||||||
import tech.easyflow.ai.entity.*;
|
import tech.easyflow.ai.entity.*;
|
||||||
|
import tech.easyflow.ai.enums.DocumentProcessStatus;
|
||||||
import tech.easyflow.ai.mapper.DocumentChunkMapper;
|
import tech.easyflow.ai.mapper.DocumentChunkMapper;
|
||||||
import tech.easyflow.ai.mapper.DocumentMapper;
|
import tech.easyflow.ai.mapper.DocumentMapper;
|
||||||
import tech.easyflow.ai.service.DocumentChunkService;
|
import tech.easyflow.ai.service.DocumentChunkService;
|
||||||
@@ -69,6 +71,7 @@ import static tech.easyflow.ai.entity.table.DocumentTableDef.DOCUMENT;
|
|||||||
@Service("AiService")
|
@Service("AiService")
|
||||||
public class DocumentServiceImpl extends ServiceImpl<DocumentMapper, Document> implements DocumentService {
|
public class DocumentServiceImpl extends ServiceImpl<DocumentMapper, Document> implements DocumentService {
|
||||||
protected Logger Log = LoggerFactory.getLogger(DocumentServiceImpl.class);
|
protected Logger Log = LoggerFactory.getLogger(DocumentServiceImpl.class);
|
||||||
|
private static final String SOURCE_RANGES_KEY = "sourceRanges";
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private DocumentMapper documentMapper;
|
private DocumentMapper documentMapper;
|
||||||
@@ -97,6 +100,9 @@ public class DocumentServiceImpl extends ServiceImpl<DocumentMapper, Document> i
|
|||||||
@Autowired
|
@Autowired
|
||||||
private DocumentImportPreviewService documentImportPreviewService;
|
private DocumentImportPreviewService documentImportPreviewService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private KnowledgeDocumentImportTaskAppService importTaskAppService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Page<Document> getDocumentList(String knowledgeId, int pageSize, int pageNum, String fileName) {
|
public Page<Document> getDocumentList(String knowledgeId, int pageSize, int pageNum, String fileName) {
|
||||||
QueryWrapper queryWrapper=QueryWrapper.create()
|
QueryWrapper queryWrapper=QueryWrapper.create()
|
||||||
@@ -130,6 +136,13 @@ public class DocumentServiceImpl extends ServiceImpl<DocumentMapper, Document> i
|
|||||||
// 查询该文档对应哪些分割的字段,先删除
|
// 查询该文档对应哪些分割的字段,先删除
|
||||||
QueryWrapper queryWrapperDocument = QueryWrapper.create().eq(Document::getId, id);
|
QueryWrapper queryWrapperDocument = QueryWrapper.create().eq(Document::getId, id);
|
||||||
Document oneByQuery = documentMapper.selectOneByQuery(queryWrapperDocument);
|
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());
|
DocumentCollection knowledge = knowledgeService.getById(oneByQuery.getCollectionId());
|
||||||
if (knowledge == null) {
|
if (knowledge == null) {
|
||||||
return false;
|
return false;
|
||||||
@@ -209,12 +222,18 @@ public class DocumentServiceImpl extends ServiceImpl<DocumentMapper, Document> i
|
|||||||
aiDocument.setDocumentPath(filePath);
|
aiDocument.setDocumentPath(filePath);
|
||||||
aiDocument.setCreated(new Date());
|
aiDocument.setCreated(new Date());
|
||||||
aiDocument.setModifiedBy(BigInteger.valueOf(StpUtil.getLoginIdAsLong()));
|
aiDocument.setModifiedBy(BigInteger.valueOf(StpUtil.getLoginIdAsLong()));
|
||||||
aiDocument.setModified(new Date());
|
aiDocument.setModified(new Date());
|
||||||
aiDocument.setContent(document.getContent());
|
aiDocument.setContent(document.getContent());
|
||||||
aiDocument.setChunkSize(documentCollectionSplitParams.getChunkSize());
|
aiDocument.setChunkSize(documentCollectionSplitParams.getChunkSize());
|
||||||
aiDocument.setOverlapSize(documentCollectionSplitParams.getOverlapSize());
|
aiDocument.setOverlapSize(documentCollectionSplitParams.getOverlapSize());
|
||||||
aiDocument.setTitle(fileOriginName);
|
aiDocument.setTitle(fileOriginName);
|
||||||
Map<String, Object> res = new HashMap<>();
|
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;
|
List<DocumentChunk> documentChunks = null;
|
||||||
String operation = documentCollectionSplitParams.getOperation();
|
String operation = documentCollectionSplitParams.getOperation();
|
||||||
@@ -334,10 +353,11 @@ public class DocumentServiceImpl extends ServiceImpl<DocumentMapper, Document> i
|
|||||||
item.setPreviewSessionId(sessionId);
|
item.setPreviewSessionId(sessionId);
|
||||||
item.setFilePath(file.getFilePath());
|
item.setFilePath(file.getFilePath());
|
||||||
item.setFileName(file.getFileName());
|
item.setFileName(file.getFileName());
|
||||||
|
item.setNormalizedContent(session.getAnalysis() == null ? null : session.getAnalysis().getNormalizedContent());
|
||||||
item.setStrategyCode(session.getStrategyConfig().getStrategyCode());
|
item.setStrategyCode(session.getStrategyConfig().getStrategyCode());
|
||||||
item.setStrategyLabel(ragIngestionService.toStrategyLabel(session.getStrategyConfig().getStrategyCode()));
|
item.setStrategyLabel(ragIngestionService.toStrategyLabel(session.getStrategyConfig().getStrategyCode()));
|
||||||
item.setAnalysis(session.getAnalysis());
|
item.setAnalysis(session.getAnalysis());
|
||||||
item.setChunks(session.getPreviewChunks());
|
item.setChunks(toPreviewChunkResults(session.getPreviewChunks()));
|
||||||
item.setTotalChunks(session.getPreviewChunks().size());
|
item.setTotalChunks(session.getPreviewChunks().size());
|
||||||
item.setTotalWarnings(countWarnings(session.getPreviewChunks()));
|
item.setTotalWarnings(countWarnings(session.getPreviewChunks()));
|
||||||
items.add(item);
|
items.add(item);
|
||||||
@@ -398,6 +418,12 @@ public class DocumentServiceImpl extends ServiceImpl<DocumentMapper, Document> i
|
|||||||
document.setModified(new Date());
|
document.setModified(new Date());
|
||||||
document.setCreatedBy(BigInteger.valueOf(StpUtil.getLoginIdAsLong()));
|
document.setCreatedBy(BigInteger.valueOf(StpUtil.getLoginIdAsLong()));
|
||||||
document.setModifiedBy(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()) {
|
for (DocumentChunk chunk : session.getDocumentChunks()) {
|
||||||
chunk.setDocumentId(document.getId());
|
chunk.setDocumentId(document.getId());
|
||||||
chunk.setDocumentCollectionId(document.getCollectionId());
|
chunk.setDocumentCollectionId(document.getCollectionId());
|
||||||
@@ -430,6 +456,7 @@ public class DocumentServiceImpl extends ServiceImpl<DocumentMapper, Document> i
|
|||||||
|
|
||||||
DocumentImportDtos.PreviewSession session = new DocumentImportDtos.PreviewSession();
|
DocumentImportDtos.PreviewSession session = new DocumentImportDtos.PreviewSession();
|
||||||
session.setKnowledgeId(knowledge.getId());
|
session.setKnowledgeId(knowledge.getId());
|
||||||
|
session.setDocumentId(document.getId());
|
||||||
session.setFilePath(fileRequest.getFilePath());
|
session.setFilePath(fileRequest.getFilePath());
|
||||||
session.setFileName(fileRequest.getFileName());
|
session.setFileName(fileRequest.getFileName());
|
||||||
session.setSourceFormat(analysis.getSourceFormat());
|
session.setSourceFormat(analysis.getSourceFormat());
|
||||||
@@ -656,6 +683,55 @@ public class DocumentServiceImpl extends ServiceImpl<DocumentMapper, Document> i
|
|||||||
return total;
|
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) {
|
private StoreExecutionContext prepareStoreContext(Document entity) {
|
||||||
DocumentCollection knowledge = knowledgeService.getById(entity.getCollectionId());
|
DocumentCollection knowledge = knowledgeService.getById(entity.getCollectionId());
|
||||||
if (knowledge == null) {
|
if (knowledge == null) {
|
||||||
@@ -882,4 +958,34 @@ public class DocumentServiceImpl extends ServiceImpl<DocumentMapper, Document> i
|
|||||||
}
|
}
|
||||||
return null;
|
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