初始化
This commit is contained in:
37
easyflow-api/easyflow-api-public/pom.xml
Normal file
37
easyflow-api/easyflow-api-public/pom.xml
Normal file
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>tech.easyflow</groupId>
|
||||
<artifactId>easyflow-api</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>easyflow-api-public</artifactId>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>tech.easyflow</groupId>
|
||||
<artifactId>easyflow-module-ai</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>tech.easyflow</groupId>
|
||||
<artifactId>easyflow-module-system</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.javaparser</groupId>
|
||||
<artifactId>javaparser-core</artifactId>
|
||||
<version>3.25.8</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,188 @@
|
||||
package tech.easyflow.publicapi;
|
||||
|
||||
import com.github.javaparser.StaticJavaParser;
|
||||
import com.github.javaparser.ast.CompilationUnit;
|
||||
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
|
||||
import com.github.javaparser.ast.expr.AnnotationExpr;
|
||||
import com.github.javaparser.ast.expr.MemberValuePair;
|
||||
import com.github.javaparser.ast.expr.NormalAnnotationExpr;
|
||||
import com.github.javaparser.ast.expr.SingleMemberAnnotationExpr;
|
||||
import com.mybatisflex.core.MybatisFlexBootstrap;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
import tech.easyflow.system.entity.SysApiKeyResource;
|
||||
import tech.easyflow.system.mapper.SysApiKeyResourceMapper;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* 同步接口到数据库
|
||||
*/
|
||||
public class SyncApis {
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
|
||||
try (HikariDataSource dataSource = new HikariDataSource()) {
|
||||
dataSource.setJdbcUrl("jdbc:mysql://192.168.2.10:3306/easyflow-v2?useInformationSchema=true&characterEncoding=utf-8");
|
||||
dataSource.setUsername("root");
|
||||
dataSource.setPassword("123456");
|
||||
|
||||
MybatisFlexBootstrap bootstrap = MybatisFlexBootstrap.getInstance();
|
||||
bootstrap.setDataSource(dataSource);
|
||||
bootstrap.addMapper(SysApiKeyResourceMapper.class);
|
||||
bootstrap.start();
|
||||
|
||||
SysApiKeyResourceMapper mapper = bootstrap.getMapper(SysApiKeyResourceMapper.class);
|
||||
String dir = System.getProperty("user.dir") + "/easyflow-api/easyflow-api-public/src/main/java/tech/easyflow/publicapi/controller";
|
||||
List<String> filePath = getAllFilePaths(dir);
|
||||
for (String path : filePath) {
|
||||
extractCommentsFromFile(path, mapper);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static List<String> getAllFilePaths(String directoryPath) throws IOException {
|
||||
Path startPath = Paths.get(directoryPath);
|
||||
|
||||
try (Stream<Path> stream = Files.walk(startPath)) {
|
||||
return stream
|
||||
.filter(Files::isRegularFile) // 只获取文件,排除目录
|
||||
.map(Path::toAbsolutePath) // 转换为绝对路径
|
||||
.map(Path::toString) // 转换为字符串
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
|
||||
public static void extractCommentsFromFile(String filePath, SysApiKeyResourceMapper mapper) throws Exception {
|
||||
System.out.println("正在解析文件: " + filePath);
|
||||
FileInputStream in = new FileInputStream(filePath);
|
||||
com.github.javaparser.JavaParser parser = new com.github.javaparser.JavaParser();
|
||||
parser.getParserConfiguration().setLanguageLevel(com.github.javaparser.ParserConfiguration.LanguageLevel.JAVA_17);
|
||||
CompilationUnit cu = parser.parse(in).getResult().orElseThrow();
|
||||
|
||||
cu.findAll(ClassOrInterfaceDeclaration.class).forEach(c -> {
|
||||
// 1. 获取类级别的 RequestMapping 路径
|
||||
String classPath = "";
|
||||
Optional<AnnotationExpr> classMapping = c.getAnnotationByName("RequestMapping");
|
||||
if (classMapping.isPresent()) {
|
||||
classPath = getAnnotationValue(classMapping.get());
|
||||
}
|
||||
|
||||
String finalClassPath = classPath; //用于lambda中使用
|
||||
String className = c.getNameAsString();
|
||||
String classComment = c.getJavadoc().map(d -> d.getDescription().toText()).orElse("");
|
||||
|
||||
System.out.println("=========================================");
|
||||
System.out.println("类名: " + className);
|
||||
System.out.println("类注释: " + classComment);
|
||||
System.out.println("类路径: " + finalClassPath);
|
||||
|
||||
// 2. 遍历方法
|
||||
c.getMethods().forEach(method -> {
|
||||
// 查找常见的 Mapping 注解
|
||||
String[] mappingTypes = {"GetMapping", "PostMapping", "PutMapping", "DeleteMapping", "PatchMapping", "RequestMapping"};
|
||||
|
||||
for (String mappingType : mappingTypes) {
|
||||
Optional<AnnotationExpr> methodMapping = method.getAnnotationByName(mappingType);
|
||||
if (methodMapping.isPresent()) {
|
||||
// 获取方法上的路径
|
||||
String methodPath = getAnnotationValue(methodMapping.get());
|
||||
|
||||
// 拼接完整 URI
|
||||
String fullUri = combinePaths(finalClassPath, methodPath);
|
||||
|
||||
// 获取请求方式 (如果是 RequestMapping,通常默认为 All 或者需要进一步解析 method 属性,这里简单处理)
|
||||
String httpMethod = mappingType.replace("Mapping", "").toUpperCase();
|
||||
if (httpMethod.equals("REQUEST")) httpMethod = "ALL";
|
||||
|
||||
// 获取方法注释
|
||||
String methodComment = method.getJavadoc().map(doc -> doc.getDescription().toText()).orElse("");
|
||||
|
||||
System.out.println("--------------------------------");
|
||||
System.out.println(" 方法名: " + method.getNameAsString());
|
||||
System.out.println(" 类型: " + httpMethod);
|
||||
System.out.println(" 完整URI: " + fullUri);
|
||||
System.out.println(" 方法注释: " + methodComment);
|
||||
|
||||
// 可以在这里调用 mapper 存入数据库
|
||||
QueryWrapper w = QueryWrapper.create();
|
||||
w.eq(SysApiKeyResource::getRequestInterface, fullUri);
|
||||
SysApiKeyResource record = mapper.selectOneByQuery(w);
|
||||
if (record != null) {
|
||||
record.setTitle(methodComment);
|
||||
mapper.insertOrUpdate(record);
|
||||
} else {
|
||||
record = new SysApiKeyResource();
|
||||
record.setRequestInterface(fullUri);
|
||||
record.setTitle(methodComment);
|
||||
mapper.insert(record);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析注解中的 value 或 path 值
|
||||
* 处理几种情况:
|
||||
* 1. @GetMapping("/api") -> SingleMemberAnnotationExpr
|
||||
* 2. @GetMapping(value = "/api") -> NormalAnnotationExpr
|
||||
* 3. @GetMapping(path = "/api") -> NormalAnnotationExpr
|
||||
* 4. @GetMapping -> 默认为空字符串
|
||||
*/
|
||||
private static String getAnnotationValue(AnnotationExpr annotation) {
|
||||
// 情况 1: @GetMapping("/path")
|
||||
if (annotation instanceof SingleMemberAnnotationExpr) {
|
||||
String value = ((SingleMemberAnnotationExpr) annotation).getMemberValue().toString();
|
||||
return removeQuotes(value);
|
||||
}
|
||||
// 情况 2: @GetMapping(value="/path") 或 @GetMapping(path="/path")
|
||||
else if (annotation instanceof NormalAnnotationExpr) {
|
||||
NormalAnnotationExpr normal = (NormalAnnotationExpr) annotation;
|
||||
for (MemberValuePair pair : normal.getPairs()) {
|
||||
String key = pair.getNameAsString();
|
||||
if ("value".equals(key) || "path".equals(key)) {
|
||||
return removeQuotes(pair.getValue().toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
// 情况 3: @GetMapping (没有参数)
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* 去除 JavaParser 解析出的字符串中的双引号
|
||||
*/
|
||||
private static String removeQuotes(String value) {
|
||||
if (value == null) return "";
|
||||
return value.replace("\"", "").trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 拼接类路径和方法路径,处理斜杠
|
||||
*/
|
||||
private static String combinePaths(String classPath, String methodPath) {
|
||||
if (classPath == null) classPath = "";
|
||||
if (methodPath == null) methodPath = "";
|
||||
|
||||
// 确保以 / 开头
|
||||
if (!classPath.startsWith("/") && !classPath.isEmpty()) classPath = "/" + classPath;
|
||||
if (!methodPath.startsWith("/") && !methodPath.isEmpty()) methodPath = "/" + methodPath;
|
||||
|
||||
// 如果 classPath 只有 /,去掉它,避免 //method
|
||||
if (classPath.equals("/")) classPath = "";
|
||||
|
||||
String full = classPath + methodPath;
|
||||
// 处理重复斜杠 (例如 class=/api/ method=/list -> /api//list)
|
||||
return full.replace("//", "/");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package tech.easyflow.publicapi.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaIgnore;
|
||||
import com.easyagents.core.message.UserMessage;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
import tech.easyflow.ai.entity.Bot;
|
||||
import tech.easyflow.ai.entity.ChatRequestParams;
|
||||
import tech.easyflow.ai.service.BotService;
|
||||
import tech.easyflow.ai.service.impl.BotServiceImpl;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.core.chat.protocol.sse.ChatSseUtil;
|
||||
import tech.easyflow.system.entity.SysApiKey;
|
||||
import tech.easyflow.system.service.SysApiKeyService;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
/**
|
||||
* bot 接口
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/public-api/bot")
|
||||
public class PublicBotController {
|
||||
|
||||
@Resource
|
||||
private BotService botService;
|
||||
@Resource
|
||||
private SysApiKeyService sysApiKeyService;
|
||||
|
||||
/**
|
||||
* 根据id或别名获取bot详情
|
||||
*/
|
||||
@GetMapping("/getByIdOrAlias")
|
||||
public Result<Bot> getByIdOrAlias(@NotBlank(message = "key不能为空") String key) {
|
||||
return Result.ok(botService.getDetail(key));
|
||||
}
|
||||
|
||||
/**
|
||||
* 第三方调用聊天助手
|
||||
*
|
||||
* @return 返回SseEmitter对象,用于服务器向客户端推送聊天响应数据
|
||||
*/
|
||||
@PostMapping("chat")
|
||||
public SseEmitter chat(@RequestBody ChatRequestParams chatRequestParams, HttpServletRequest request) {
|
||||
String apikey = request.getHeader(SysApiKey.KEY_Apikey);
|
||||
String requestURI = request.getRequestURI();
|
||||
if (!StringUtils.hasText(apikey)) {
|
||||
return ChatSseUtil.sendSystemError(null, "Apikey不能为空!");
|
||||
}
|
||||
sysApiKeyService.checkApikeyPermission(apikey, requestURI);
|
||||
BotServiceImpl.ChatCheckResult chatCheckResult = new BotServiceImpl.ChatCheckResult();
|
||||
int size = chatRequestParams.getMessages().size();
|
||||
String prompt = null;
|
||||
if (chatRequestParams.getMessages().get(size - 1) instanceof UserMessage) {
|
||||
prompt = ((UserMessage) chatRequestParams.getMessages().get(size - 1)).getContent();
|
||||
}
|
||||
// 前置校验:失败则直接返回错误SseEmitter
|
||||
SseEmitter errorEmitter = botService.checkChatBeforeStart(chatRequestParams.getBotId(), prompt, chatRequestParams.getConversationId(), chatCheckResult);
|
||||
if (errorEmitter != null) {
|
||||
return errorEmitter;
|
||||
}
|
||||
return botService.startPublicChat(chatRequestParams.getBotId(), prompt, chatRequestParams.getMessages(), chatCheckResult);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package tech.easyflow.publicapi.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import com.easyagents.flow.core.chain.ChainDefinition;
|
||||
import com.easyagents.flow.core.chain.Parameter;
|
||||
import com.easyagents.flow.core.chain.runtime.ChainExecutor;
|
||||
import com.easyagents.flow.core.parser.ChainParser;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
import tech.easyflow.ai.service.WorkflowService;
|
||||
import tech.easyflow.ai.easyagentsflow.entity.ChainInfo;
|
||||
import tech.easyflow.ai.easyagentsflow.entity.NodeInfo;
|
||||
import tech.easyflow.ai.easyagentsflow.service.TinyFlowService;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 工作流
|
||||
*/
|
||||
@RequestMapping("/public-api/workflow")
|
||||
@RestController
|
||||
public class PublicWorkflowController {
|
||||
|
||||
@Resource
|
||||
private WorkflowService workflowService;
|
||||
@Resource
|
||||
private ChainExecutor chainExecutor;
|
||||
@Resource
|
||||
private ChainParser chainParser;
|
||||
@Resource
|
||||
private TinyFlowService tinyFlowService;
|
||||
|
||||
/**
|
||||
* 通过id或别名获取工作流详情
|
||||
*
|
||||
* @param key id或者别名
|
||||
* @return 工作流详情
|
||||
*/
|
||||
@GetMapping(value = "/getByIdOrAlias")
|
||||
public Result<Workflow> getByIdOrAlias(
|
||||
@RequestParam
|
||||
@NotBlank(message = "key不能为空") String key) {
|
||||
Workflow workflow = workflowService.getDetail(key);
|
||||
return Result.ok(workflow);
|
||||
}
|
||||
|
||||
/**
|
||||
* 节点单独运行
|
||||
*/
|
||||
@PostMapping("/singleRun")
|
||||
@SaCheckPermission("/api/v1/workflow/save")
|
||||
public Result<?> singleRun(
|
||||
@JsonBody(value = "workflowId", required = true) BigInteger workflowId,
|
||||
@JsonBody(value = "nodeId", required = true) String nodeId,
|
||||
@JsonBody("variables") Map<String, Object> variables) {
|
||||
|
||||
Workflow workflow = workflowService.getById(workflowId);
|
||||
if (workflow == null) {
|
||||
return Result.fail(1, "工作流不存在");
|
||||
}
|
||||
Map<String, Object> res = chainExecutor.executeNode(workflowId.toString(), nodeId, variables);
|
||||
return Result.ok(res);
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行工作流 - v2
|
||||
*/
|
||||
@PostMapping("/runAsync")
|
||||
@SaCheckPermission("/api/v1/workflow/save")
|
||||
public Result<String> runAsync(@JsonBody(value = "id", required = true) BigInteger id,
|
||||
@JsonBody("variables") Map<String, Object> variables) {
|
||||
if (variables == null) {
|
||||
variables = new HashMap<>();
|
||||
}
|
||||
Workflow workflow = workflowService.getById(id);
|
||||
if (workflow == null) {
|
||||
throw new RuntimeException("工作流不存在");
|
||||
}
|
||||
String executeId = chainExecutor.executeAsync(id.toString(), variables);
|
||||
return Result.ok(executeId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取工作流运行状态 - v2
|
||||
*/
|
||||
@PostMapping("/getChainStatus")
|
||||
public Result<ChainInfo> getChainStatus(@JsonBody(value = "executeId") String executeId,
|
||||
@JsonBody("nodes") List<NodeInfo> nodes) {
|
||||
ChainInfo res = tinyFlowService.getChainStatus(executeId, nodes);
|
||||
return Result.ok(res);
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复工作流运行 - v2
|
||||
*/
|
||||
@PostMapping("/resume")
|
||||
@SaCheckPermission("/api/v1/workflow/save")
|
||||
public Result<Void> resume(@JsonBody(value = "executeId", required = true) String executeId,
|
||||
@JsonBody("confirmParams") Map<String, Object> confirmParams) {
|
||||
chainExecutor.resumeAsync(executeId, confirmParams);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@GetMapping("getRunningParameters")
|
||||
@SaCheckPermission("/api/v1/workflow/query")
|
||||
public Result<?> getRunningParameters(@RequestParam BigInteger id) {
|
||||
Workflow workflow = workflowService.getById(id);
|
||||
|
||||
if (workflow == null) {
|
||||
return Result.fail(1, "can not find the workflow by id: " + id);
|
||||
}
|
||||
|
||||
ChainDefinition definition = chainParser.parse(workflow.getContent());
|
||||
if (definition == null) {
|
||||
return Result.fail(2, "节点配置错误,请检查! ");
|
||||
}
|
||||
List<Parameter> chainParameters = definition.getStartParameters();
|
||||
Map<String, Object> res = new HashMap<>();
|
||||
res.put("parameters", chainParameters);
|
||||
res.put("title", workflow.getTitle());
|
||||
res.put("description", workflow.getDescription());
|
||||
res.put("icon", workflow.getIcon());
|
||||
return Result.ok(res);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package tech.easyflow.publicapi.interceptor;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
@Configuration
|
||||
public class PublicApiConfig implements WebMvcConfigurer {
|
||||
|
||||
@Resource
|
||||
private PublicApiInterceptor publicApiInterceptor;
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
|
||||
registry.addInterceptor(publicApiInterceptor)
|
||||
.addPathPatterns("/public-api/**")
|
||||
.excludePathPatterns("/public-api/bot/chat")
|
||||
;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package tech.easyflow.publicapi.interceptor;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.util.ResponseUtil;
|
||||
import tech.easyflow.system.service.SysApiKeyService;
|
||||
|
||||
@Component
|
||||
public class PublicApiInterceptor implements HandlerInterceptor {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(PublicApiInterceptor.class);
|
||||
|
||||
@Resource
|
||||
private SysApiKeyService sysApiKeyService;
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
||||
|
||||
String requestURI = request.getRequestURI();
|
||||
String apiKey = request.getHeader("ApiKey");
|
||||
|
||||
if (apiKey == null || apiKey.isEmpty()) {
|
||||
Result<Void> failed = Result.fail(401, "密钥不正确");
|
||||
ResponseUtil.renderJson(response, failed);
|
||||
return false;
|
||||
}
|
||||
sysApiKeyService.checkApikeyPermission(apiKey, requestURI);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user