初始化

This commit is contained in:
2026-02-22 18:56:10 +08:00
commit 26677972a6
3112 changed files with 255972 additions and 0 deletions

View 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>

View File

@@ -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("//", "/");
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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")
;
}
}

View File

@@ -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;
}
}