feat: 完成工作流 Public API 授权闭环
- 新增访问令牌工作流 API 全局授权与 Public Workflow API 权限断言 - 补齐 API Key 执行记录归属、状态查询与下线后不可恢复边界 - 增加管理端接口调用说明与访问令牌授权开关
This commit is contained in:
@@ -11,6 +11,7 @@ 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.ai.service.KnowledgeSharePermissionService;
|
||||||
|
import tech.easyflow.ai.service.WorkflowApiPermissionService;
|
||||||
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;
|
||||||
@@ -46,6 +47,8 @@ public class SysApiKeyController extends BaseCurdController<SysApiKeyService, Sy
|
|||||||
private SysApiKeyResourceMappingService sysApiKeyResourceMappingService;
|
private SysApiKeyResourceMappingService sysApiKeyResourceMappingService;
|
||||||
@Resource
|
@Resource
|
||||||
private KnowledgeSharePermissionService knowledgeSharePermissionService;
|
private KnowledgeSharePermissionService knowledgeSharePermissionService;
|
||||||
|
@Resource
|
||||||
|
private WorkflowApiPermissionService workflowApiPermissionService;
|
||||||
/**
|
/**
|
||||||
* 添加(保存)数据
|
* 添加(保存)数据
|
||||||
*
|
*
|
||||||
@@ -88,6 +91,9 @@ public class SysApiKeyController extends BaseCurdController<SysApiKeyService, Sy
|
|||||||
if (entity.getKnowledgeShareEnabled() != null) {
|
if (entity.getKnowledgeShareEnabled() != null) {
|
||||||
knowledgeSharePermissionService.replaceApiShareEnabled(entity.getId(), entity.getKnowledgeShareEnabled());
|
knowledgeSharePermissionService.replaceApiShareEnabled(entity.getId(), entity.getKnowledgeShareEnabled());
|
||||||
}
|
}
|
||||||
|
if (entity.getWorkflowApiEnabled() != null) {
|
||||||
|
workflowApiPermissionService.replaceWorkflowApiEnabled(entity.getId(), entity.getWorkflowApiEnabled());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -129,5 +135,11 @@ public class SysApiKeyController extends BaseCurdController<SysApiKeyService, Sy
|
|||||||
.eq(SysApiKeyResourceMapping::getApiKeyId, entity.getId())
|
.eq(SysApiKeyResourceMapping::getApiKeyId, entity.getId())
|
||||||
.eq(SysApiKeyResourceMapping::getResourceType, "KNOWLEDGE");
|
.eq(SysApiKeyResourceMapping::getResourceType, "KNOWLEDGE");
|
||||||
entity.setKnowledgeShareEnabled(sysApiKeyResourceMappingService.count(knowledgeWrapper) > 0);
|
entity.setKnowledgeShareEnabled(sysApiKeyResourceMappingService.count(knowledgeWrapper) > 0);
|
||||||
|
|
||||||
|
QueryWrapper workflowWrapper = QueryWrapper.create()
|
||||||
|
.select(SysApiKeyResourceMapping::getId)
|
||||||
|
.eq(SysApiKeyResourceMapping::getApiKeyId, entity.getId())
|
||||||
|
.eq(SysApiKeyResourceMapping::getResourceType, WorkflowApiPermissionService.RESOURCE_TYPE_WORKFLOW);
|
||||||
|
entity.setWorkflowApiEnabled(sysApiKeyResourceMappingService.count(workflowWrapper) > 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
package tech.easyflow.admin.controller.system;
|
package tech.easyflow.admin.controller.system;
|
||||||
|
|
||||||
|
import com.mybatisflex.core.query.QueryWrapper;
|
||||||
import tech.easyflow.common.annotation.UsePermission;
|
import tech.easyflow.common.annotation.UsePermission;
|
||||||
|
import tech.easyflow.common.domain.Result;
|
||||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||||
|
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.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import tech.easyflow.system.entity.SysApiKeyResource;
|
import tech.easyflow.system.entity.SysApiKeyResource;
|
||||||
import tech.easyflow.system.service.SysApiKeyResourceService;
|
import tech.easyflow.system.service.SysApiKeyResourceService;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 请求接口表 控制层。
|
* 请求接口表 控制层。
|
||||||
*
|
*
|
||||||
@@ -20,4 +25,26 @@ public class SysApiKeyResourceController extends BaseCurdController<SysApiKeyRes
|
|||||||
public SysApiKeyResourceController(SysApiKeyResourceService service) {
|
public SysApiKeyResourceController(SysApiKeyResourceService service) {
|
||||||
super(service);
|
super(service);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询普通 API Key 接口授权资源。
|
||||||
|
*
|
||||||
|
* <p>工作流 Public API 使用独立的全局授权开关,不进入普通接口授权列表,避免用户误以为勾选
|
||||||
|
* 具体接口资源即可完成工作流调用授权。</p>
|
||||||
|
*
|
||||||
|
* @param entity 查询条件
|
||||||
|
* @param asTree 是否树形返回
|
||||||
|
* @param sortKey 排序字段
|
||||||
|
* @param sortType 排序方向
|
||||||
|
* @return 普通接口授权资源
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@GetMapping("list")
|
||||||
|
public Result<List<SysApiKeyResource>> list(SysApiKeyResource entity, Boolean asTree, String sortKey, String sortType) {
|
||||||
|
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
|
||||||
|
queryWrapper.notLike(SysApiKeyResource::getRequestInterface, "/public-api/workflow/");
|
||||||
|
queryWrapper.notLike(SysApiKeyResource::getRequestInterface, "/public-api/knowledge-share/");
|
||||||
|
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||||
|
return Result.ok(service.list(queryWrapper));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -4,22 +4,31 @@ import cn.dev33.satoken.annotation.SaCheckPermission;
|
|||||||
import cn.dev33.satoken.stp.StpUtil;
|
import cn.dev33.satoken.stp.StpUtil;
|
||||||
import com.easyagents.flow.core.chain.runtime.ChainExecutor;
|
import com.easyagents.flow.core.chain.runtime.ChainExecutor;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import tech.easyflow.approval.annotation.RequirePublishedAccess;
|
import tech.easyflow.approval.annotation.RequirePublishedAccess;
|
||||||
import tech.easyflow.ai.easyagentsflow.support.PublishedWorkflowDefinitionIds;
|
|
||||||
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;
|
||||||
import tech.easyflow.ai.easyagentsflow.entity.WorkflowCheckStage;
|
import tech.easyflow.ai.easyagentsflow.entity.WorkflowCheckStage;
|
||||||
import tech.easyflow.ai.easyagentsflow.service.TinyFlowService;
|
import tech.easyflow.ai.easyagentsflow.service.TinyFlowService;
|
||||||
import tech.easyflow.ai.easyagentsflow.service.WorkflowCheckService;
|
import tech.easyflow.ai.easyagentsflow.service.WorkflowCheckService;
|
||||||
import tech.easyflow.ai.easyagentsflow.service.WorkflowRunningParameterResolver;
|
import tech.easyflow.ai.easyagentsflow.service.WorkflowRunningParameterResolver;
|
||||||
|
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.enums.PublishStatus;
|
||||||
|
import tech.easyflow.ai.service.WorkflowExecResultService;
|
||||||
import tech.easyflow.ai.service.WorkflowService;
|
import tech.easyflow.ai.service.WorkflowService;
|
||||||
|
import tech.easyflow.ai.service.WorkflowApiPermissionService;
|
||||||
|
import tech.easyflow.ai.utils.WorkFlowUtil;
|
||||||
import tech.easyflow.common.constant.Constants;
|
import tech.easyflow.common.constant.Constants;
|
||||||
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.satoken.util.SaTokenUtil;
|
||||||
|
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||||
|
import tech.easyflow.system.entity.SysApiKey;
|
||||||
|
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@@ -43,6 +52,10 @@ public class PublicWorkflowController {
|
|||||||
private WorkflowCheckService workflowCheckService;
|
private WorkflowCheckService workflowCheckService;
|
||||||
@Resource
|
@Resource
|
||||||
private WorkflowRunningParameterResolver workflowRunningParameterResolver;
|
private WorkflowRunningParameterResolver workflowRunningParameterResolver;
|
||||||
|
@Resource
|
||||||
|
private WorkflowApiPermissionService workflowApiPermissionService;
|
||||||
|
@Resource
|
||||||
|
private WorkflowExecResultService workflowExecResultService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通过id或别名获取工作流详情
|
* 通过id或别名获取工作流详情
|
||||||
@@ -54,8 +67,11 @@ public class PublicWorkflowController {
|
|||||||
@RequirePublishedAccess(resourceType = "WORKFLOW", idExpr = "#key", denyMessage = "工作流尚未发布")
|
@RequirePublishedAccess(resourceType = "WORKFLOW", idExpr = "#key", denyMessage = "工作流尚未发布")
|
||||||
public Result<Workflow> getByIdOrAlias(
|
public Result<Workflow> getByIdOrAlias(
|
||||||
@RequestParam
|
@RequestParam
|
||||||
@NotBlank(message = "key不能为空") String key) {
|
@NotBlank(message = "key不能为空") String key,
|
||||||
|
HttpServletRequest request) {
|
||||||
|
workflowApiPermissionService.assertWorkflowApi(request.getHeader("ApiKey"), request.getRequestURI());
|
||||||
Workflow workflow = workflowService.getPublishedDetail(key);
|
Workflow workflow = workflowService.getPublishedDetail(key);
|
||||||
|
assertStrictPublishedWorkflow(workflow);
|
||||||
return Result.ok(workflow);
|
return Result.ok(workflow);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,19 +104,20 @@ public class PublicWorkflowController {
|
|||||||
* 运行工作流 - v2
|
* 运行工作流 - v2
|
||||||
*/
|
*/
|
||||||
@PostMapping("/runAsync")
|
@PostMapping("/runAsync")
|
||||||
@SaCheckPermission("/api/v1/workflow/save")
|
|
||||||
@RequirePublishedAccess(resourceType = "WORKFLOW", idExpr = "#id", denyMessage = "工作流尚未发布")
|
@RequirePublishedAccess(resourceType = "WORKFLOW", idExpr = "#id", denyMessage = "工作流尚未发布")
|
||||||
public Result<String> runAsync(@JsonBody(value = "id", required = true) BigInteger id,
|
public Result<String> runAsync(@JsonBody(value = "id", required = true) BigInteger id,
|
||||||
@JsonBody("variables") Map<String, Object> variables) {
|
@JsonBody("variables") Map<String, Object> variables,
|
||||||
|
HttpServletRequest request) {
|
||||||
|
SysApiKey apiKey = workflowApiPermissionService.assertWorkflowApi(request.getHeader("ApiKey"), request.getRequestURI());
|
||||||
if (variables == null) {
|
if (variables == null) {
|
||||||
variables = new HashMap<>();
|
variables = new HashMap<>();
|
||||||
}
|
}
|
||||||
Workflow workflow = workflowService.getPublishedById(id);
|
Workflow workflow = workflowService.getPublishedById(id);
|
||||||
if (workflow == null) {
|
assertStrictPublishedWorkflow(workflow);
|
||||||
throw new RuntimeException("工作流不存在");
|
|
||||||
}
|
|
||||||
workflowCheckService.checkOrThrow(workflow.getContent(), WorkflowCheckStage.PRE_EXECUTE, workflow.getId());
|
workflowCheckService.checkOrThrow(workflow.getContent(), WorkflowCheckStage.PRE_EXECUTE, workflow.getId());
|
||||||
variables = workflowRunningParameterResolver.normalizeRuntimeVariables(workflow.getContent(), variables);
|
variables = workflowRunningParameterResolver.normalizeRuntimeVariables(workflow.getContent(), variables);
|
||||||
|
variables.put(Constants.LOGIN_USER_KEY, buildApiKeyLoginAccount(apiKey));
|
||||||
|
variables.put(WorkFlowUtil.CREATED_KEY_MEMORY_KEY, WorkFlowUtil.API_KEY);
|
||||||
String executeId = chainExecutor.executeAsync(PublishedWorkflowDefinitionIds.published(id.toString()), variables);
|
String executeId = chainExecutor.executeAsync(PublishedWorkflowDefinitionIds.published(id.toString()), variables);
|
||||||
return Result.ok(executeId);
|
return Result.ok(executeId);
|
||||||
}
|
}
|
||||||
@@ -110,7 +127,10 @@ public class PublicWorkflowController {
|
|||||||
*/
|
*/
|
||||||
@PostMapping("/getChainStatus")
|
@PostMapping("/getChainStatus")
|
||||||
public Result<ChainInfo> getChainStatus(@JsonBody(value = "executeId") String executeId,
|
public Result<ChainInfo> getChainStatus(@JsonBody(value = "executeId") String executeId,
|
||||||
@JsonBody("nodes") List<NodeInfo> nodes) {
|
@JsonBody("nodes") List<NodeInfo> nodes,
|
||||||
|
HttpServletRequest request) {
|
||||||
|
SysApiKey apiKey = workflowApiPermissionService.assertWorkflowApi(request.getHeader("ApiKey"), request.getRequestURI());
|
||||||
|
assertApiKeyExecutionOwnership(apiKey, executeId);
|
||||||
ChainInfo res = tinyFlowService.getChainStatus(executeId, nodes);
|
ChainInfo res = tinyFlowService.getChainStatus(executeId, nodes);
|
||||||
return Result.ok(res);
|
return Result.ok(res);
|
||||||
}
|
}
|
||||||
@@ -119,22 +139,23 @@ public class PublicWorkflowController {
|
|||||||
* 恢复工作流运行 - v2
|
* 恢复工作流运行 - v2
|
||||||
*/
|
*/
|
||||||
@PostMapping("/resume")
|
@PostMapping("/resume")
|
||||||
@SaCheckPermission("/api/v1/workflow/save")
|
|
||||||
public Result<Void> resume(@JsonBody(value = "executeId", required = true) String executeId,
|
public Result<Void> resume(@JsonBody(value = "executeId", required = true) String executeId,
|
||||||
@JsonBody("confirmParams") Map<String, Object> confirmParams) {
|
@JsonBody("confirmParams") Map<String, Object> confirmParams,
|
||||||
|
HttpServletRequest request) {
|
||||||
|
SysApiKey apiKey = workflowApiPermissionService.assertWorkflowApi(request.getHeader("ApiKey"), request.getRequestURI());
|
||||||
|
WorkflowExecResult execResult = assertApiKeyExecutionOwnership(apiKey, executeId);
|
||||||
|
assertWorkflowExecutionResumable(execResult);
|
||||||
chainExecutor.resumeAsync(executeId, confirmParams);
|
chainExecutor.resumeAsync(executeId, confirmParams);
|
||||||
return Result.ok();
|
return Result.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("getRunningParameters")
|
@GetMapping("getRunningParameters")
|
||||||
@SaCheckPermission("/api/v1/workflow/query")
|
|
||||||
@RequirePublishedAccess(resourceType = "WORKFLOW", idExpr = "#id", denyMessage = "工作流尚未发布")
|
@RequirePublishedAccess(resourceType = "WORKFLOW", idExpr = "#id", denyMessage = "工作流尚未发布")
|
||||||
public Result<?> getRunningParameters(@RequestParam BigInteger id) {
|
public Result<?> getRunningParameters(@RequestParam BigInteger id, HttpServletRequest request) {
|
||||||
|
workflowApiPermissionService.assertWorkflowApi(request.getHeader("ApiKey"), request.getRequestURI());
|
||||||
Workflow workflow = workflowService.getPublishedById(id);
|
Workflow workflow = workflowService.getPublishedById(id);
|
||||||
|
|
||||||
if (workflow == null) {
|
assertStrictPublishedWorkflow(workflow);
|
||||||
return Result.fail(1, "can not find the workflow by id: " + id);
|
|
||||||
}
|
|
||||||
workflowCheckService.checkOrThrow(workflow.getContent(), WorkflowCheckStage.PRE_EXECUTE, workflow.getId());
|
workflowCheckService.checkOrThrow(workflow.getContent(), WorkflowCheckStage.PRE_EXECUTE, workflow.getId());
|
||||||
Map<String, Object> res = workflowRunningParameterResolver.buildRunningParametersView(workflow);
|
Map<String, Object> res = workflowRunningParameterResolver.buildRunningParametersView(workflow);
|
||||||
if (res == null) {
|
if (res == null) {
|
||||||
@@ -142,4 +163,72 @@ public class PublicWorkflowController {
|
|||||||
}
|
}
|
||||||
return Result.ok(res);
|
return Result.ok(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建 API Key 调用方的运行身份。
|
||||||
|
*
|
||||||
|
* @param apiKey 访问令牌
|
||||||
|
* @return 工作流运行身份
|
||||||
|
*/
|
||||||
|
private LoginAccount buildApiKeyLoginAccount(SysApiKey apiKey) {
|
||||||
|
LoginAccount account = new LoginAccount();
|
||||||
|
account.setId(apiKey.getId());
|
||||||
|
account.setDeptId(apiKey.getDeptId() == null ? BigInteger.ZERO : apiKey.getDeptId());
|
||||||
|
account.setTenantId(apiKey.getTenantId() == null ? BigInteger.ZERO : apiKey.getTenantId());
|
||||||
|
account.setLoginName("apikey:" + apiKey.getId());
|
||||||
|
account.setNickname("API 调用方");
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验工作流 Public API 只能访问严格已发布且存在发布快照的工作流。
|
||||||
|
*
|
||||||
|
* @param workflow 工作流发布视图
|
||||||
|
*/
|
||||||
|
private void assertStrictPublishedWorkflow(Workflow workflow) {
|
||||||
|
if (workflow == null || !PublishStatus.PUBLISHED.getCode().equals(workflow.getPublishStatus())
|
||||||
|
|| workflow.getPublishedSnapshotJson() == null || workflow.getPublishedSnapshotJson().isEmpty()) {
|
||||||
|
throw new BusinessException("工作流尚未发布");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验 Public Workflow API 后续操作只能作用于当前 API Key 发起的执行实例。
|
||||||
|
*
|
||||||
|
* @param apiKey 当前 API Key
|
||||||
|
* @param executeId 执行 ID
|
||||||
|
* @return 已通过归属校验的执行记录
|
||||||
|
*/
|
||||||
|
private WorkflowExecResult assertApiKeyExecutionOwnership(SysApiKey apiKey, String executeId) {
|
||||||
|
if (executeId == null || executeId.isBlank()) {
|
||||||
|
throw new BusinessException("执行ID不能为空");
|
||||||
|
}
|
||||||
|
WorkflowExecResult execResult = workflowExecResultService.getByExecKey(executeId);
|
||||||
|
if (execResult == null) {
|
||||||
|
throw new BusinessException("工作流执行记录不存在,请稍后重试");
|
||||||
|
}
|
||||||
|
if (!WorkFlowUtil.API_KEY.equals(execResult.getCreatedKey())
|
||||||
|
|| apiKey == null
|
||||||
|
|| apiKey.getId() == null
|
||||||
|
|| !String.valueOf(apiKey.getId()).equals(execResult.getCreatedBy())) {
|
||||||
|
throw new BusinessException("无权限访问当前工作流执行记录");
|
||||||
|
}
|
||||||
|
return execResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验当前执行实例是否仍允许恢复。
|
||||||
|
*
|
||||||
|
* @param execResult 执行记录
|
||||||
|
*/
|
||||||
|
private void assertWorkflowExecutionResumable(WorkflowExecResult execResult) {
|
||||||
|
if (execResult == null || execResult.getWorkflowId() == null) {
|
||||||
|
throw new BusinessException("工作流执行记录不存在,请稍后重试");
|
||||||
|
}
|
||||||
|
Workflow workflow = workflowService.getById(execResult.getWorkflowId());
|
||||||
|
if (workflow == null || !PublishStatus.PUBLISHED.getCode().equals(workflow.getPublishStatus())
|
||||||
|
|| workflow.getPublishedSnapshotJson() == null || workflow.getPublishedSnapshotJson().isEmpty()) {
|
||||||
|
throw new BusinessException("工作流已下线或不可恢复执行");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ public class ChainEventListenerForSave implements ChainEventListener {
|
|||||||
record.setWorkflowJson(workflow.getContent());
|
record.setWorkflowJson(workflow.getContent());
|
||||||
record.setStartTime(new Date());
|
record.setStartTime(new Date());
|
||||||
record.setStatus(state.getStatus().getValue());
|
record.setStatus(state.getStatus().getValue());
|
||||||
record.setCreatedKey(WorkFlowUtil.USER_KEY);
|
record.setCreatedKey(WorkFlowUtil.getCreatedKey(chain));
|
||||||
record.setCreatedBy(WorkFlowUtil.getOperator(chain).getId().toString());
|
record.setCreatedBy(WorkFlowUtil.getOperator(chain).getId().toString());
|
||||||
try {
|
try {
|
||||||
workflowExecResultService.save(record);
|
workflowExecResultService.save(record);
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package tech.easyflow.ai.service;
|
||||||
|
|
||||||
|
import tech.easyflow.system.entity.SysApiKey;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作流 Public API 访问令牌授权服务。
|
||||||
|
*/
|
||||||
|
public interface WorkflowApiPermissionService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作流资源类型。
|
||||||
|
*/
|
||||||
|
String RESOURCE_TYPE_WORKFLOW = "WORKFLOW";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作流 Public API 调用动作范围。
|
||||||
|
*/
|
||||||
|
String ACTION_SCOPE_INVOKE = "INVOKE";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按访问令牌维度开启或关闭工作流 Public API 调用授权。
|
||||||
|
*
|
||||||
|
* @param apiKeyId 系统访问令牌 ID
|
||||||
|
* @param enabled 是否启用工作流 API 调用授权
|
||||||
|
*/
|
||||||
|
void replaceWorkflowApiEnabled(BigInteger apiKeyId, boolean enabled);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 断言当前令牌具备工作流 Public API 调用权限。
|
||||||
|
*
|
||||||
|
* @param apiKey 原始访问令牌
|
||||||
|
* @param requestUri 请求地址
|
||||||
|
* @return 已校验通过的访问令牌实体
|
||||||
|
*/
|
||||||
|
SysApiKey assertWorkflowApi(String apiKey, String requestUri);
|
||||||
|
}
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
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.service.WorkflowApiPermissionService;
|
||||||
|
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.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作流 Public API 访问令牌授权服务实现。
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class WorkflowApiPermissionServiceImpl implements WorkflowApiPermissionService {
|
||||||
|
|
||||||
|
private static final String RESOURCE_TITLE = "工作流 API 调用";
|
||||||
|
|
||||||
|
private static final List<String> WORKFLOW_API_URIS = List.of(
|
||||||
|
"/public-api/workflow/getByIdOrAlias",
|
||||||
|
"/public-api/workflow/getRunningParameters",
|
||||||
|
"/public-api/workflow/runAsync",
|
||||||
|
"/public-api/workflow/getChainStatus",
|
||||||
|
"/public-api/workflow/resume"
|
||||||
|
);
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private SysApiKeyService sysApiKeyService;
|
||||||
|
@Resource
|
||||||
|
private SysApiKeyResourceService resourceService;
|
||||||
|
@Resource
|
||||||
|
private SysApiKeyResourceMappingService mappingService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void replaceWorkflowApiEnabled(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_WORKFLOW);
|
||||||
|
if (!enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<SysApiKeyResourceMapping> rows = new ArrayList<>(WORKFLOW_API_URIS.size());
|
||||||
|
for (String uri : WORKFLOW_API_URIS) {
|
||||||
|
SysApiKeyResource resource = ensureResource(uri);
|
||||||
|
SysApiKeyResourceMapping row = new SysApiKeyResourceMapping();
|
||||||
|
row.setApiKeyId(apiKeyId);
|
||||||
|
row.setApiKeyResourceId(resource.getId());
|
||||||
|
row.setResourceType(RESOURCE_TYPE_WORKFLOW);
|
||||||
|
row.setActionScope(ACTION_SCOPE_INVOKE);
|
||||||
|
rows.add(row);
|
||||||
|
}
|
||||||
|
if (!rows.isEmpty()) {
|
||||||
|
mappingService.saveBatch(rows);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public SysApiKey assertWorkflowApi(String apiKey, String requestUri) {
|
||||||
|
SysApiKey sysApiKey = sysApiKeyService.getSysApiKey(apiKey);
|
||||||
|
SysApiKeyResource resource = getResource(requestUri);
|
||||||
|
QueryWrapper wrapper = QueryWrapper.create()
|
||||||
|
.eq(SysApiKeyResourceMapping::getApiKeyId, sysApiKey.getId())
|
||||||
|
.eq(SysApiKeyResourceMapping::getApiKeyResourceId, resource.getId())
|
||||||
|
.eq(SysApiKeyResourceMapping::getResourceType, RESOURCE_TYPE_WORKFLOW)
|
||||||
|
.isNull(SysApiKeyResourceMapping::getResourceTargetId)
|
||||||
|
.eq(SysApiKeyResourceMapping::getActionScope, ACTION_SCOPE_INVOKE);
|
||||||
|
if (mappingService.count(wrapper) == 0) {
|
||||||
|
throw new BusinessException("该apiKey无权限调用工作流 API");
|
||||||
|
}
|
||||||
|
return sysApiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取工作流 Public API 资源。
|
||||||
|
*
|
||||||
|
* @param requestInterface 请求地址
|
||||||
|
* @return API 资源
|
||||||
|
*/
|
||||||
|
private SysApiKeyResource getResource(String requestInterface) {
|
||||||
|
QueryWrapper wrapper = QueryWrapper.create()
|
||||||
|
.eq(SysApiKeyResource::getRequestInterface, requestInterface);
|
||||||
|
SysApiKeyResource resource = resourceService.getOne(wrapper);
|
||||||
|
if (resource == null) {
|
||||||
|
throw new BusinessException("该接口不存在");
|
||||||
|
}
|
||||||
|
return resource;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确保工作流 Public API 资源已存在。
|
||||||
|
*
|
||||||
|
* @param requestInterface 请求地址
|
||||||
|
* @return API 资源
|
||||||
|
*/
|
||||||
|
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(RESOURCE_TITLE);
|
||||||
|
resourceService.save(resource);
|
||||||
|
return resource;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,7 +12,9 @@ import java.math.BigInteger;
|
|||||||
public class WorkFlowUtil {
|
public class WorkFlowUtil {
|
||||||
|
|
||||||
public final static String USER_KEY = "user";
|
public final static String USER_KEY = "user";
|
||||||
|
public final static String API_KEY = "API_KEY";
|
||||||
public final static String WORKFLOW_KEY = "workflow";
|
public final static String WORKFLOW_KEY = "workflow";
|
||||||
|
public final static String CREATED_KEY_MEMORY_KEY = "workflowCreatedKey";
|
||||||
|
|
||||||
public static String removeSensitiveInfo(String originJson) {
|
public static String removeSensitiveInfo(String originJson) {
|
||||||
JSONObject workflowInfo = JSON.parseObject(originJson);
|
JSONObject workflowInfo = JSON.parseObject(originJson);
|
||||||
@@ -35,6 +37,17 @@ public class WorkFlowUtil {
|
|||||||
return cache == null ? defaultAccount() : (LoginAccount) cache;
|
return cache == null ? defaultAccount() : (LoginAccount) cache;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取工作流执行记录的执行人标识。
|
||||||
|
*
|
||||||
|
* @param chain 当前工作流执行链
|
||||||
|
* @return 执行人标识
|
||||||
|
*/
|
||||||
|
public static String getCreatedKey(Chain chain) {
|
||||||
|
Object value = chain.getState().getMemory().get(CREATED_KEY_MEMORY_KEY);
|
||||||
|
return value == null ? USER_KEY : String.valueOf(value);
|
||||||
|
}
|
||||||
|
|
||||||
public static LoginAccount defaultAccount() {
|
public static LoginAccount defaultAccount() {
|
||||||
LoginAccount account = new LoginAccount();
|
LoginAccount account = new LoginAccount();
|
||||||
account.setId(new BigInteger("0"));
|
account.setId(new BigInteger("0"));
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ public class SysApiKey extends SysApiKeyBase {
|
|||||||
@Column(ignore = true)
|
@Column(ignore = true)
|
||||||
private Boolean knowledgeShareEnabled;
|
private Boolean knowledgeShareEnabled;
|
||||||
|
|
||||||
|
@Column(ignore = true)
|
||||||
|
private Boolean workflowApiEnabled;
|
||||||
|
|
||||||
@RelationOneToMany(selfField = "id", targetField = "apiKeyId", targetTable = "tb_sys_api_key_resource_mapping")
|
@RelationOneToMany(selfField = "id", targetField = "apiKeyId", targetTable = "tb_sys_api_key_resource_mapping")
|
||||||
private List<SysApiKeyResourceMapping> resourcePermissions;
|
private List<SysApiKeyResourceMapping> resourcePermissions;
|
||||||
|
|
||||||
@@ -52,4 +55,12 @@ public class SysApiKey extends SysApiKeyBase {
|
|||||||
public void setKnowledgeShareEnabled(Boolean knowledgeShareEnabled) {
|
public void setKnowledgeShareEnabled(Boolean knowledgeShareEnabled) {
|
||||||
this.knowledgeShareEnabled = knowledgeShareEnabled;
|
this.knowledgeShareEnabled = knowledgeShareEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Boolean getWorkflowApiEnabled() {
|
||||||
|
return workflowApiEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setWorkflowApiEnabled(Boolean workflowApiEnabled) {
|
||||||
|
this.workflowApiEnabled = workflowApiEnabled;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
INSERT INTO `tb_sys_api_key_resource` (`id`, `request_interface`, `title`)
|
||||||
|
VALUES
|
||||||
|
(366700000000000003, '/public-api/workflow/getByIdOrAlias', '工作流 API 调用'),
|
||||||
|
(366700000000000004, '/public-api/workflow/getRunningParameters', '工作流 API 调用'),
|
||||||
|
(366700000000000005, '/public-api/workflow/runAsync', '工作流 API 调用'),
|
||||||
|
(366700000000000006, '/public-api/workflow/getChainStatus', '工作流 API 调用'),
|
||||||
|
(366700000000000007, '/public-api/workflow/resume', '工作流 API 调用')
|
||||||
|
ON DUPLICATE KEY UPDATE `title` = VALUES(`title`);
|
||||||
@@ -80,6 +80,15 @@
|
|||||||
"publishStatusOffline": "Offline",
|
"publishStatusOffline": "Offline",
|
||||||
"publishStatusDeletePending": "Delete Pending",
|
"publishStatusDeletePending": "Delete Pending",
|
||||||
"publishStatusLabel": "Release",
|
"publishStatusLabel": "Release",
|
||||||
|
"apiInstruction": "API Instructions",
|
||||||
|
"apiInstructionPublishRequired": "Publish the workflow before viewing API instructions",
|
||||||
|
"apiEndpoint": "Endpoint",
|
||||||
|
"apiRequestExample": "Request Example",
|
||||||
|
"apiResponseExample": "Response Example",
|
||||||
|
"apiVariables": "Variables",
|
||||||
|
"apiVariablesEmpty": "This workflow has no start parameters",
|
||||||
|
"apiStatusExample": "Status Query Example",
|
||||||
|
"apiResumeExample": "Resume Example",
|
||||||
"submitPublishApprovalConfirm": "Publish the current workflow now?",
|
"submitPublishApprovalConfirm": "Publish the current workflow now?",
|
||||||
"submitRepublishApprovalConfirm": "Republish the current workflow now?",
|
"submitRepublishApprovalConfirm": "Republish the current workflow now?",
|
||||||
"submitOfflineApprovalConfirm": "Take the current workflow offline?",
|
"submitOfflineApprovalConfirm": "Take the current workflow offline?",
|
||||||
|
|||||||
@@ -15,5 +15,6 @@
|
|||||||
},
|
},
|
||||||
"permissions": "AuthInterface",
|
"permissions": "AuthInterface",
|
||||||
"knowledgeSharePermission": "Knowledge Share",
|
"knowledgeSharePermission": "Knowledge Share",
|
||||||
|
"workflowApiPermission": "Workflow API",
|
||||||
"addApiKeyNotice": "This operation will generate an API key. Please confirm whether to proceed"
|
"addApiKeyNotice": "This operation will generate an API key. Please confirm whether to proceed"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,6 +80,15 @@
|
|||||||
"publishStatusOffline": "已下线",
|
"publishStatusOffline": "已下线",
|
||||||
"publishStatusDeletePending": "删除审批中",
|
"publishStatusDeletePending": "删除审批中",
|
||||||
"publishStatusLabel": "发布状态",
|
"publishStatusLabel": "发布状态",
|
||||||
|
"apiInstruction": "接口调用说明",
|
||||||
|
"apiInstructionPublishRequired": "发布后可获取 API 调用说明",
|
||||||
|
"apiEndpoint": "调用地址",
|
||||||
|
"apiRequestExample": "请求示例",
|
||||||
|
"apiResponseExample": "返回示例",
|
||||||
|
"apiVariables": "入参说明",
|
||||||
|
"apiVariablesEmpty": "当前工作流没有开始参数",
|
||||||
|
"apiStatusExample": "状态查询示例",
|
||||||
|
"apiResumeExample": "恢复执行示例",
|
||||||
"submitPublishApprovalConfirm": "确认发布当前工作流吗?",
|
"submitPublishApprovalConfirm": "确认发布当前工作流吗?",
|
||||||
"submitRepublishApprovalConfirm": "确认重新发布当前工作流吗?",
|
"submitRepublishApprovalConfirm": "确认重新发布当前工作流吗?",
|
||||||
"submitOfflineApprovalConfirm": "确认下线当前工作流吗?",
|
"submitOfflineApprovalConfirm": "确认下线当前工作流吗?",
|
||||||
|
|||||||
@@ -15,5 +15,6 @@
|
|||||||
},
|
},
|
||||||
"permissions": "授权接口",
|
"permissions": "授权接口",
|
||||||
"knowledgeSharePermission": "知识库分享授权",
|
"knowledgeSharePermission": "知识库分享授权",
|
||||||
|
"workflowApiPermission": "工作流 API 调用授权",
|
||||||
"addApiKeyNotice": "该操作会生成一个apiKey,请确认是否生成"
|
"addApiKeyNotice": "该操作会生成一个apiKey,请确认是否生成"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,11 +5,15 @@ import type {
|
|||||||
ActionButton,
|
ActionButton,
|
||||||
CardPrimaryAction,
|
CardPrimaryAction,
|
||||||
} from '#/components/page/CardList.vue';
|
} from '#/components/page/CardList.vue';
|
||||||
|
import type { OfflineImpactCheck } from '#/views/ai/shared/offline-impact';
|
||||||
|
|
||||||
import { computed, h, markRaw, onMounted, ref } from 'vue';
|
import { computed, h, markRaw, onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import ElXMarkdown from 'vue-element-plus-x/es/XMarkdown/index.js';
|
||||||
|
|
||||||
import { useAccess } from '@easyflow/access';
|
import { useAccess } from '@easyflow/access';
|
||||||
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
||||||
|
import { useAppConfig } from '@easyflow/hooks';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Check,
|
Check,
|
||||||
@@ -17,6 +21,7 @@ import {
|
|||||||
Delete,
|
Delete,
|
||||||
Download,
|
Download,
|
||||||
Edit,
|
Edit,
|
||||||
|
Link,
|
||||||
Lock,
|
Lock,
|
||||||
OfficeBuilding,
|
OfficeBuilding,
|
||||||
Plus,
|
Plus,
|
||||||
@@ -26,6 +31,7 @@ import {
|
|||||||
VideoPlay,
|
VideoPlay,
|
||||||
} from '@element-plus/icons-vue';
|
} from '@element-plus/icons-vue';
|
||||||
import {
|
import {
|
||||||
|
ElDialog,
|
||||||
ElForm,
|
ElForm,
|
||||||
ElFormItem,
|
ElFormItem,
|
||||||
ElIcon,
|
ElIcon,
|
||||||
@@ -49,10 +55,7 @@ import { $t } from '#/locales';
|
|||||||
import { router } from '#/router';
|
import { router } from '#/router';
|
||||||
import { useDictStore } from '#/store';
|
import { useDictStore } from '#/store';
|
||||||
import AiResourceCornerMeta from '#/views/ai/shared/AiResourceCornerMeta.vue';
|
import AiResourceCornerMeta from '#/views/ai/shared/AiResourceCornerMeta.vue';
|
||||||
import {
|
import { buildOfflineImpactMessage } from '#/views/ai/shared/offline-impact';
|
||||||
buildOfflineImpactMessage,
|
|
||||||
type OfflineImpactCheck,
|
|
||||||
} from '#/views/ai/shared/offline-impact';
|
|
||||||
import {
|
import {
|
||||||
canAiResourceDelete,
|
canAiResourceDelete,
|
||||||
canAiResourceOffline,
|
canAiResourceOffline,
|
||||||
@@ -64,6 +67,8 @@ import {
|
|||||||
|
|
||||||
import WorkflowModal from './WorkflowModal.vue';
|
import WorkflowModal from './WorkflowModal.vue';
|
||||||
|
|
||||||
|
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
|
||||||
|
|
||||||
interface FieldDefinition {
|
interface FieldDefinition {
|
||||||
// 字段名称
|
// 字段名称
|
||||||
prop: string;
|
prop: string;
|
||||||
@@ -79,6 +84,15 @@ interface FieldDefinition {
|
|||||||
|
|
||||||
type VisibilityScope = 'DEPT' | 'PRIVATE' | 'PUBLIC';
|
type VisibilityScope = 'DEPT' | 'PRIVATE' | 'PUBLIC';
|
||||||
|
|
||||||
|
interface ApiFieldDoc {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
type: string;
|
||||||
|
required: boolean;
|
||||||
|
description: string;
|
||||||
|
placeholder: string;
|
||||||
|
}
|
||||||
|
|
||||||
const primaryAction: CardPrimaryAction = {
|
const primaryAction: CardPrimaryAction = {
|
||||||
icon: DesignIcon,
|
icon: DesignIcon,
|
||||||
text: $t('button.design'),
|
text: $t('button.design'),
|
||||||
@@ -94,6 +108,8 @@ const canManageWorkflow = computed(() =>
|
|||||||
);
|
);
|
||||||
const updatingScopeId = ref<null | number | string>(null);
|
const updatingScopeId = ref<null | number | string>(null);
|
||||||
const visibilityScopePopoverRefs = ref<Record<string, any>>({});
|
const visibilityScopePopoverRefs = ref<Record<string, any>>({});
|
||||||
|
const apiInstructionVisible = ref(false);
|
||||||
|
const apiInstructionRow = ref<any>(null);
|
||||||
const visibilityScopeMeta = computed(() => ({
|
const visibilityScopeMeta = computed(() => ({
|
||||||
PRIVATE: {
|
PRIVATE: {
|
||||||
label: $t('aiWorkflow.visibilityScopePrivate'),
|
label: $t('aiWorkflow.visibilityScopePrivate'),
|
||||||
@@ -158,6 +174,14 @@ const actions: ActionButton[] = [
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
icon: Link,
|
||||||
|
text: $t('aiWorkflow.apiInstruction'),
|
||||||
|
placement: 'menu',
|
||||||
|
onClick: (row: any) => {
|
||||||
|
showApiInstruction(row);
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
icon: Download,
|
icon: Download,
|
||||||
text: $t('button.export'),
|
text: $t('button.export'),
|
||||||
@@ -199,7 +223,8 @@ const actions: ActionButton[] = [
|
|||||||
text: $t('button.offline'),
|
text: $t('button.offline'),
|
||||||
permission: '/api/v1/workflow/save',
|
permission: '/api/v1/workflow/save',
|
||||||
placement: 'menu',
|
placement: 'menu',
|
||||||
visible: (row: any) => canAiResourceOffline(row.displayPublishStatus, row.publishStatus),
|
visible: (row: any) =>
|
||||||
|
canAiResourceOffline(row.displayPublishStatus, row.publishStatus),
|
||||||
onClick: (row: any) => {
|
onClick: (row: any) => {
|
||||||
submitOfflineAction(row);
|
submitOfflineAction(row);
|
||||||
},
|
},
|
||||||
@@ -210,7 +235,8 @@ const actions: ActionButton[] = [
|
|||||||
tone: 'danger',
|
tone: 'danger',
|
||||||
permission: '/api/v1/workflow/remove',
|
permission: '/api/v1/workflow/remove',
|
||||||
placement: 'menu',
|
placement: 'menu',
|
||||||
visible: (row: any) => canAiResourceDelete(row.displayPublishStatus, row.publishStatus),
|
visible: (row: any) =>
|
||||||
|
canAiResourceDelete(row.displayPublishStatus, row.publishStatus),
|
||||||
onClick: (row: any) => {
|
onClick: (row: any) => {
|
||||||
submitDeleteApproval(row);
|
submitDeleteApproval(row);
|
||||||
},
|
},
|
||||||
@@ -306,6 +332,321 @@ function resolveNavTitle(row: any) {
|
|||||||
function isRepublishAction(row: any) {
|
function isRepublishAction(row: any) {
|
||||||
return canAiResourceRepublish(row.displayPublishStatus, row.publishStatus);
|
return canAiResourceRepublish(row.displayPublishStatus, row.publishStatus);
|
||||||
}
|
}
|
||||||
|
function isWorkflowPublished(row: any) {
|
||||||
|
return (
|
||||||
|
resolveAiResourceDisplayStatus(
|
||||||
|
row.displayPublishStatus,
|
||||||
|
row.publishStatus,
|
||||||
|
) === 'PUBLISHED'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function showApiInstruction(row: any) {
|
||||||
|
if (!isWorkflowPublished(row) || !getPublishedWorkflowContent(row)) {
|
||||||
|
ElMessage.warning($t('aiWorkflow.apiInstructionPublishRequired'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
apiInstructionRow.value = row;
|
||||||
|
apiInstructionVisible.value = true;
|
||||||
|
}
|
||||||
|
function getPublishedWorkflowContent(row: any) {
|
||||||
|
const snapshot = row?.publishedSnapshotJson;
|
||||||
|
if (snapshot && typeof snapshot === 'object' && snapshot.content) {
|
||||||
|
return snapshot.content;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
function parseWorkflowContent(row: any) {
|
||||||
|
const content = getPublishedWorkflowContent(row);
|
||||||
|
if (!content) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (typeof content === 'object') {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JSON.parse(String(content));
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function normalizeApiOrigin(value?: string) {
|
||||||
|
const rawValue = String(value || '').trim();
|
||||||
|
if (!rawValue) {
|
||||||
|
return window.location.origin;
|
||||||
|
}
|
||||||
|
if (/^https?:\/\//i.test(rawValue)) {
|
||||||
|
return rawValue.replace(/\/+$/, '');
|
||||||
|
}
|
||||||
|
const normalizedPath = rawValue.startsWith('/') ? rawValue : `/${rawValue}`;
|
||||||
|
return `${window.location.origin}${normalizedPath}`.replace(/\/+$/, '');
|
||||||
|
}
|
||||||
|
function resolveApiBaseUrl() {
|
||||||
|
return `${normalizeApiOrigin(apiURL)}/public-api/workflow`;
|
||||||
|
}
|
||||||
|
function resolveStartNodeData(row: any) {
|
||||||
|
const workflow = parseWorkflowContent(row);
|
||||||
|
const nodes = Array.isArray(workflow?.nodes) ? workflow.nodes : [];
|
||||||
|
const startNode = nodes.find((node: any) => node?.type === 'startNode');
|
||||||
|
return startNode?.data || {};
|
||||||
|
}
|
||||||
|
function resolveApiFields(row: any): ApiFieldDoc[] {
|
||||||
|
const startNodeData = resolveStartNodeData(row);
|
||||||
|
const schema = Array.isArray(startNodeData.startFormSchema)
|
||||||
|
? startNodeData.startFormSchema
|
||||||
|
: [];
|
||||||
|
const parameters = Array.isArray(startNodeData.parameters)
|
||||||
|
? startNodeData.parameters
|
||||||
|
: [];
|
||||||
|
const source = schema.length > 0 ? schema : parameters;
|
||||||
|
return source
|
||||||
|
.map((item: any) => {
|
||||||
|
const key = String(item?.key || item?.name || '').trim();
|
||||||
|
if (!key) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
label: String(item?.label || item?.title || item?.name || key),
|
||||||
|
type: String(item?.type || item?.contentType || 'text'),
|
||||||
|
required: Boolean(item?.required),
|
||||||
|
description: String(item?.description || item?.formDescription || ''),
|
||||||
|
placeholder: String(item?.placeholder || item?.formPlaceholder || ''),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter(Boolean) as ApiFieldDoc[];
|
||||||
|
}
|
||||||
|
function buildExampleVariables(row: any) {
|
||||||
|
const variables: Record<string, any> = {};
|
||||||
|
for (const field of resolveApiFields(row)) {
|
||||||
|
if (field.type === 'file') {
|
||||||
|
variables[field.key] = [
|
||||||
|
{
|
||||||
|
fileName: 'example.pdf',
|
||||||
|
filePath: 'https://example.com/example.pdf',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (field.type === 'checkbox') {
|
||||||
|
variables[field.key] = [];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
variables[field.key] = field.placeholder || field.label;
|
||||||
|
}
|
||||||
|
return variables;
|
||||||
|
}
|
||||||
|
function buildRunRequestExample(row: any) {
|
||||||
|
return JSON.stringify(
|
||||||
|
{
|
||||||
|
id: row?.id,
|
||||||
|
variables: buildExampleVariables(row),
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function buildRunResponseExample() {
|
||||||
|
return JSON.stringify(
|
||||||
|
{
|
||||||
|
errorCode: 0,
|
||||||
|
message: '成功',
|
||||||
|
data: '执行ID',
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function buildResumeRequestExample() {
|
||||||
|
return JSON.stringify(
|
||||||
|
{
|
||||||
|
executeId: '执行ID',
|
||||||
|
confirmParams: {
|
||||||
|
confirm: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
async function copyApiContent(content: string) {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(content);
|
||||||
|
ElMessage.success($t('message.copySuccess'));
|
||||||
|
} catch {
|
||||||
|
ElMessage.error($t('message.copyFail'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function handleApiDocClick(e: MouseEvent) {
|
||||||
|
const target = (e.target as HTMLElement).closest('.api-url-copy-btn');
|
||||||
|
if (!target) return;
|
||||||
|
const url = (target as HTMLElement).dataset.copy;
|
||||||
|
if (url) copyApiContent(url);
|
||||||
|
}
|
||||||
|
function apiUrlLine(method: string, url: string) {
|
||||||
|
const escaped = url.replace(/"/g, '"');
|
||||||
|
return `\`${method}\` \`${url}\` <button class="api-url-copy-btn" data-copy="${escaped}" title="复制">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg></button>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiDocMarkdown = computed(() => {
|
||||||
|
const row = apiInstructionRow.value;
|
||||||
|
if (!row) return '';
|
||||||
|
|
||||||
|
const baseUrl = resolveApiBaseUrl();
|
||||||
|
const fields = resolveApiFields(row);
|
||||||
|
|
||||||
|
const lines: string[] = [];
|
||||||
|
|
||||||
|
// ---- 概述 ----
|
||||||
|
lines.push(`## 概述`);
|
||||||
|
lines.push(``);
|
||||||
|
lines.push(`通过 Public API 可异步执行已发布的工作流并获取执行结果。`);
|
||||||
|
lines.push(``);
|
||||||
|
lines.push(`**调用流程**:发起执行 → 轮询查询执行结果 → (若有确认节点)恢复执行`);
|
||||||
|
lines.push(``);
|
||||||
|
|
||||||
|
// ---- 鉴权 ----
|
||||||
|
lines.push(`## 鉴权`);
|
||||||
|
lines.push(``);
|
||||||
|
lines.push(`所有请求需在 Header 中携带访问令牌,访问令牌需开启 **工作流 API 调用授权**。`);
|
||||||
|
lines.push(``);
|
||||||
|
lines.push('```');
|
||||||
|
lines.push(`ApiKey: <your-api-key>`);
|
||||||
|
lines.push('```');
|
||||||
|
lines.push(``);
|
||||||
|
lines.push(`---`);
|
||||||
|
lines.push(``);
|
||||||
|
|
||||||
|
// ---- 1. 发起执行 ----
|
||||||
|
lines.push(`## 1. 发起执行`);
|
||||||
|
lines.push(``);
|
||||||
|
lines.push(apiUrlLine('POST', `${baseUrl}/runAsync`));
|
||||||
|
lines.push(``);
|
||||||
|
lines.push(`异步执行工作流,立即返回执行 ID。工作流必须已发布且存在发布快照。`);
|
||||||
|
lines.push(``);
|
||||||
|
lines.push(`### 请求体`);
|
||||||
|
lines.push(``);
|
||||||
|
lines.push('```json');
|
||||||
|
lines.push(buildRunRequestExample(row));
|
||||||
|
lines.push('```');
|
||||||
|
lines.push(``);
|
||||||
|
|
||||||
|
// 入参说明
|
||||||
|
lines.push(`### 入参说明`);
|
||||||
|
lines.push(``);
|
||||||
|
lines.push(`| 参数 | 类型 | 必填 | 说明 |`);
|
||||||
|
lines.push(`| --- | --- | --- | --- |`);
|
||||||
|
lines.push(`| id | string | 是 | 工作流 ID |`);
|
||||||
|
lines.push(`| variables | object | 否 | 运行参数,字段说明见下方 |`);
|
||||||
|
lines.push(``);
|
||||||
|
|
||||||
|
if (fields.length > 0) {
|
||||||
|
lines.push(`#### variables 字段明细`);
|
||||||
|
lines.push(``);
|
||||||
|
lines.push(`以下字段基于当前发布快照的开始节点配置生成。`);
|
||||||
|
lines.push(``);
|
||||||
|
lines.push(`| 参数 | 类型 | 必填 | 说明 |`);
|
||||||
|
lines.push(`| --- | --- | --- | --- |`);
|
||||||
|
for (const f of fields) {
|
||||||
|
const desc = [f.label, f.description].filter(Boolean).join(' · ');
|
||||||
|
lines.push(`| ${f.key} | ${f.type} | ${f.required ? '是' : '否'} | ${desc} |`);
|
||||||
|
}
|
||||||
|
lines.push(``);
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push(`### 响应`);
|
||||||
|
lines.push(``);
|
||||||
|
lines.push('```json');
|
||||||
|
lines.push(buildRunResponseExample());
|
||||||
|
lines.push('```');
|
||||||
|
lines.push(``);
|
||||||
|
lines.push(`\`data\` 为 **执行 ID**(executeId),后续查询和恢复均需使用此 ID。`);
|
||||||
|
lines.push(``);
|
||||||
|
lines.push(`---`);
|
||||||
|
lines.push(``);
|
||||||
|
|
||||||
|
// ---- 2. 查询执行结果 ----
|
||||||
|
lines.push(`## 2. 查询执行结果`);
|
||||||
|
lines.push(``);
|
||||||
|
lines.push(apiUrlLine('POST', `${baseUrl}/getChainStatus`));
|
||||||
|
lines.push(``);
|
||||||
|
lines.push(`查询工作流执行的整体状态与各节点执行详情。工作流为异步执行,建议**轮询**该接口直到状态为终态。`);
|
||||||
|
lines.push(``);
|
||||||
|
lines.push(`### 请求体`);
|
||||||
|
lines.push(``);
|
||||||
|
lines.push('```json');
|
||||||
|
lines.push(JSON.stringify({ executeId: '<runAsync 返回的执行 ID>' }, null, 2));
|
||||||
|
lines.push('```');
|
||||||
|
lines.push(``);
|
||||||
|
lines.push(`> \`nodes\` 参数可选。不传则只返回工作流整体状态;传入节点 ID 数组可额外获取对应节点的执行详情。`);
|
||||||
|
lines.push(``);
|
||||||
|
lines.push(`### 响应示例`);
|
||||||
|
lines.push(``);
|
||||||
|
lines.push('```json');
|
||||||
|
lines.push(JSON.stringify({
|
||||||
|
errorCode: 0,
|
||||||
|
message: '成功',
|
||||||
|
data: {
|
||||||
|
executeId: 'abc5358c-a310-4caa-97ec-455062b2235e',
|
||||||
|
status: 'FINISHED',
|
||||||
|
message: null,
|
||||||
|
result: { output: '工作流执行结果' },
|
||||||
|
nodes: {},
|
||||||
|
},
|
||||||
|
}, null, 2));
|
||||||
|
lines.push('```');
|
||||||
|
lines.push(``);
|
||||||
|
lines.push(`### 状态值说明`);
|
||||||
|
lines.push(``);
|
||||||
|
lines.push(`| 状态 | 说明 |`);
|
||||||
|
lines.push(`| --- | --- |`);
|
||||||
|
lines.push(`| READY | 就绪,尚未开始 |`);
|
||||||
|
lines.push(`| RUNNING | 执行中 |`);
|
||||||
|
lines.push(`| SUSPEND | 挂起,等待确认节点恢复 |`);
|
||||||
|
lines.push(`| FINISHED | 执行完成 |`);
|
||||||
|
lines.push(`| FAILED | 执行失败 |`);
|
||||||
|
lines.push(`| ERROR | 执行异常 |`);
|
||||||
|
lines.push(``);
|
||||||
|
|
||||||
|
// ---- 3. 恢复执行 ----
|
||||||
|
lines.push(`---`);
|
||||||
|
lines.push(``);
|
||||||
|
lines.push(`## 3. 恢复执行(确认节点)`);
|
||||||
|
lines.push(``);
|
||||||
|
lines.push(apiUrlLine('POST', `${baseUrl}/resume`));
|
||||||
|
lines.push(``);
|
||||||
|
lines.push(`当工作流包含**确认节点**时,执行到该节点后状态变为 \`SUSPEND\`,需要调用此接口传入确认参数后恢复执行。若工作流不包含确认节点则无需调用。`);
|
||||||
|
lines.push(``);
|
||||||
|
lines.push(`### 请求体`);
|
||||||
|
lines.push(``);
|
||||||
|
lines.push('```json');
|
||||||
|
lines.push(buildResumeRequestExample());
|
||||||
|
lines.push('```');
|
||||||
|
lines.push(``);
|
||||||
|
lines.push(`### 响应`);
|
||||||
|
lines.push(``);
|
||||||
|
lines.push('```json');
|
||||||
|
lines.push(JSON.stringify({ errorCode: 0, message: '成功', data: null }, null, 2));
|
||||||
|
lines.push('```');
|
||||||
|
lines.push(``);
|
||||||
|
lines.push(`恢复后可继续轮询 \`getChainStatus\` 获取后续执行状态。`);
|
||||||
|
lines.push(``);
|
||||||
|
|
||||||
|
// ---- 错误码 ----
|
||||||
|
lines.push(`---`);
|
||||||
|
lines.push(``);
|
||||||
|
lines.push(`## 错误处理`);
|
||||||
|
lines.push(``);
|
||||||
|
lines.push(`当请求失败时,\`errorCode\` 不为 0,\`message\` 包含错误原因。常见错误:`);
|
||||||
|
lines.push(``);
|
||||||
|
lines.push(`| 场景 | 说明 |`);
|
||||||
|
lines.push(`| --- | --- |`);
|
||||||
|
lines.push(`| ApiKey 无效或过期 | 检查访问令牌状态与有效期 |`);
|
||||||
|
lines.push(`| 未授权工作流 API 调用 | 在访问令牌中开启「工作流 API 调用授权」 |`);
|
||||||
|
lines.push(`| 工作流尚未发布 | 仅已发布且存在发布快照的工作流可通过 API 调用 |`);
|
||||||
|
|
||||||
|
return lines.join('\n');
|
||||||
|
});
|
||||||
|
|
||||||
async function submitPublishAction(row: any) {
|
async function submitPublishAction(row: any) {
|
||||||
if (
|
if (
|
||||||
@@ -329,12 +670,9 @@ async function submitPublishAction(row: any) {
|
|||||||
} catch {
|
} catch {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const res = await api.post(
|
const res = await api.post('/api/v1/workflow/submitPublishApproval', {
|
||||||
'/api/v1/workflow/submitPublishApproval',
|
id: row.id,
|
||||||
{
|
});
|
||||||
id: row.id,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
if (res.errorCode === 0) {
|
if (res.errorCode === 0) {
|
||||||
ElMessage.success(res.message || $t('message.saveOkMessage'));
|
ElMessage.success(res.message || $t('message.saveOkMessage'));
|
||||||
pageDataRef.value?.reload?.();
|
pageDataRef.value?.reload?.();
|
||||||
@@ -350,17 +688,15 @@ async function submitOfflineAction(row: any) {
|
|||||||
const impactRes = await api.get<{
|
const impactRes = await api.get<{
|
||||||
data: OfflineImpactCheck;
|
data: OfflineImpactCheck;
|
||||||
errorCode: number;
|
errorCode: number;
|
||||||
}>(
|
}>('/api/v1/workflow/offlineImpactCheck', {
|
||||||
'/api/v1/workflow/offlineImpactCheck',
|
params: { id: row.id },
|
||||||
{
|
});
|
||||||
params: { id: row.id },
|
|
||||||
},
|
|
||||||
);
|
|
||||||
if (impactRes.errorCode !== 0) {
|
if (impactRes.errorCode !== 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const sections = [];
|
const sections = [];
|
||||||
|
let offlineImpactFooter = $t('aiWorkflow.offlineImpactBoundBotsFooter');
|
||||||
if (impactRes.data?.hasBotBindings) {
|
if (impactRes.data?.hasBotBindings) {
|
||||||
sections.push(
|
sections.push(
|
||||||
buildOfflineImpactMessage(
|
buildOfflineImpactMessage(
|
||||||
@@ -383,32 +719,23 @@ async function submitOfflineAction(row: any) {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (impactRes.data?.hasBotBindings && impactRes.data?.hasPluginBindings) {
|
||||||
|
offlineImpactFooter = $t('aiWorkflow.offlineImpactBoundMixedFooter');
|
||||||
|
} else if (impactRes.data?.hasPluginBindings) {
|
||||||
|
offlineImpactFooter = $t('aiWorkflow.offlineImpactBoundPluginsFooter');
|
||||||
|
}
|
||||||
const impactMessage =
|
const impactMessage =
|
||||||
sections.length > 0
|
sections.length > 0
|
||||||
? h('div', [
|
? h('div', [
|
||||||
...sections,
|
...sections,
|
||||||
h(
|
h('p', { style: 'margin-top: 12px;' }, offlineImpactFooter),
|
||||||
'p',
|
|
||||||
{
|
|
||||||
style: 'margin-top: 12px;',
|
|
||||||
},
|
|
||||||
impactRes.data?.hasBotBindings && impactRes.data?.hasPluginBindings
|
|
||||||
? $t('aiWorkflow.offlineImpactBoundMixedFooter')
|
|
||||||
: impactRes.data?.hasPluginBindings
|
|
||||||
? $t('aiWorkflow.offlineImpactBoundPluginsFooter')
|
|
||||||
: $t('aiWorkflow.offlineImpactBoundBotsFooter'),
|
|
||||||
),
|
|
||||||
])
|
])
|
||||||
: $t('aiWorkflow.submitOfflineApprovalConfirm');
|
: $t('aiWorkflow.submitOfflineApprovalConfirm');
|
||||||
await ElMessageBox.confirm(
|
await ElMessageBox.confirm(impactMessage, $t('message.noticeTitle'), {
|
||||||
impactMessage,
|
confirmButtonText: $t('button.confirm'),
|
||||||
$t('message.noticeTitle'),
|
cancelButtonText: $t('button.cancel'),
|
||||||
{
|
type: 'warning',
|
||||||
confirmButtonText: $t('button.confirm'),
|
});
|
||||||
cancelButtonText: $t('button.cancel'),
|
|
||||||
type: 'warning',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} catch {
|
} catch {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -459,18 +786,18 @@ function resolvePublishStatusMetaByInstance(
|
|||||||
tone: 'danger',
|
tone: 'danger',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case 'OFFLINE_PENDING': {
|
|
||||||
return {
|
|
||||||
label: $t('aiWorkflow.publishStatusOfflinePending'),
|
|
||||||
tone: 'pending',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case 'OFFLINE': {
|
case 'OFFLINE': {
|
||||||
return {
|
return {
|
||||||
label: $t('aiWorkflow.publishStatusOffline'),
|
label: $t('aiWorkflow.publishStatusOffline'),
|
||||||
tone: 'draft',
|
tone: 'draft',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case 'OFFLINE_PENDING': {
|
||||||
|
return {
|
||||||
|
label: $t('aiWorkflow.publishStatusOfflinePending'),
|
||||||
|
tone: 'pending',
|
||||||
|
};
|
||||||
|
}
|
||||||
case 'PUBLISH_PENDING': {
|
case 'PUBLISH_PENDING': {
|
||||||
return {
|
return {
|
||||||
label: $t('aiWorkflow.publishStatusPublishPending'),
|
label: $t('aiWorkflow.publishStatusPublishPending'),
|
||||||
@@ -668,6 +995,25 @@ function handleHeaderButtonClick(data: any) {
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex h-full flex-col gap-6 p-6">
|
<div class="flex h-full flex-col gap-6 p-6">
|
||||||
<WorkflowModal ref="saveDialog" @reload="reset" />
|
<WorkflowModal ref="saveDialog" @reload="reset" />
|
||||||
|
<ElDialog
|
||||||
|
v-model="apiInstructionVisible"
|
||||||
|
:title="$t('aiWorkflow.apiInstruction')"
|
||||||
|
width="780px"
|
||||||
|
class="workflow-api-dialog"
|
||||||
|
:footer="false"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="apiInstructionRow"
|
||||||
|
class="workflow-api-markdown-wrap"
|
||||||
|
@click="handleApiDocClick"
|
||||||
|
>
|
||||||
|
<ElXMarkdown
|
||||||
|
:markdown="apiDocMarkdown"
|
||||||
|
:allow-html="true"
|
||||||
|
:sanitize="false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ElDialog>
|
||||||
<HeaderSearch
|
<HeaderSearch
|
||||||
:buttons="headerButtons"
|
:buttons="headerButtons"
|
||||||
@search="handleSearch"
|
@search="handleSearch"
|
||||||
@@ -705,7 +1051,10 @@ function handleHeaderButtonClick(data: any) {
|
|||||||
>
|
>
|
||||||
<span class="workflow-publish-chip__dot"></span>
|
<span class="workflow-publish-chip__dot"></span>
|
||||||
<span>{{
|
<span>{{
|
||||||
resolvePublishStatusMetaByInstance(item.displayPublishStatus, item.publishStatus).label
|
resolvePublishStatusMetaByInstance(
|
||||||
|
item.displayPublishStatus,
|
||||||
|
item.publishStatus,
|
||||||
|
).label
|
||||||
}}</span>
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -1102,4 +1451,153 @@ button.workflow-scope-chip:disabled {
|
|||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
box-shadow: 0 18px 34px -28px hsl(var(--foreground) / 20%);
|
box-shadow: 0 18px 34px -28px hsl(var(--foreground) / 20%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.workflow-api-markdown-wrap {
|
||||||
|
max-height: 65vh;
|
||||||
|
padding: 0 4px;
|
||||||
|
overflow-y: auto;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.7;
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-api-markdown-wrap::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-api-markdown-wrap::-webkit-scrollbar-thumb {
|
||||||
|
background-color: hsl(var(--muted-foreground) / 20%);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-api-markdown-wrap :deep(h2) {
|
||||||
|
padding-bottom: 8px;
|
||||||
|
margin-top: 28px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
border-bottom: 1px solid hsl(var(--border));
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-api-markdown-wrap :deep(h2:first-child) {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-api-markdown-wrap :deep(h3) {
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-api-markdown-wrap :deep(h4) {
|
||||||
|
margin-top: 16px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-api-markdown-wrap :deep(hr) {
|
||||||
|
margin: 24px 0;
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid hsl(var(--border));
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-api-markdown-wrap :deep(p) {
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-api-markdown-wrap :deep(code) {
|
||||||
|
padding: 2px 6px;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
background: hsl(var(--muted) / 50%);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-api-markdown-wrap :deep(pre) {
|
||||||
|
max-height: 280px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
margin: 10px 0;
|
||||||
|
overflow: auto;
|
||||||
|
background: hsl(220 14% 96%);
|
||||||
|
border: 1px solid hsl(var(--border));
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-api-markdown-wrap :deep(pre code) {
|
||||||
|
padding: 0;
|
||||||
|
font-size: 12.5px;
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
background: transparent;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-api-markdown-wrap :deep(table) {
|
||||||
|
width: 100%;
|
||||||
|
margin: 10px 0;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-api-markdown-wrap :deep(th),
|
||||||
|
.workflow-api-markdown-wrap :deep(td) {
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
text-align: left;
|
||||||
|
border: 1px solid hsl(var(--border));
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-api-markdown-wrap :deep(th) {
|
||||||
|
font-weight: 600;
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
background: hsl(var(--muted) / 40%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-api-markdown-wrap :deep(td) {
|
||||||
|
color: hsl(var(--foreground) / 85%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-api-markdown-wrap :deep(blockquote) {
|
||||||
|
padding: 8px 16px;
|
||||||
|
margin: 10px 0;
|
||||||
|
color: hsl(var(--muted-foreground));
|
||||||
|
border-left: 3px solid hsl(var(--primary) / 40%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-api-markdown-wrap :deep(strong) {
|
||||||
|
font-weight: 600;
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-api-markdown-wrap :deep(.api-url-copy-btn) {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
padding: 0;
|
||||||
|
margin-left: 6px;
|
||||||
|
color: hsl(var(--muted-foreground));
|
||||||
|
vertical-align: middle;
|
||||||
|
cursor: pointer;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid hsl(var(--border));
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-api-markdown-wrap :deep(.api-url-copy-btn:hover) {
|
||||||
|
color: hsl(var(--primary));
|
||||||
|
background: hsl(var(--muted) / 50%);
|
||||||
|
border-color: hsl(var(--primary) / 40%);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.workflow-api-dialog .el-dialog__body) {
|
||||||
|
padding: 0 20px 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ interface Entity {
|
|||||||
expiredAt: Date | null | string;
|
expiredAt: Date | null | string;
|
||||||
permissionIds: (number | string)[]; // 绑定值:权限 ID 数组
|
permissionIds: (number | string)[]; // 绑定值:权限 ID 数组
|
||||||
knowledgeShareEnabled: boolean;
|
knowledgeShareEnabled: boolean;
|
||||||
|
workflowApiEnabled: boolean;
|
||||||
id?: number; // 编辑时的主键
|
id?: number; // 编辑时的主键
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,6 +52,7 @@ const entity = ref<Entity>({
|
|||||||
expiredAt: null,
|
expiredAt: null,
|
||||||
permissionIds: [],
|
permissionIds: [],
|
||||||
knowledgeShareEnabled: false,
|
knowledgeShareEnabled: false,
|
||||||
|
workflowApiEnabled: false,
|
||||||
});
|
});
|
||||||
// 加载状态
|
// 加载状态
|
||||||
const btnLoading = ref(false);
|
const btnLoading = ref(false);
|
||||||
@@ -120,6 +122,7 @@ function getResourcePermissionList() {
|
|||||||
function createDefaultEntity(row: Partial<Entity> = {}): Entity {
|
function createDefaultEntity(row: Partial<Entity> = {}): Entity {
|
||||||
const permissionIds = row.permissionIds || [];
|
const permissionIds = row.permissionIds || [];
|
||||||
const knowledgeShareEnabled = Boolean(row.knowledgeShareEnabled);
|
const knowledgeShareEnabled = Boolean(row.knowledgeShareEnabled);
|
||||||
|
const workflowApiEnabled = Boolean(row.workflowApiEnabled);
|
||||||
return {
|
return {
|
||||||
apiKey: '',
|
apiKey: '',
|
||||||
status: '',
|
status: '',
|
||||||
@@ -128,6 +131,7 @@ function createDefaultEntity(row: Partial<Entity> = {}): Entity {
|
|||||||
...row,
|
...row,
|
||||||
permissionIds,
|
permissionIds,
|
||||||
knowledgeShareEnabled,
|
knowledgeShareEnabled,
|
||||||
|
workflowApiEnabled,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,6 +187,7 @@ function closeDialog() {
|
|||||||
expiredAt: null,
|
expiredAt: null,
|
||||||
permissionIds: [],
|
permissionIds: [],
|
||||||
knowledgeShareEnabled: false,
|
knowledgeShareEnabled: false,
|
||||||
|
workflowApiEnabled: false,
|
||||||
};
|
};
|
||||||
isAdd.value = true;
|
isAdd.value = true;
|
||||||
dialogVisible.value = false;
|
dialogVisible.value = false;
|
||||||
@@ -261,6 +266,12 @@ defineExpose({
|
|||||||
>
|
>
|
||||||
{{ $t('sysApiKey.knowledgeSharePermission') }}
|
{{ $t('sysApiKey.knowledgeSharePermission') }}
|
||||||
</ElCheckbox>
|
</ElCheckbox>
|
||||||
|
<ElCheckbox
|
||||||
|
v-model="entity.workflowApiEnabled"
|
||||||
|
class="permission-checkbox"
|
||||||
|
>
|
||||||
|
{{ $t('sysApiKey.workflowApiPermission') }}
|
||||||
|
</ElCheckbox>
|
||||||
</div>
|
</div>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</ElForm>
|
</ElForm>
|
||||||
|
|||||||
Reference in New Issue
Block a user