diff --git a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/system/SysApiKeyController.java b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/system/SysApiKeyController.java
index 3a007c5..6f9fa8e 100644
--- a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/system/SysApiKeyController.java
+++ b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/system/SysApiKeyController.java
@@ -11,6 +11,7 @@ import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import tech.easyflow.ai.service.KnowledgeSharePermissionService;
+import tech.easyflow.ai.service.WorkflowApiPermissionService;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.satoken.util.SaTokenUtil;
@@ -46,6 +47,8 @@ public class SysApiKeyController extends BaseCurdController 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);
}
}
diff --git a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/system/SysApiKeyResourceController.java b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/system/SysApiKeyResourceController.java
index 00caa48..91b6c20 100644
--- a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/system/SysApiKeyResourceController.java
+++ b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/system/SysApiKeyResourceController.java
@@ -1,12 +1,17 @@
package tech.easyflow.admin.controller.system;
+import com.mybatisflex.core.query.QueryWrapper;
import tech.easyflow.common.annotation.UsePermission;
+import tech.easyflow.common.domain.Result;
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.RestController;
import tech.easyflow.system.entity.SysApiKeyResource;
import tech.easyflow.system.service.SysApiKeyResourceService;
+import java.util.List;
+
/**
* 请求接口表 控制层。
*
@@ -20,4 +25,26 @@ public class SysApiKeyResourceController extends BaseCurdController工作流 Public API 使用独立的全局授权开关,不进入普通接口授权列表,避免用户误以为勾选
+ * 具体接口资源即可完成工作流调用授权。
+ *
+ * @param entity 查询条件
+ * @param asTree 是否树形返回
+ * @param sortKey 排序字段
+ * @param sortType 排序方向
+ * @return 普通接口授权资源
+ */
+ @Override
+ @GetMapping("list")
+ public Result> 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));
+ }
+}
diff --git a/easyflow-api/easyflow-api-public/src/main/java/tech/easyflow/publicapi/controller/PublicWorkflowController.java b/easyflow-api/easyflow-api-public/src/main/java/tech/easyflow/publicapi/controller/PublicWorkflowController.java
index 4cd5045..7c0e1ab 100644
--- a/easyflow-api/easyflow-api-public/src/main/java/tech/easyflow/publicapi/controller/PublicWorkflowController.java
+++ b/easyflow-api/easyflow-api-public/src/main/java/tech/easyflow/publicapi/controller/PublicWorkflowController.java
@@ -4,22 +4,31 @@ import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.stp.StpUtil;
import com.easyagents.flow.core.chain.runtime.ChainExecutor;
import jakarta.annotation.Resource;
+import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.constraints.NotBlank;
import org.springframework.web.bind.annotation.*;
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.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.service.WorkflowRunningParameterResolver;
+import tech.easyflow.ai.easyagentsflow.support.PublishedWorkflowDefinitionIds;
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.WorkflowApiPermissionService;
+import tech.easyflow.ai.utils.WorkFlowUtil;
import tech.easyflow.common.constant.Constants;
import tech.easyflow.common.domain.Result;
+import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.satoken.util.SaTokenUtil;
+import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.common.web.jsonbody.JsonBody;
+import tech.easyflow.system.entity.SysApiKey;
import java.math.BigInteger;
import java.util.HashMap;
@@ -43,6 +52,10 @@ public class PublicWorkflowController {
private WorkflowCheckService workflowCheckService;
@Resource
private WorkflowRunningParameterResolver workflowRunningParameterResolver;
+ @Resource
+ private WorkflowApiPermissionService workflowApiPermissionService;
+ @Resource
+ private WorkflowExecResultService workflowExecResultService;
/**
* 通过id或别名获取工作流详情
@@ -54,8 +67,11 @@ public class PublicWorkflowController {
@RequirePublishedAccess(resourceType = "WORKFLOW", idExpr = "#key", denyMessage = "工作流尚未发布")
public Result getByIdOrAlias(
@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);
+ assertStrictPublishedWorkflow(workflow);
return Result.ok(workflow);
}
@@ -88,19 +104,20 @@ public class PublicWorkflowController {
* 运行工作流 - v2
*/
@PostMapping("/runAsync")
- @SaCheckPermission("/api/v1/workflow/save")
@RequirePublishedAccess(resourceType = "WORKFLOW", idExpr = "#id", denyMessage = "工作流尚未发布")
public Result runAsync(@JsonBody(value = "id", required = true) BigInteger id,
- @JsonBody("variables") Map variables) {
+ @JsonBody("variables") Map variables,
+ HttpServletRequest request) {
+ SysApiKey apiKey = workflowApiPermissionService.assertWorkflowApi(request.getHeader("ApiKey"), request.getRequestURI());
if (variables == null) {
variables = new HashMap<>();
}
Workflow workflow = workflowService.getPublishedById(id);
- if (workflow == null) {
- throw new RuntimeException("工作流不存在");
- }
+ assertStrictPublishedWorkflow(workflow);
workflowCheckService.checkOrThrow(workflow.getContent(), WorkflowCheckStage.PRE_EXECUTE, workflow.getId());
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);
return Result.ok(executeId);
}
@@ -110,7 +127,10 @@ public class PublicWorkflowController {
*/
@PostMapping("/getChainStatus")
public Result getChainStatus(@JsonBody(value = "executeId") String executeId,
- @JsonBody("nodes") List nodes) {
+ @JsonBody("nodes") List nodes,
+ HttpServletRequest request) {
+ SysApiKey apiKey = workflowApiPermissionService.assertWorkflowApi(request.getHeader("ApiKey"), request.getRequestURI());
+ assertApiKeyExecutionOwnership(apiKey, executeId);
ChainInfo res = tinyFlowService.getChainStatus(executeId, nodes);
return Result.ok(res);
}
@@ -119,22 +139,23 @@ public class PublicWorkflowController {
* 恢复工作流运行 - v2
*/
@PostMapping("/resume")
- @SaCheckPermission("/api/v1/workflow/save")
public Result resume(@JsonBody(value = "executeId", required = true) String executeId,
- @JsonBody("confirmParams") Map confirmParams) {
+ @JsonBody("confirmParams") Map confirmParams,
+ HttpServletRequest request) {
+ SysApiKey apiKey = workflowApiPermissionService.assertWorkflowApi(request.getHeader("ApiKey"), request.getRequestURI());
+ WorkflowExecResult execResult = assertApiKeyExecutionOwnership(apiKey, executeId);
+ assertWorkflowExecutionResumable(execResult);
chainExecutor.resumeAsync(executeId, confirmParams);
return Result.ok();
}
@GetMapping("getRunningParameters")
- @SaCheckPermission("/api/v1/workflow/query")
@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);
- if (workflow == null) {
- return Result.fail(1, "can not find the workflow by id: " + id);
- }
+ assertStrictPublishedWorkflow(workflow);
workflowCheckService.checkOrThrow(workflow.getContent(), WorkflowCheckStage.PRE_EXECUTE, workflow.getId());
Map res = workflowRunningParameterResolver.buildRunningParametersView(workflow);
if (res == null) {
@@ -142,4 +163,72 @@ public class PublicWorkflowController {
}
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("工作流已下线或不可恢复执行");
+ }
+ }
}
diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagentsflow/listener/ChainEventListenerForSave.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagentsflow/listener/ChainEventListenerForSave.java
index ebd6a6b..4b89b1e 100644
--- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagentsflow/listener/ChainEventListenerForSave.java
+++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagentsflow/listener/ChainEventListenerForSave.java
@@ -82,7 +82,7 @@ public class ChainEventListenerForSave implements ChainEventListener {
record.setWorkflowJson(workflow.getContent());
record.setStartTime(new Date());
record.setStatus(state.getStatus().getValue());
- record.setCreatedKey(WorkFlowUtil.USER_KEY);
+ record.setCreatedKey(WorkFlowUtil.getCreatedKey(chain));
record.setCreatedBy(WorkFlowUtil.getOperator(chain).getId().toString());
try {
workflowExecResultService.save(record);
diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/WorkflowApiPermissionService.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/WorkflowApiPermissionService.java
new file mode 100644
index 0000000..5118bda
--- /dev/null
+++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/WorkflowApiPermissionService.java
@@ -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);
+}
diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/WorkflowApiPermissionServiceImpl.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/WorkflowApiPermissionServiceImpl.java
new file mode 100644
index 0000000..5a0a98e
--- /dev/null
+++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/WorkflowApiPermissionServiceImpl.java
@@ -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 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 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;
+ }
+}
diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/utils/WorkFlowUtil.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/utils/WorkFlowUtil.java
index 7d6d07f..72c1a52 100644
--- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/utils/WorkFlowUtil.java
+++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/utils/WorkFlowUtil.java
@@ -12,7 +12,9 @@ import java.math.BigInteger;
public class WorkFlowUtil {
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 CREATED_KEY_MEMORY_KEY = "workflowCreatedKey";
public static String removeSensitiveInfo(String originJson) {
JSONObject workflowInfo = JSON.parseObject(originJson);
@@ -35,6 +37,17 @@ public class WorkFlowUtil {
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() {
LoginAccount account = new LoginAccount();
account.setId(new BigInteger("0"));
diff --git a/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/entity/SysApiKey.java b/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/entity/SysApiKey.java
index 0871f58..579f30d 100644
--- a/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/entity/SysApiKey.java
+++ b/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/entity/SysApiKey.java
@@ -26,6 +26,9 @@ public class SysApiKey extends SysApiKeyBase {
@Column(ignore = true)
private Boolean knowledgeShareEnabled;
+ @Column(ignore = true)
+ private Boolean workflowApiEnabled;
+
@RelationOneToMany(selfField = "id", targetField = "apiKeyId", targetTable = "tb_sys_api_key_resource_mapping")
private List resourcePermissions;
@@ -52,4 +55,12 @@ public class SysApiKey extends SysApiKeyBase {
public void setKnowledgeShareEnabled(Boolean knowledgeShareEnabled) {
this.knowledgeShareEnabled = knowledgeShareEnabled;
}
+
+ public Boolean getWorkflowApiEnabled() {
+ return workflowApiEnabled;
+ }
+
+ public void setWorkflowApiEnabled(Boolean workflowApiEnabled) {
+ this.workflowApiEnabled = workflowApiEnabled;
+ }
}
diff --git a/easyflow-starter/easyflow-starter-all/src/main/resources/db/migration/mysql/V17__mysql_workflow_public_api_resources.sql b/easyflow-starter/easyflow-starter-all/src/main/resources/db/migration/mysql/V17__mysql_workflow_public_api_resources.sql
new file mode 100644
index 0000000..983bf25
--- /dev/null
+++ b/easyflow-starter/easyflow-starter-all/src/main/resources/db/migration/mysql/V17__mysql_workflow_public_api_resources.sql
@@ -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`);
diff --git a/easyflow-ui-admin/app/src/locales/langs/en-US/aiWorkflow.json b/easyflow-ui-admin/app/src/locales/langs/en-US/aiWorkflow.json
index a7a0143..6104b40 100644
--- a/easyflow-ui-admin/app/src/locales/langs/en-US/aiWorkflow.json
+++ b/easyflow-ui-admin/app/src/locales/langs/en-US/aiWorkflow.json
@@ -80,6 +80,15 @@
"publishStatusOffline": "Offline",
"publishStatusDeletePending": "Delete Pending",
"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?",
"submitRepublishApprovalConfirm": "Republish the current workflow now?",
"submitOfflineApprovalConfirm": "Take the current workflow offline?",
diff --git a/easyflow-ui-admin/app/src/locales/langs/en-US/sysApiKey.json b/easyflow-ui-admin/app/src/locales/langs/en-US/sysApiKey.json
index 8f3f67b..9c13277 100644
--- a/easyflow-ui-admin/app/src/locales/langs/en-US/sysApiKey.json
+++ b/easyflow-ui-admin/app/src/locales/langs/en-US/sysApiKey.json
@@ -15,5 +15,6 @@
},
"permissions": "AuthInterface",
"knowledgeSharePermission": "Knowledge Share",
+ "workflowApiPermission": "Workflow API",
"addApiKeyNotice": "This operation will generate an API key. Please confirm whether to proceed"
}
diff --git a/easyflow-ui-admin/app/src/locales/langs/zh-CN/aiWorkflow.json b/easyflow-ui-admin/app/src/locales/langs/zh-CN/aiWorkflow.json
index 5337026..4fd3eed 100644
--- a/easyflow-ui-admin/app/src/locales/langs/zh-CN/aiWorkflow.json
+++ b/easyflow-ui-admin/app/src/locales/langs/zh-CN/aiWorkflow.json
@@ -80,6 +80,15 @@
"publishStatusOffline": "已下线",
"publishStatusDeletePending": "删除审批中",
"publishStatusLabel": "发布状态",
+ "apiInstruction": "接口调用说明",
+ "apiInstructionPublishRequired": "发布后可获取 API 调用说明",
+ "apiEndpoint": "调用地址",
+ "apiRequestExample": "请求示例",
+ "apiResponseExample": "返回示例",
+ "apiVariables": "入参说明",
+ "apiVariablesEmpty": "当前工作流没有开始参数",
+ "apiStatusExample": "状态查询示例",
+ "apiResumeExample": "恢复执行示例",
"submitPublishApprovalConfirm": "确认发布当前工作流吗?",
"submitRepublishApprovalConfirm": "确认重新发布当前工作流吗?",
"submitOfflineApprovalConfirm": "确认下线当前工作流吗?",
diff --git a/easyflow-ui-admin/app/src/locales/langs/zh-CN/sysApiKey.json b/easyflow-ui-admin/app/src/locales/langs/zh-CN/sysApiKey.json
index fe21b23..a638663 100644
--- a/easyflow-ui-admin/app/src/locales/langs/zh-CN/sysApiKey.json
+++ b/easyflow-ui-admin/app/src/locales/langs/zh-CN/sysApiKey.json
@@ -15,5 +15,6 @@
},
"permissions": "授权接口",
"knowledgeSharePermission": "知识库分享授权",
+ "workflowApiPermission": "工作流 API 调用授权",
"addApiKeyNotice": "该操作会生成一个apiKey,请确认是否生成"
}
diff --git a/easyflow-ui-admin/app/src/views/ai/workflow/WorkflowList.vue b/easyflow-ui-admin/app/src/views/ai/workflow/WorkflowList.vue
index bc87603..ba70f34 100644
--- a/easyflow-ui-admin/app/src/views/ai/workflow/WorkflowList.vue
+++ b/easyflow-ui-admin/app/src/views/ai/workflow/WorkflowList.vue
@@ -5,11 +5,15 @@ import type {
ActionButton,
CardPrimaryAction,
} from '#/components/page/CardList.vue';
+import type { OfflineImpactCheck } from '#/views/ai/shared/offline-impact';
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 { EasyFlowFormModal } from '@easyflow/common-ui';
+import { useAppConfig } from '@easyflow/hooks';
import {
Check,
@@ -17,6 +21,7 @@ import {
Delete,
Download,
Edit,
+ Link,
Lock,
OfficeBuilding,
Plus,
@@ -26,6 +31,7 @@ import {
VideoPlay,
} from '@element-plus/icons-vue';
import {
+ ElDialog,
ElForm,
ElFormItem,
ElIcon,
@@ -49,10 +55,7 @@ import { $t } from '#/locales';
import { router } from '#/router';
import { useDictStore } from '#/store';
import AiResourceCornerMeta from '#/views/ai/shared/AiResourceCornerMeta.vue';
-import {
- buildOfflineImpactMessage,
- type OfflineImpactCheck,
-} from '#/views/ai/shared/offline-impact';
+import { buildOfflineImpactMessage } from '#/views/ai/shared/offline-impact';
import {
canAiResourceDelete,
canAiResourceOffline,
@@ -64,6 +67,8 @@ import {
import WorkflowModal from './WorkflowModal.vue';
+const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
+
interface FieldDefinition {
// 字段名称
prop: string;
@@ -79,6 +84,15 @@ interface FieldDefinition {
type VisibilityScope = 'DEPT' | 'PRIVATE' | 'PUBLIC';
+interface ApiFieldDoc {
+ key: string;
+ label: string;
+ type: string;
+ required: boolean;
+ description: string;
+ placeholder: string;
+}
+
const primaryAction: CardPrimaryAction = {
icon: DesignIcon,
text: $t('button.design'),
@@ -94,6 +108,8 @@ const canManageWorkflow = computed(() =>
);
const updatingScopeId = ref(null);
const visibilityScopePopoverRefs = ref>({});
+const apiInstructionVisible = ref(false);
+const apiInstructionRow = ref(null);
const visibilityScopeMeta = computed(() => ({
PRIVATE: {
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,
text: $t('button.export'),
@@ -199,7 +223,8 @@ const actions: ActionButton[] = [
text: $t('button.offline'),
permission: '/api/v1/workflow/save',
placement: 'menu',
- visible: (row: any) => canAiResourceOffline(row.displayPublishStatus, row.publishStatus),
+ visible: (row: any) =>
+ canAiResourceOffline(row.displayPublishStatus, row.publishStatus),
onClick: (row: any) => {
submitOfflineAction(row);
},
@@ -210,7 +235,8 @@ const actions: ActionButton[] = [
tone: 'danger',
permission: '/api/v1/workflow/remove',
placement: 'menu',
- visible: (row: any) => canAiResourceDelete(row.displayPublishStatus, row.publishStatus),
+ visible: (row: any) =>
+ canAiResourceDelete(row.displayPublishStatus, row.publishStatus),
onClick: (row: any) => {
submitDeleteApproval(row);
},
@@ -306,6 +332,321 @@ function resolveNavTitle(row: any) {
function isRepublishAction(row: any) {
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 = {};
+ 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}\` `;
+}
+
+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: `);
+ 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: '' }, 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) {
if (
@@ -329,12 +670,9 @@ async function submitPublishAction(row: any) {
} catch {
return;
}
- const res = await api.post(
- '/api/v1/workflow/submitPublishApproval',
- {
- id: row.id,
- },
- );
+ const res = await api.post('/api/v1/workflow/submitPublishApproval', {
+ id: row.id,
+ });
if (res.errorCode === 0) {
ElMessage.success(res.message || $t('message.saveOkMessage'));
pageDataRef.value?.reload?.();
@@ -350,17 +688,15 @@ async function submitOfflineAction(row: any) {
const impactRes = await api.get<{
data: OfflineImpactCheck;
errorCode: number;
- }>(
- '/api/v1/workflow/offlineImpactCheck',
- {
- params: { id: row.id },
- },
- );
+ }>('/api/v1/workflow/offlineImpactCheck', {
+ params: { id: row.id },
+ });
if (impactRes.errorCode !== 0) {
return;
}
try {
const sections = [];
+ let offlineImpactFooter = $t('aiWorkflow.offlineImpactBoundBotsFooter');
if (impactRes.data?.hasBotBindings) {
sections.push(
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 =
sections.length > 0
? h('div', [
...sections,
- h(
- 'p',
- {
- style: 'margin-top: 12px;',
- },
- impactRes.data?.hasBotBindings && impactRes.data?.hasPluginBindings
- ? $t('aiWorkflow.offlineImpactBoundMixedFooter')
- : impactRes.data?.hasPluginBindings
- ? $t('aiWorkflow.offlineImpactBoundPluginsFooter')
- : $t('aiWorkflow.offlineImpactBoundBotsFooter'),
- ),
+ h('p', { style: 'margin-top: 12px;' }, offlineImpactFooter),
])
: $t('aiWorkflow.submitOfflineApprovalConfirm');
- await ElMessageBox.confirm(
- impactMessage,
- $t('message.noticeTitle'),
- {
- confirmButtonText: $t('button.confirm'),
- cancelButtonText: $t('button.cancel'),
- type: 'warning',
- },
- );
+ await ElMessageBox.confirm(impactMessage, $t('message.noticeTitle'), {
+ confirmButtonText: $t('button.confirm'),
+ cancelButtonText: $t('button.cancel'),
+ type: 'warning',
+ });
} catch {
return;
}
@@ -459,18 +786,18 @@ function resolvePublishStatusMetaByInstance(
tone: 'danger',
};
}
- case 'OFFLINE_PENDING': {
- return {
- label: $t('aiWorkflow.publishStatusOfflinePending'),
- tone: 'pending',
- };
- }
case 'OFFLINE': {
return {
label: $t('aiWorkflow.publishStatusOffline'),
tone: 'draft',
};
}
+ case 'OFFLINE_PENDING': {
+ return {
+ label: $t('aiWorkflow.publishStatusOfflinePending'),
+ tone: 'pending',
+ };
+ }
case 'PUBLISH_PENDING': {
return {
label: $t('aiWorkflow.publishStatusPublishPending'),
@@ -668,6 +995,25 @@ function handleHeaderButtonClick(data: any) {
+
+
+
+
+
{{
- resolvePublishStatusMetaByInstance(item.displayPublishStatus, item.publishStatus).label
+ resolvePublishStatusMetaByInstance(
+ item.displayPublishStatus,
+ item.publishStatus,
+ ).label
}}
@@ -1102,4 +1451,153 @@ button.workflow-scope-chip:disabled {
border-radius: 16px;
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;
+}
diff --git a/easyflow-ui-admin/app/src/views/config/apikey/SysApiKeyModal.vue b/easyflow-ui-admin/app/src/views/config/apikey/SysApiKeyModal.vue
index cc62180..065fb5d 100644
--- a/easyflow-ui-admin/app/src/views/config/apikey/SysApiKeyModal.vue
+++ b/easyflow-ui-admin/app/src/views/config/apikey/SysApiKeyModal.vue
@@ -33,6 +33,7 @@ interface Entity {
expiredAt: Date | null | string;
permissionIds: (number | string)[]; // 绑定值:权限 ID 数组
knowledgeShareEnabled: boolean;
+ workflowApiEnabled: boolean;
id?: number; // 编辑时的主键
}
@@ -51,6 +52,7 @@ const entity = ref({
expiredAt: null,
permissionIds: [],
knowledgeShareEnabled: false,
+ workflowApiEnabled: false,
});
// 加载状态
const btnLoading = ref(false);
@@ -120,6 +122,7 @@ function getResourcePermissionList() {
function createDefaultEntity(row: Partial = {}): Entity {
const permissionIds = row.permissionIds || [];
const knowledgeShareEnabled = Boolean(row.knowledgeShareEnabled);
+ const workflowApiEnabled = Boolean(row.workflowApiEnabled);
return {
apiKey: '',
status: '',
@@ -128,6 +131,7 @@ function createDefaultEntity(row: Partial = {}): Entity {
...row,
permissionIds,
knowledgeShareEnabled,
+ workflowApiEnabled,
};
}
@@ -183,6 +187,7 @@ function closeDialog() {
expiredAt: null,
permissionIds: [],
knowledgeShareEnabled: false,
+ workflowApiEnabled: false,
};
isAdd.value = true;
dialogVisible.value = false;
@@ -261,6 +266,12 @@ defineExpose({
>
{{ $t('sysApiKey.knowledgeSharePermission') }}
+
+ {{ $t('sysApiKey.workflowApiPermission') }}
+