初始化

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

36
.dockerignore Normal file
View File

@@ -0,0 +1,36 @@
# Git
.git
.gitignore
# Maven
target/
*.jar
*.war
.mvn/
mvnw
mvnw.cmd
# Node.js
**/node_modules
**/dist
**/pnpm-lock.yaml
.npmrc
# IDE
.idea/
.vscode/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Logs
*.log
logs/
# Docker
docker-compose.yml
Dockerfile
.dockerignore

47
.gitignore vendored Normal file
View File

@@ -0,0 +1,47 @@
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
.flattened-pom.xml
application-me.yml
### IntelliJ IDEA ###
*.iws
*.iml
*.ipr
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store
/.logs/
/.idea/
.logs
.idea
.jlsp/
.arts/
luceneKnowledge
# v1
/easyflow-ui-react

117
.mvn/wrapper/MavenWrapperDownloader.java vendored Normal file
View File

@@ -0,0 +1,117 @@
/*
* Copyright 2007-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import java.net.*;
import java.io.*;
import java.nio.channels.*;
import java.util.Properties;
public class MavenWrapperDownloader {
private static final String WRAPPER_VERSION = "0.5.6";
/**
* Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided.
*/
private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/"
+ WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar";
/**
* Path to the maven-wrapper.properties file, which might contain a downloadUrl property to
* use instead of the default one.
*/
private static final String MAVEN_WRAPPER_PROPERTIES_PATH =
".mvn/wrapper/maven-wrapper.properties";
/**
* Path where the maven-wrapper.jar will be saved to.
*/
private static final String MAVEN_WRAPPER_JAR_PATH =
".mvn/wrapper/maven-wrapper.jar";
/**
* Name of the property which should be used to override the default download url for the wrapper.
*/
private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl";
public static void main(String args[]) {
System.out.println("- Downloader started");
File baseDirectory = new File(args[0]);
System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath());
// If the maven-wrapper.properties exists, read it and check if it contains a custom
// wrapperUrl parameter.
File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH);
String url = DEFAULT_DOWNLOAD_URL;
if(mavenWrapperPropertyFile.exists()) {
FileInputStream mavenWrapperPropertyFileInputStream = null;
try {
mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile);
Properties mavenWrapperProperties = new Properties();
mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream);
url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url);
} catch (IOException e) {
System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'");
} finally {
try {
if(mavenWrapperPropertyFileInputStream != null) {
mavenWrapperPropertyFileInputStream.close();
}
} catch (IOException e) {
// Ignore ...
}
}
}
System.out.println("- Downloading from: " + url);
File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH);
if(!outputFile.getParentFile().exists()) {
if(!outputFile.getParentFile().mkdirs()) {
System.out.println(
"- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'");
}
}
System.out.println("- Downloading to: " + outputFile.getAbsolutePath());
try {
downloadFileFromURL(url, outputFile);
System.out.println("Done");
System.exit(0);
} catch (Throwable e) {
System.out.println("- Error downloading");
e.printStackTrace();
System.exit(1);
}
}
private static void downloadFileFromURL(String urlString, File destination) throws Exception {
if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) {
String username = System.getenv("MVNW_USERNAME");
char[] password = System.getenv("MVNW_PASSWORD").toCharArray();
Authenticator.setDefault(new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(username, password);
}
});
}
URL website = new URL(urlString);
ReadableByteChannel rbc;
rbc = Channels.newChannel(website.openStream());
FileOutputStream fos = new FileOutputStream(destination);
fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
fos.close();
rbc.close();
}
}

BIN
.mvn/wrapper/maven-wrapper.jar vendored Normal file

Binary file not shown.

2
.mvn/wrapper/maven-wrapper.properties vendored Normal file
View File

@@ -0,0 +1,2 @@
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip
wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar

View File

@@ -0,0 +1,53 @@
version: '1.0'
name: branch-pipeline
displayName: BranchPipeline
stages:
- stage:
name: compile
displayName: 编译
steps:
- step: build@maven
name: build_maven
displayName: Maven 构建
# 支持6、7、8、9、10、11六个版本
jdkVersion: 8
# 支持2.2.1、3.2.5、3.3.9、3.5.2、3.5.3、3.5.4、3.6.1、3.6.3八个版本
mavenVersion: 3.3.9
# 构建命令
commands:
- mvn -B clean package -Dmaven.test.skip=true
# 非必填字段开启后表示将构建产物暂存但不会上传到制品库中7天后自动清除
artifacts:
# 构建产物名字作为产物的唯一标识可向下传递支持自定义默认为BUILD_ARTIFACT。在下游可以通过${BUILD_ARTIFACT}方式引用来获取构建物地址
- name: BUILD_ARTIFACT
# 构建产物获取路径是指代码编译完毕之后构建物的所在路径如通常jar包在target目录下。当前目录为代码库根目录
path:
- ./easyflow-starter/target
- step: publish@general_artifacts
name: publish_general_artifacts
displayName: 上传制品
# 上游构建任务定义的产物名默认BUILD_ARTIFACT
dependArtifact: BUILD_ARTIFACT
# 上传到制品库时的制品命名默认output
artifactName: output
dependsOn: build_maven
- stage:
name: release
displayName: 发布
steps:
- step: publish@release_artifacts
name: publish_release_artifacts
displayName: '发布'
# 上游上传制品任务的产出
dependArtifact: output
# 发布制品版本号
version: '1.0.0'
# 是否开启版本号自增,默认开启
autoIncrement: true
triggers:
push:
branches:
exclude:
- master
include:
- .*

View File

@@ -0,0 +1,51 @@
version: '1.0'
name: master-pipeline
displayName: MasterPipeline
stages:
- stage:
name: compile
displayName: 编译
steps:
- step: build@maven
name: build_maven
displayName: Maven 构建
# 支持6、7、8、9、10、11六个版本
jdkVersion: 8
# 支持2.2.1、3.2.5、3.3.9、3.5.2、3.5.3、3.5.4、3.6.1、3.6.3八个版本
mavenVersion: 3.3.9
# 构建命令
commands:
- mvn -B clean package -Dmaven.test.skip=true
# 非必填字段开启后表示将构建产物暂存但不会上传到制品库中7天后自动清除
artifacts:
# 构建产物名字作为产物的唯一标识可向下传递支持自定义默认为BUILD_ARTIFACT。在下游可以通过${BUILD_ARTIFACT}方式引用来获取构建物地址
- name: BUILD_ARTIFACT
# 构建产物获取路径是指代码编译完毕之后构建物的所在路径如通常jar包在target目录下。当前目录为代码库根目录
path:
- ./easyflow-starter/target
- step: publish@general_artifacts
name: publish_general_artifacts
displayName: 上传制品
# 上游构建任务定义的产物名默认BUILD_ARTIFACT
dependArtifact: BUILD_ARTIFACT
# 上传到制品库时的制品命名默认output
artifactName: output
dependsOn: build_maven
- stage:
name: release
displayName: 发布
steps:
- step: publish@release_artifacts
name: publish_release_artifacts
displayName: '发布'
# 上游上传制品任务的产出
dependArtifact: output
# 发布制品版本号
version: '1.0.0.0'
# 是否开启版本号自增,默认开启
autoIncrement: true
triggers:
push:
branches:
include:
- master

View File

@@ -0,0 +1,24 @@
version: '1.0'
name: pipeline-sbom
displayName: pipeline-sbom
triggers:
trigger: manual
push:
branches:
prefix:
- ''
stages:
- name: stage-4d6a9b8b
displayName: 未命名
strategy: naturally
trigger: auto
executor: []
steps:
- step: sc@sbom
name: sbom
displayName: SBOM 扫描
scan: code
codePath: ./
notify: []
strategy:
retry: '0'

40
.workflow/pr-pipeline.yml Normal file
View File

@@ -0,0 +1,40 @@
version: '1.0'
name: pr-pipeline
displayName: PRPipeline
stages:
- stage:
name: compile
displayName: 编译
steps:
- step: build@maven
name: build_maven
displayName: Maven 构建
# 支持6、7、8、9、10、11六个版本
jdkVersion: 8
# 支持2.2.1、3.2.5、3.3.9、3.5.2、3.5.3、3.5.4、3.6.1、3.6.3八个版本
mavenVersion: 3.3.9
# 构建命令
commands:
- mvn -B clean package -Dmaven.test.skip=true
# 非必填字段开启后表示将构建产物暂存但不会上传到制品库中7天后自动清除
artifacts:
# 构建产物名字作为产物的唯一标识可向下传递支持自定义默认为BUILD_ARTIFACT。在下游可以通过${BUILD_ARTIFACT}方式引用来获取构建物地址
- name: BUILD_ARTIFACT
# 构建产物获取路径是指代码编译完毕之后构建物的所在路径如通常jar包在target目录下。当前目录为代码库根目录
path:
- ./easyflow-starter/target
- step: publish@general_artifacts
name: publish_general_artifacts
displayName: 上传制品
# 上游构建任务定义的产物名默认BUILD_ARTIFACT
dependArtifact: BUILD_ARTIFACT
# 构建产物制品库默认default系统默认创建
artifactRepository: default
# 上传到制品库时的制品命名默认output
artifactName: output
dependsOn: build_maven
triggers:
pr:
branches:
include:
- master

51
Dockerfile Normal file
View File

@@ -0,0 +1,51 @@
# 第一阶段:构建阶段
FROM maven:3.9-eclipse-temurin-17 AS builder
WORKDIR /build
# 复制 pom.xml 并下载依赖(利用 Docker 缓存)
COPY pom.xml .
COPY easyflow-api/pom.xml easyflow-api/
COPY easyflow-commons/pom.xml easyflow-commons/
COPY easyflow-modules/pom.xml easyflow-modules/
COPY easyflow-starter/pom.xml easyflow-starter/
COPY easyflow-starter/easyflow-starter-all/pom.xml easyflow-starter/easyflow-starter-all/
# 注意:这里需要复制所有模块的 pom.xml 才能正确解析依赖
# 如果模块很多,可能需要更精细的复制,但这里先简单处理
COPY . .
# 执行构建
RUN mvn clean package -DskipTests
# 第二阶段:运行阶段
FROM eclipse-temurin:17-jre
LABEL maintainer="Cennac <cennac@163.com>"
ARG VERSION=2.0.4
ARG SERVICE_NAME=easyflow-starter-all
ARG SERVICE_PORT=8080
ENV VERSION ${VERSION}
ENV SERVICE_NAME ${SERVICE_NAME}
ENV SERVICE_PORT ${SERVICE_PORT}
ENV LANG=C.UTF-8 LC_ALL=C.UTF-8
ENV JAVA_OPTS=""
ENV TZ=Asia/Shanghai
WORKDIR /app
# 安装必要的字体和工具
RUN apt-get update && \
apt-get install -y --no-install-recommends fonts-dejavu-core fontconfig && \
ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone && \
rm -rf /var/lib/apt/lists/*
# 从构建阶段复制 jar 包
COPY --from=builder /build/easyflow-starter/easyflow-starter-all/target/${SERVICE_NAME}-*.jar app.jar
VOLUME /tmp
EXPOSE ${SERVICE_PORT}
ENTRYPOINT java ${JAVA_OPTS} -Djava.security.egd=file:/dev/./urandom -jar ./app.jar

135
README.md Normal file
View File

@@ -0,0 +1,135 @@
# EasyFlow
EasyFlow 是一个面向企业场景的 Java AI 应用开发平台提供智能体、工作流编排、知识库RAG、插件调用、MCP 接入和系统管理能力。
当前后端已基于 `easy-agents``easy-agents-flow` 生态进行整合。
## 核心模块
### 后端聚合模块
- `easyflow-api`:对外 API 层,包含 admin/public/usercenter/mcp 接口。
- `easyflow-commons`公共能力层基础工具、AI 公共组件、缓存、文件存储、验证码、鉴权等)。
- `easyflow-modules`业务模块层AI、系统、日志、任务、数据中心、认证等
- `easyflow-starter`:启动层,`easyflow-starter-all` 为完整可运行服务。
### 前端项目
- `easyflow-ui-admin`:管理后台前端。
- `easyflow-ui-usercenter`:用户中心前端。
- `easyflow-ui-websdk`Web SDK 示例/开发工程。
## 环境要求
- JDK 17+
- Maven 3.8+
- MySQL 8+
- Redis 6+
- Node.js >= 20.10.0
- pnpm >= 9.12.0
## 快速启动(开发环境)
### 1. 初始化数据库
在 MySQL 中导入:
- `sql/01-easyflow-v2.ddl.sql`
- `sql/02-easyflow-v2.data.sql`
### 2. 启动后端
在项目根目录执行:
```bash
mvn -DskipTests clean package
java -jar easyflow-starter/easyflow-starter-all/target/easyflow-starter-all-0.0.1.jar --spring.profiles.active=dev
```
默认端口:`8111`(见 `easyflow-starter/easyflow-starter-all/src/main/resources/application.yml`)。
### 3. 启动前端
管理后台:
```bash
cd easyflow-ui-admin
pnpm install
pnpm dev
```
用户中心:
```bash
cd easyflow-ui-usercenter
pnpm install
pnpm dev
```
默认测试账号:`admin / 123456`
## 后端 Jar 包构建与部署
### 构建 Jar
```bash
mvn -DskipTests -Dmaven.javadoc.skip=true clean package
```
产物路径:
- `easyflow-starter/easyflow-starter-all/target/easyflow-starter-all-0.0.1.jar`
### 启动 Jar生产建议
```bash
java -jar easyflow-starter/easyflow-starter-all/target/easyflow-starter-all-0.0.1.jar --spring.profiles.active=prod
```
可通过环境变量覆盖关键配置(示例):
```bash
SPRING_DATASOURCE_URL=jdbc:mysql://127.0.0.1:3306/easyflow?useInformationSchema=true&characterEncoding=utf-8
SPRING_DATASOURCE_USERNAME=easyflow
SPRING_DATASOURCE_PASSWORD=123456
SPRING_REDIS_HOST=127.0.0.1
SPRING_REDIS_PASSWORD=your_redis_password
```
## 前端 dist 包构建与部署
### 构建 dist
管理后台:
```bash
cd easyflow-ui-admin
pnpm install
pnpm run build
```
用户中心:
```bash
cd easyflow-ui-usercenter
pnpm install
pnpm run build
```
构建产物:
- `easyflow-ui-admin/app/dist`
- `easyflow-ui-usercenter/app/dist`
### 部署方式Nginx
将 dist 目录部署到 Nginx 静态目录,并配置反向代理:
- `/api/` -> 后端服务
- `/userCenter/` -> 后端服务
可直接参考:
- `easyflow-ui-admin/scripts/deploy/nginx.conf`
- `easyflow-ui-usercenter/scripts/deploy/nginx.conf`
## Docker Compose 一键部署(可选)
在项目根目录执行:
```bash
docker compose up --build -d
```
默认端口:
- 后端 API`8080`
- Admin`8081`
- UserCenter`8082`

80
docker-compose.yml Normal file
View File

@@ -0,0 +1,80 @@
services:
# 后端服务
easyflow-api:
build:
context: .
dockerfile: Dockerfile
ports:
- "8080:8080"
environment:
- SPRING_DATASOURCE_URL=jdbc:mysql://mysql:3306/easyflow?useInformationSchema=true&useUnicode=true&characterEncoding=utf-8&allowPublicKeyRetrieval=true&useSSL=false&serverTimezone=Asia/Shanghai
- SPRING_DATASOURCE_USERNAME=root
- SPRING_DATASOURCE_PASSWORD=123456
- SPRING_REDIS_HOST=redis
- SPRING_REDIS_PASSWORD=${REDIS_PASSWORD:-easyflow_redis_2026}
- SPRING_WEB_RESOURCES_STATIC_LOCATIONS=file:/www/easyflow/attachment/
- EASYFLOW_STORAGE_LOCAL_ROOT=/www/easyflow/attachment
networks:
- easyflow-net
volumes:
- ./attachment:/www/easyflow/attachment
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_started
# 管理后台前端
easyflow-ui-admin:
build:
context: ./easyflow-ui-admin
dockerfile: scripts/deploy/Dockerfile
ports:
- "8081:8080"
networks:
- easyflow-net
depends_on:
- easyflow-api
# 用户中心前端
easyflow-ui-usercenter:
build:
context: ./easyflow-ui-usercenter
dockerfile: scripts/deploy/Dockerfile
ports:
- "8082:8080"
networks:
- easyflow-net
depends_on:
- easyflow-api
# 数据库
mysql:
image: mysql:8.0
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_general_ci --lower-case-table-names=1
environment:
MYSQL_ROOT_PASSWORD: 123456
MYSQL_DATABASE: easyflow
ports:
- "3306:3306"
volumes:
- ./sql:/docker-entrypoint-initdb.d
# 这个命令在默认 entrypoint 运行前修复权限
entrypoint: sh -c "chown -R mysql:mysql /docker-entrypoint-initdb.d && docker-entrypoint.sh mysqld"
networks:
- easyflow-net
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-u", "root", "-p$MYSQL_ROOT_PASSWORD", "-h", "localhost"]
timeout: 5s
retries: 5
# Redis (后端可能需要)
redis:
image: redis:alpine
command: redis-server --requirepass ${REDIS_PASSWORD:-easyflow_redis_2026}
networks:
- easyflow-net
networks:
easyflow-net:
driver: bridge

View File

@@ -0,0 +1,32 @@
<?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-admin</artifactId>
<dependencies>
<dependency>
<groupId>tech.easyflow</groupId>
<artifactId>easyflow-module-ai</artifactId>
</dependency>
<dependency>
<groupId>tech.easyflow</groupId>
<artifactId>easyflow-module-auth</artifactId>
</dependency>
<dependency>
<groupId>tech.easyflow</groupId>
<artifactId>easyflow-module-job</artifactId>
</dependency>
<dependency>
<groupId>tech.easyflow</groupId>
<artifactId>easyflow-common-captcha</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,23 @@
package tech.easyflow.admin.controller.ai;
import tech.easyflow.ai.entity.BotCategory;
import tech.easyflow.ai.service.BotCategoryService;
import tech.easyflow.common.annotation.UsePermission;
import tech.easyflow.common.web.controller.BaseCurdController;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* bot分类 控制层。
*
* @author ArkLight
* @since 2025-12-18
*/
@RestController
@RequestMapping("/api/v1/botCategory")
@UsePermission(moduleName = "/api/v1/bot")
public class BotCategoryController extends BaseCurdController<BotCategoryService, BotCategory> {
public BotCategoryController(BotCategoryService service) {
super(service);
}
}

View File

@@ -0,0 +1,372 @@
package tech.easyflow.admin.controller.ai;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.annotation.SaIgnore;
import com.easyagents.core.model.chat.ChatModel;
import com.easyagents.core.model.chat.ChatOptions;
import com.alicp.jetcache.Cache;
import com.mybatisflex.core.keygen.impl.SnowFlakeIDKeyGenerator;
import com.mybatisflex.core.query.QueryWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import tech.easyflow.ai.easyagents.listener.PromptChoreChatStreamListener;
import tech.easyflow.ai.entity.*;
import tech.easyflow.ai.service.*;
import tech.easyflow.ai.service.impl.BotServiceImpl;
import tech.easyflow.common.audio.core.AudioServiceManager;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.web.controller.BaseCurdController;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.common.web.jsonbody.JsonBody;
import tech.easyflow.core.chat.protocol.sse.ChatSseEmitter;
import tech.easyflow.core.chat.protocol.sse.ChatSseUtil;
import javax.annotation.Resource;
import java.io.Serializable;
import java.math.BigInteger;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 控制层。
*
* @author michael
* @since 2024-08-23
*/
@RestController
@RequestMapping("/api/v1/bot")
public class BotController extends BaseCurdController<BotService, Bot> {
private final ModelService modelService;
private final BotWorkflowService botWorkflowService;
private final BotDocumentCollectionService botDocumentCollectionService;
private final BotMessageService botMessageService;
@Resource
private BotService botService;
@Autowired
@Qualifier("defaultCache") // 指定 Bean 名称
private Cache<String, Object> cache;
@Resource
private AudioServiceManager audioServiceManager;
public BotController(BotService service, ModelService modelService, BotWorkflowService botWorkflowService,
BotDocumentCollectionService botDocumentCollectionService, BotMessageService botMessageService) {
super(service);
this.modelService = modelService;
this.botWorkflowService = botWorkflowService;
this.botDocumentCollectionService = botDocumentCollectionService;
this.botMessageService = botMessageService;
}
@Resource
private BotPluginService botPluginService;
@GetMapping("/generateConversationId")
public Result<Long> generateConversationId() {
long nextId = new SnowFlakeIDKeyGenerator().nextId();
return Result.ok(nextId);
}
@PostMapping("updateOptions")
@SaCheckPermission("/api/v1/bot/save")
public Result<Void> updateOptions(@JsonBody("id") BigInteger id,
@JsonBody("options") Map<String, Object> options) {
Bot aiBot = service.getById(id);
Map<String, Object> existOptions = aiBot.getOptions();
if (existOptions == null) {
existOptions = new HashMap<>();
}
if (options != null) {
existOptions.putAll(options);
}
aiBot.setOptions(existOptions);
service.updateById(aiBot);
return Result.ok();
}
@PostMapping("updateLlmOptions")
@SaCheckPermission("/api/v1/bot/save")
public Result<Void> updateLlmOptions(@JsonBody("id")
BigInteger id, @JsonBody("llmOptions")
Map<String, Object> llmOptions) {
Bot aiBot = service.getById(id);
Map<String, Object> existLlmOptions = aiBot.getModelOptions();
if (existLlmOptions == null) {
existLlmOptions = new HashMap<>();
}
if (llmOptions != null) {
existLlmOptions.putAll(llmOptions);
}
aiBot.setModelOptions(existLlmOptions);
service.updateById(aiBot);
return Result.ok();
}
@PostMapping("voiceInput")
@SaIgnore
public Result<String> voiceInput(@RequestParam("audio")
MultipartFile audioFile) {
String recognize = null;
try {
recognize = audioServiceManager.audioToText(audioFile.getInputStream());
} catch (Exception e) {
throw new RuntimeException(e);
}
return Result.ok("", recognize);
}
/**
* 处理聊天请求的接口方法
*
* @param prompt 用户输入的聊天内容,必须提供
* @param botId 聊天机器人的唯一标识符,必须提供
* @param conversationId 会话ID用于标识当前对话会话必须提供
* @param messages 历史消息,用于提供上下文,可选
* @return 返回SseEmitter对象用于服务器向客户端推送聊天响应数据
*/
@PostMapping("chat")
@SaIgnore
public SseEmitter chat(
@JsonBody(value = "prompt", required = true) String prompt,
@JsonBody(value = "botId", required = true) BigInteger botId,
@JsonBody(value = "conversationId", required = true) BigInteger conversationId,
@JsonBody(value = "messages") List<Map<String, String>> messages,
@JsonBody(value = "attachments") List<String> attachments
) {
BotServiceImpl.ChatCheckResult chatCheckResult = new BotServiceImpl.ChatCheckResult();
// 前置校验失败则直接返回错误SseEmitter
SseEmitter errorEmitter = botService.checkChatBeforeStart(botId, prompt, conversationId.toString(), chatCheckResult);
if (errorEmitter != null) {
return errorEmitter;
}
return botService.startChat(botId, prompt, conversationId, messages, chatCheckResult, attachments);
}
@PostMapping("updateLlmId")
@SaCheckPermission("/api/v1/bot/save")
public Result<Void> updateBotLlmId(@RequestBody
Bot aiBot) {
service.updateBotLlmId(aiBot);
return Result.ok();
}
@GetMapping("getDetail")
@SaIgnore
public Result<Bot> getDetail(String id) {
return Result.ok(botService.getDetail(id));
}
@Override
@SaIgnore
public Result<Bot> detail(String id) {
Bot data = botService.getDetail(id);
if (data == null) {
return Result.ok(data);
}
Map<String, Object> llmOptions = data.getModelOptions();
if (llmOptions == null) {
llmOptions = new HashMap<>();
}
if (data.getModelId() == null) {
return Result.ok(data);
}
BigInteger llmId = data.getModelId();
Model llm = modelService.getById(llmId);
if (llm == null) {
data.setModelId(null);
return Result.ok(data);
}
Map<String, Object> options = llm.getOptions();
if (options != null && !options.isEmpty()) {
// 获取是否多模态
Boolean multimodal = (Boolean) options.get("multimodal");
llmOptions.put("multimodal", multimodal != null && multimodal);
}
return Result.ok(data);
}
@Override
protected Result<?> onSaveOrUpdateBefore(Bot entity, boolean isSave) {
String alias = entity.getAlias();
if (StringUtils.hasLength(alias)) {
Bot aiBot = service.getByAlias(alias);
if (aiBot != null && isSave) {
throw new BusinessException("别名已存在!");
}
if (aiBot != null && aiBot.getId().compareTo(entity.getId()) != 0) {
throw new BusinessException("别名已存在!");
}
}
if (isSave) {
// 设置默认值
entity.setModelOptions(getDefaultLlmOptions());
}
return super.onSaveOrUpdateBefore(entity, isSave);
}
private ChatOptions getChatOptions(Map<String, Object> llmOptions) {
ChatOptions defaultOptions = new ChatOptions();
if (llmOptions != null) {
Object topK = llmOptions.get("topK");
Object maxReplyLength = llmOptions.get("maxReplyLength");
Object temperature = llmOptions.get("temperature");
Object topP = llmOptions.get("topP");
Object thinkingEnabled = llmOptions.get("thinkingEnabled");
if (topK != null) {
defaultOptions.setTopK(Integer.parseInt(String.valueOf(topK)));
}
if (maxReplyLength != null) {
defaultOptions.setMaxTokens(Integer.parseInt(String.valueOf(maxReplyLength)));
}
if (temperature != null) {
defaultOptions.setTemperature(Float.parseFloat(String.valueOf(temperature)));
}
if (topP != null) {
defaultOptions.setTopP(Float.parseFloat(String.valueOf(topP)));
}
if (thinkingEnabled != null) {
defaultOptions.setThinkingEnabled(Boolean.parseBoolean(String.valueOf(thinkingEnabled)));
}
}
return defaultOptions;
}
private Map<String, Object> getDefaultLlmOptions() {
Map<String, Object> defaultLlmOptions = new HashMap<>();
defaultLlmOptions.put("temperature", 0.7);
defaultLlmOptions.put("topK", 4);
defaultLlmOptions.put("maxReplyLength", 2048);
defaultLlmOptions.put("topP", 0.7);
defaultLlmOptions.put("maxMessageCount", 10);
return defaultLlmOptions;
}
private Map<String, Object> errorRespnseMsg(int errorCode, String message) {
HashMap<String, Object> result = new HashMap<>();
result.put("error", errorCode);
result.put("message", message);
return result;
}
@Override
protected Result<?> onRemoveBefore(Collection<Serializable> ids) {
QueryWrapper queryWrapperKnowledge = QueryWrapper.create().in(BotDocumentCollection::getBotId, ids);
botDocumentCollectionService.remove(queryWrapperKnowledge);
QueryWrapper queryWrapperBotWorkflow = QueryWrapper.create().in(BotWorkflow::getBotId, ids);
botWorkflowService.remove(queryWrapperBotWorkflow);
QueryWrapper queryWrapperBotPlugins = QueryWrapper.create().in(BotPlugin::getBotId, ids);
botPluginService.remove(queryWrapperBotPlugins);
return super.onRemoveBefore(ids);
}
/**
* 系统提示词优化
*
* @param prompt
* @return
*/
@PostMapping("prompt/chore/chat")
@SaIgnore
public SseEmitter chat(
@JsonBody(value = "prompt", required = true) String prompt,
@JsonBody(value = "botId", required = true) BigInteger botId
){
if (!StringUtils.hasLength(prompt)) {
throw new BusinessException("提示词不能为空!");
}
String promptChore = "# 角色与目标\n" +
"\n" +
"你是一位专业的提示词工程师Prompt Engineer。你的任务是分析我提供的“用户原始提示词”并将其优化成一个结构清晰、指令明确、效果最优的“系统提示词System Prompt”。\n" +
"\n" +
"这个优化后的系统提示词将直接用于引导一个AI助手使其能够精准、高效地完成用户的请求。\n" +
"\n" +
"# 优化指南 (请严格遵循)\n" +
"\n" +
"在优化过程中,请遵循以下原则,以确保最终提示词的质量:\n" +
"\n" +
"1. **角色定义 (Role Definition)**\n" +
" * 为AI助手明确一个具体、专业的角色。这个角色应该与任务高度相关。\n" +
" * 例如:“你是一位资深的软件架构师”、“你是一位经验丰富的产品经理”。\n" +
"\n" +
"2. **任务与目标 (Task & Goal)**\n" +
" * 清晰、具体地描述AI需要完成的任务。\n" +
" * 明确指出期望的最终输出是什么,以及输出的目标和用途。\n" +
" * 避免模糊不清的指令。\n" +
"\n" +
"3. **输入输出格式 (Input/Output Format)**\n" +
" * 如果任务涉及到处理特定格式的数据,请明确说明输入数据的格式。\n" +
" * **至关重要**请为AI的输出指定一个清晰、结构化的格式。这能极大地提升结果的可用性。\n" +
" * 例如“请以Markdown表格的形式输出”、“请分点列出每点不超过20字”。\n" +
"\n" +
"4. **背景与上下文 (Context & Background)**\n" +
" * 提供完成任务所必需的背景信息。\n" +
" * 例如:项目的阶段、目标用户、使用的技术栈、相关的约束条件等。\n" +
"\n" +
"5. **语气与风格 (Tone & Style)**\n" +
" * 指定AI回答时应采用的语气和风格。\n" +
" * 例如:“专业且简洁”、“通俗易懂,避免使用专业术语”。\n" +
"\n" +
"6. **约束与规则 (Constraints & Rules)**\n" +
" * 设定AI在回答时必须遵守的规则和限制。\n" +
" * 例如:“回答必须基于提供的文档”、“禁止猜测用户未提及的信息”。\n" +
"\n" +
"7. **思考过程 (Chain-of-Thought)**\n" +
" * 对于复杂的推理任务可以引导AI展示其思考过程。\n" +
" * 例如:“请先分析问题的关键点,然后给出解决方案,并解释你的推理步骤。”\n" +
"\n" +
"# 输出要求\n" +
"\n" +
"请你直接输出优化后的完整系统提示词。不要包含任何额外的解释或说明。\n" +
"\n" +
"---\n" +
"\n" +
"## 用户原始提示词\n" +
"[" + prompt + "]\n";
Bot aiBot = service.getById(botId);
if (aiBot == null) {
return ChatSseUtil.sendSystemError(null, "聊天助手不存在");
}
ChatSseEmitter sseEmitter = new ChatSseEmitter();
Model model = modelService.getModelInstance(aiBot.getModelId());
if (model == null) {
return ChatSseUtil.sendSystemError(null, "模型不存在");
}
ChatModel chatModel = model.toChatModel();
if (chatModel == null) {
return ChatSseUtil.sendSystemError(null, "模型不存在");
}
PromptChoreChatStreamListener promptChoreChatStreamListener = new PromptChoreChatStreamListener(sseEmitter);
chatModel.chatStream(promptChore, promptChoreChatStreamListener);
return sseEmitter.getEmitter();
}
}

View File

@@ -0,0 +1,46 @@
package tech.easyflow.admin.controller.ai;
import org.springframework.web.bind.annotation.PostMapping;
import tech.easyflow.ai.entity.BotDocumentCollection;
import tech.easyflow.ai.service.BotDocumentCollectionService;
import tech.easyflow.common.annotation.UsePermission;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.web.controller.BaseCurdController;
import com.mybatisflex.core.query.QueryWrapper;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import tech.easyflow.common.web.jsonbody.JsonBody;
import java.math.BigInteger;
import java.util.List;
/**
* 控制层。
*
* @author michael
* @since 2024-08-28
*/
@RestController
@RequestMapping("/api/v1/botKnowledge")
@UsePermission(moduleName = "/api/v1/bot")
public class BotDocumentCollectionController extends BaseCurdController<BotDocumentCollectionService, BotDocumentCollection> {
public BotDocumentCollectionController(BotDocumentCollectionService service) {
super(service);
}
@GetMapping("list")
@Override
public Result<List<BotDocumentCollection>> list(BotDocumentCollection entity, Boolean asTree, String sortKey, String sortType) {
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
List<BotDocumentCollection> botDocumentCollections = service.getMapper().selectListWithRelationsByQuery(queryWrapper);
return Result.ok(botDocumentCollections);
}
@PostMapping("updateBotKnowledgeIds")
public Result<?> save(@JsonBody("botId") BigInteger botId, @JsonBody("knowledgeIds") BigInteger [] knowledgeIds) {
service.saveBotAndKnowledge(botId, knowledgeIds);
return Result.ok();
}
}

View File

@@ -0,0 +1,40 @@
package tech.easyflow.admin.controller.ai;
import org.springframework.web.bind.annotation.PostMapping;
import tech.easyflow.common.annotation.UsePermission;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.web.controller.BaseCurdController;
import tech.easyflow.ai.entity.BotMcp;
import tech.easyflow.ai.service.BotMcpService;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.web.jsonbody.JsonBody;
import java.math.BigInteger;
import java.util.List;
import java.util.Map;
/**
* 控制层。
*
* @author wangGangQiang
* @since 2026-01-05
*/
@RestController
@RequestMapping("/api/v1/botMcp")
@UsePermission(moduleName = "/api/v1/bot")
public class BotMcpController extends BaseCurdController<BotMcpService, BotMcp> {
public BotMcpController(BotMcpService service) {
super(service);
}
@PostMapping("updateBotMcpToolIds")
public Result<?> save(@JsonBody("botId") BigInteger botId,
@JsonBody("mcpSelectedData") List<Map<String, List<List<String>>>> mcpSelectedData) {
service.updateBotMcpToolIds(botId, mcpSelectedData);
return Result.ok();
}
}

View File

@@ -0,0 +1,35 @@
package tech.easyflow.admin.controller.ai;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import tech.easyflow.ai.entity.BotMessage;
import tech.easyflow.ai.service.BotMessageService;
import tech.easyflow.common.annotation.UsePermission;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.common.web.controller.BaseCurdController;
/**
* Bot 消息记录表 控制层。
*
* @author michael
* @since 2024-11-04
*/
@RestController
@RequestMapping("/api/v1/botMessage")
@UsePermission(moduleName = "/api/v1/bot")
public class BotMessageController extends BaseCurdController<BotMessageService, BotMessage> {
private final BotMessageService botMessageService;
public BotMessageController(BotMessageService service, BotMessageService botMessageService) {
super(service);
this.botMessageService = botMessageService;
}
@Override
protected Result<?> onSaveOrUpdateBefore(BotMessage entity, boolean isSave) {
entity.setAccountId(SaTokenUtil.getLoginAccount().getId());
return super.onSaveOrUpdateBefore(entity, isSave);
}
}

View File

@@ -0,0 +1,23 @@
package tech.easyflow.admin.controller.ai;
import tech.easyflow.ai.entity.BotModel;
import tech.easyflow.ai.service.BotModelService;
import tech.easyflow.common.annotation.UsePermission;
import tech.easyflow.common.web.controller.BaseCurdController;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 控制层。
*
* @author michael
* @since 2024-08-28
*/
@RestController
@RequestMapping("/api/v1/botLlm")
@UsePermission(moduleName = "/api/v1/bot")
public class BotModelController extends BaseCurdController<BotModelService, BotModel> {
public BotModelController(BotModelService service) {
super(service);
}
}

View File

@@ -0,0 +1,74 @@
package tech.easyflow.admin.controller.ai;
import org.springframework.web.bind.annotation.PostMapping;
import tech.easyflow.ai.entity.Plugin;
import tech.easyflow.ai.entity.BotPlugin;
import tech.easyflow.common.annotation.UsePermission;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.tree.Tree;
import tech.easyflow.common.web.controller.BaseCurdController;
import tech.easyflow.ai.service.BotPluginService;
import com.mybatisflex.core.query.QueryWrapper;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import tech.easyflow.common.web.jsonbody.JsonBody;
import javax.annotation.Resource;
import java.math.BigInteger;
import java.util.List;
/**
* 控制层。
*
* @author michael
* @since 2025-04-07
*/
@RestController
@RequestMapping("/api/v1/botPlugins")
@UsePermission(moduleName = "/api/v1/bot")
public class BotPluginController extends BaseCurdController<BotPluginService, BotPlugin> {
public BotPluginController(BotPluginService service) {
super(service);
}
@Resource
private BotPluginService botPluginService;
@GetMapping("list")
public Result<List<BotPlugin>> list(BotPlugin entity, Boolean asTree, String sortKey, String sortType){
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
List<BotPlugin> botPlugins = service.getMapper().selectListWithRelationsByQuery(queryWrapper);
List<BotPlugin> list = Tree.tryToTree(botPlugins, asTree);
return Result.ok(list);
}
@PostMapping("/getList")
public Result<List<Plugin>> getList(@JsonBody(value = "botId", required = true) String botId){
return Result.ok(botPluginService.getList(botId));
}
@PostMapping("/getBotPluginToolIds")
public Result<List<BigInteger>> getBotPluginToolIds(@JsonBody(value = "botId", required = true) String botId){
return Result.ok(botPluginService.getBotPluginToolIds(botId));
}
@PostMapping("/doRemove")
public Result<Boolean> doRemove(@JsonBody(value = "botId", required = true) String botId,
@JsonBody(value = "pluginToolId", required = true) String pluginToolId){
return Result.ok(botPluginService.doRemove(botId, pluginToolId));
}
@PostMapping("updateBotPluginToolIds")
public Result<?> save(@JsonBody("botId") BigInteger botId, @JsonBody("pluginToolIds") BigInteger [] pluginToolIds) {
service.saveBotAndPluginTool(botId, pluginToolIds);
return Result.ok();
}
}

View File

@@ -0,0 +1,23 @@
package tech.easyflow.admin.controller.ai;
import tech.easyflow.ai.entity.BotRecentlyUsed;
import tech.easyflow.ai.service.BotRecentlyUsedService;
import tech.easyflow.common.annotation.UsePermission;
import tech.easyflow.common.web.controller.BaseCurdController;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 最近使用 控制层。
*
* @author ArkLight
* @since 2025-12-18
*/
@RestController
@RequestMapping("/api/v1/botRecentlyUsed")
@UsePermission(moduleName = "/api/v1/bot")
public class BotRecentlyUsedController extends BaseCurdController<BotRecentlyUsedService, BotRecentlyUsed> {
public BotRecentlyUsedController(BotRecentlyUsedService service) {
super(service);
}
}

View File

@@ -0,0 +1,48 @@
package tech.easyflow.admin.controller.ai;
import org.springframework.web.bind.annotation.PostMapping;
import tech.easyflow.ai.entity.BotWorkflow;
import tech.easyflow.ai.service.BotWorkflowService;
import tech.easyflow.common.annotation.UsePermission;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.tree.Tree;
import tech.easyflow.common.web.controller.BaseCurdController;
import com.mybatisflex.core.query.QueryWrapper;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import tech.easyflow.common.web.jsonbody.JsonBody;
import java.math.BigInteger;
import java.util.List;
/**
* 控制层。
*
* @author michael
* @since 2024-08-28
*/
@RestController
@RequestMapping("/api/v1/botWorkflow")
@UsePermission(moduleName = "/api/v1/bot")
public class BotWorkflowController extends BaseCurdController<BotWorkflowService, BotWorkflow> {
public BotWorkflowController(BotWorkflowService service) {
super(service);
}
@GetMapping("list")
@Override
public Result<List<BotWorkflow>> list(BotWorkflow entity, Boolean asTree, String sortKey, String sortType) {
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
List<BotWorkflow> botWorkflows = service.getMapper().selectListWithRelationsByQuery(queryWrapper);
List<BotWorkflow> list = Tree.tryToTree(botWorkflows, asTree);
return Result.ok(list);
}
@PostMapping("updateBotWorkflowIds")
public Result<?> save(@JsonBody("botId") BigInteger botId, @JsonBody("workflowIds") BigInteger [] workflowIds) {
service.saveBotAndWorkflowTool(botId, workflowIds);
return Result.ok();
}
}

View File

@@ -0,0 +1,118 @@
package tech.easyflow.admin.controller.ai;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.easyagents.core.model.embedding.EmbeddingModel;
import tech.easyflow.ai.entity.DocumentChunk;
import tech.easyflow.ai.entity.DocumentCollection;
import tech.easyflow.ai.entity.Model;
import tech.easyflow.ai.service.DocumentChunkService;
import tech.easyflow.ai.service.DocumentCollectionService;
import tech.easyflow.ai.service.ModelService;
import tech.easyflow.common.annotation.UsePermission;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.web.controller.BaseCurdController;
import tech.easyflow.common.web.jsonbody.JsonBody;
import com.easyagents.core.document.Document;
import com.easyagents.core.store.DocumentStore;
import com.easyagents.core.store.StoreOptions;
import com.easyagents.core.store.StoreResult;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 控制层。
*
* @author michael
* @since 2024-08-23
*/
@RestController
@RequestMapping("/api/v1/documentChunk")
@UsePermission(moduleName = "/api/v1/documentCollection")
public class DocumentChunkController extends BaseCurdController<DocumentChunkService, DocumentChunk> {
@Resource
DocumentCollectionService documentCollectionService;
@Resource
ModelService modelService;
@Resource
DocumentChunkService documentChunkService;
public DocumentChunkController(DocumentChunkService service) {
super(service);
}
@PostMapping("update")
@SaCheckPermission("/api/v1/documentCollection/save")
public Result<?> update(@JsonBody DocumentChunk documentChunk) {
boolean success = service.updateById(documentChunk);
if (success){
DocumentChunk record = documentChunkService.getById(documentChunk.getId());
DocumentCollection knowledge = documentCollectionService.getById(record.getDocumentCollectionId());
if (knowledge == null) {
return Result.fail(1, "知识库不存在");
}
DocumentStore documentStore = knowledge.toDocumentStore();
if (documentStore == null) {
return Result.fail(2, "知识库没有配置向量库");
}
// 设置向量模型
Model model = modelService.getModelInstance(knowledge.getVectorEmbedModelId());
if (model == null) {
return Result.fail(3, "知识库没有配置向量模型");
}
EmbeddingModel embeddingModel = model.toEmbeddingModel();
documentStore.setEmbeddingModel(embeddingModel);
StoreOptions options = StoreOptions.ofCollectionName(knowledge.getVectorStoreCollection());
Document document = Document.of(documentChunk.getContent());
document.setId(documentChunk.getId());
Map<String, Object> metadata = new HashMap<>();
metadata.put("keywords", documentChunk.getMetadataKeyWords());
metadata.put("questions", documentChunk.getMetadataQuestions());
document.setMetadataMap(metadata);
StoreResult result = documentStore.update(document, options); // 更新已有记录
return Result.ok(result);
}
return Result.ok(false);
}
@PostMapping("removeChunk")
@SaCheckPermission("/api/v1/documentCollection/remove")
public Result<?> remove(@JsonBody(value = "id", required = true) BigInteger chunkId) {
DocumentChunk docChunk = documentChunkService.getById(chunkId);
if (docChunk == null) {
return Result.fail(1, "记录不存在");
}
DocumentCollection knowledge = documentCollectionService.getById(docChunk.getDocumentCollectionId());
if (knowledge == null) {
return Result.fail(2, "知识库不存在");
}
DocumentStore documentStore = knowledge.toDocumentStore();
if (documentStore == null) {
return Result.fail(3, "知识库没有配置向量库");
}
// 设置向量模型
Model model = modelService.getModelInstance(knowledge.getVectorEmbedModelId());
if (model == null) {
return Result.fail(4, "知识库没有配置向量模型");
}
EmbeddingModel embeddingModel = model.toEmbeddingModel();
documentStore.setEmbeddingModel(embeddingModel);
StoreOptions options = StoreOptions.ofCollectionName(knowledge.getVectorStoreCollection());
List<BigInteger> deleteList = new ArrayList<>();
deleteList.add(chunkId);
documentStore.delete(deleteList, options);
documentChunkService.removeChunk(knowledge, chunkId);
return super.remove(chunkId);
}
}

View File

@@ -0,0 +1,54 @@
package tech.easyflow.admin.controller.ai;
import com.mybatisflex.core.query.QueryWrapper;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import tech.easyflow.ai.entity.DocumentCollection;
import tech.easyflow.ai.entity.DocumentCollectionCategory;
import tech.easyflow.ai.entity.WorkflowCategory;
import tech.easyflow.ai.mapper.DocumentCollectionMapper;
import tech.easyflow.ai.service.DocumentCollectionCategoryService;
import tech.easyflow.ai.service.DocumentCollectionService;
import tech.easyflow.ai.service.WorkflowCategoryService;
import tech.easyflow.common.annotation.UsePermission;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.web.controller.BaseCurdController;
import tech.easyflow.common.web.exceptions.BusinessException;
import javax.annotation.Resource;
import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
/**
* 控制层。
*
* @author wangGangQiang
* @since 2026-01-23
*/
@RestController
@RequestMapping("/api/v1/documentCollectionCategory")
@UsePermission(moduleName = "/api/v1/documentCollection")
public class DocumentCollectionCategoryController extends BaseCurdController<DocumentCollectionCategoryService, DocumentCollectionCategory> {
@Resource
private DocumentCollectionMapper documentCollectionMapper;
public DocumentCollectionCategoryController(DocumentCollectionCategoryService service) {
super(service);
}
@Override
protected Result<?> onRemoveBefore(Collection<Serializable> ids) {
ids.forEach(id -> {
QueryWrapper queryWrapper = QueryWrapper.create().eq(DocumentCollection::getCategoryId, id);
List<DocumentCollection> documentCollections = documentCollectionMapper.selectListByQuery(queryWrapper);
if (!documentCollections.isEmpty()) {
throw new BusinessException("请先删除该分类下的所有知识库");
}
});
return super.onRemoveBefore(ids);
}
}

View File

@@ -0,0 +1,108 @@
package tech.easyflow.admin.controller.ai;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.easyagents.core.document.Document;
import com.mybatisflex.core.query.QueryWrapper;
import org.springframework.util.StringUtils;
import tech.easyflow.ai.entity.BotDocumentCollection;
import tech.easyflow.ai.entity.DocumentCollection;
import tech.easyflow.ai.service.BotDocumentCollectionService;
import tech.easyflow.ai.service.DocumentChunkService;
import tech.easyflow.ai.service.DocumentCollectionService;
import tech.easyflow.ai.service.ModelService;
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.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import tech.easyflow.common.web.exceptions.BusinessException;
import javax.annotation.Resource;
import java.io.Serializable;
import java.math.BigInteger;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 控制层。
*
* @author michael
* @since 2024-08-23
*/
@RestController
@RequestMapping("/api/v1/documentCollection")
public class DocumentCollectionController extends BaseCurdController<DocumentCollectionService, DocumentCollection> {
private final DocumentChunkService chunkService;
private final ModelService llmService;
@Resource
private BotDocumentCollectionService botDocumentCollectionService;
public DocumentCollectionController(DocumentCollectionService service, DocumentChunkService chunkService, ModelService llmService) {
super(service);
this.chunkService = chunkService;
this.llmService = llmService;
}
@Override
protected Result<?> onSaveOrUpdateBefore(DocumentCollection entity, boolean isSave) {
String alias = entity.getAlias();
if (StringUtils.hasLength(alias)){
DocumentCollection knowledge = service.getByAlias(alias);
if (knowledge != null && isSave){
throw new BusinessException("别名已存在!");
}
if (knowledge != null && knowledge.getId().compareTo(entity.getId()) != 0){
throw new BusinessException("别名已存在!");
}
} else {
entity.setAlias(null);
}
if (isSave){
Map<String, Object> options = new HashMap<>();
if (entity.getSearchEngineEnable() == null){
entity.setSearchEngineEnable(false);
}
options.put("canUpdateEmbeddingModel", true);
entity.setOptions(options);
}
return super.onSaveOrUpdateBefore(entity, isSave);
}
@GetMapping("search")
@SaCheckPermission("/api/v1/documentCollection/query")
public Result<List<Document>> search(@RequestParam BigInteger knowledgeId, @RequestParam String keyword) {
return Result.ok(service.search(knowledgeId, keyword));
}
@Override
protected Result<Void> onRemoveBefore(Collection<Serializable> ids) {
QueryWrapper queryWrapper = QueryWrapper.create();
queryWrapper.in(BotDocumentCollection::getId, ids);
boolean exists = botDocumentCollectionService.exists(queryWrapper);
if (exists){
throw new BusinessException("此知识库还关联着bot请先取消关联");
}
return null;
}
@Override
public Result<DocumentCollection> detail(String id) {
return Result.ok(service.getDetail(id));
}
}

View File

@@ -0,0 +1,201 @@
package tech.easyflow.admin.controller.ai;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.mybatisflex.core.paginate.Page;
import com.mybatisflex.core.query.QueryWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.MediaType;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
import tech.easyflow.ai.entity.Document;
import tech.easyflow.ai.entity.DocumentCollection;
import tech.easyflow.ai.entity.DocumentCollectionSplitParams;
import tech.easyflow.ai.service.DocumentChunkService;
import tech.easyflow.ai.service.DocumentCollectionService;
import tech.easyflow.ai.service.DocumentService;
import tech.easyflow.ai.service.ModelService;
import tech.easyflow.common.annotation.UsePermission;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.tree.Tree;
import tech.easyflow.common.util.RequestUtil;
import tech.easyflow.common.util.StringUtil;
import tech.easyflow.common.web.controller.BaseCurdController;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.common.web.jsonbody.JsonBody;
import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* 控制层。
*
* @author michael
* @since 2024-08-23
*/
@RestController
@RequestMapping("/api/v1/document")
@UsePermission(moduleName = "/api/v1/documentCollection")
public class DocumentController extends BaseCurdController<DocumentService, Document> {
private final DocumentCollectionService knowledgeService;
@Autowired
private DocumentService documentService;
@Value("${easyflow.storage.local.root}")
private String fileUploadPath;
public DocumentController(DocumentService service,
DocumentCollectionService knowledgeService,
DocumentChunkService documentChunkService, ModelService modelService) {
super(service);
this.knowledgeService = knowledgeService;
}
@PostMapping("removeDoc")
@Transactional
@SaCheckPermission("/api/v1/documentCollection/remove")
public Result<?> remove(@JsonBody(value = "id", required = true) String id) {
List<Serializable> ids = Collections.singletonList(id);
Result<?> result = onRemoveBefore(ids);
if (result != null) return result;
boolean isSuccess = documentService.removeDoc(id);
if (!isSuccess){
return Result.ok(false);
}
boolean success = service.removeById(id);
onRemoveAfter(ids);
return Result.ok(success);
}
/**
* 查询所有所有数据
*
* @param entity
* @param asTree
* @param sortKey
* @param sortType
* @return 所有数据
*/
@GetMapping("list")
@Override
@SaCheckPermission("/api/v1/documentCollection/query")
public Result<List<Document>> list(Document entity, Boolean asTree, String sortKey, String sortType) {
String kbSlug = RequestUtil.getParamAsString("id");
if (StringUtil.noText(kbSlug)) {
throw new BusinessException("知识库id不能为空");
}
DocumentCollection knowledge = StringUtil.isNumeric(kbSlug)
? knowledgeService.getById(kbSlug)
: knowledgeService.getOne(QueryWrapper.create().eq(DocumentCollection::getSlug, kbSlug));
if (knowledge == null) {
throw new BusinessException("知识库不存在");
}
QueryWrapper queryWrapper = QueryWrapper.create()
.eq(Document::getCollectionId, knowledge.getId());
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
List<Document> documents = service.list(queryWrapper);
List<Document> list = Tree.tryToTree(documents, asTree);
return Result.ok(list);
}
@GetMapping("documentList")
@SaCheckPermission("/api/v1/documentCollection/query")
public Result<Page<Document>> documentList(@RequestParam(name="title", required = false) String fileName, @RequestParam(name="pageSize") int pageSize, @RequestParam(name = "pageNumber") int pageNumber) {
String kbSlug = RequestUtil.getParamAsString("id");
if (StringUtil.noText(kbSlug)) {
throw new BusinessException("知识库id不能为空");
}
Page<Document> documentList = documentService.getDocumentList(kbSlug, pageSize, pageNumber,fileName);
return Result.ok(documentList);
}
@Override
protected String getDefaultOrderBy() {
return "order_no asc";
}
@PostMapping("update")
@Override
@SaCheckPermission("/api/v1/documentCollection/save")
public Result<Boolean> update(@JsonBody Document entity) {
super.update(entity);
return Result.ok(updatePosition(entity));
}
/**
* 文本拆分/保存
*
*/
@PostMapping(value = {"textSplit", "/saveText"})
@SaCheckPermission("/api/v1/documentCollection/save")
public Result<?> textSplit(@JsonBody DocumentCollectionSplitParams documentCollectionSplitParams) {
return documentService.textSplit(documentCollectionSplitParams);
}
/**
* 更新 entity
*
* @param entity
* @return Result
*/
private boolean updatePosition(Document entity) {
Integer orderNo = entity.getOrderNo();
if (orderNo != null) {
if (orderNo <= 0) orderNo = 0;
BigInteger knowledgeId = service.getById(entity.getId()).getCollectionId();
List<Document> list = service.list(QueryWrapper.create()
.eq(Document::getCollectionId, knowledgeId)
.orderBy(getDefaultOrderBy())
);
list.removeIf(item -> item.getId().equals(entity.getId()));
if (orderNo >= list.size()) {
list.add(entity);
} else {
list.add(orderNo, entity);
}
List<Document> updateList = new ArrayList<>();
for (int i = 0; i < list.size(); i++) {
Document updateItem = new Document();
updateItem.setId(list.get(i).getId());
updateItem.setOrderNo(i);
updateList.add(updateItem);
}
service.updateBatch(updateList);
}
return true;
}
public String getRootPath() {
if (StringUtil.hasText(this.fileUploadPath)) {
return this.fileUploadPath;
}
ClassPathResource fileResource = new ClassPathResource("/");
try {
return new File(fileResource.getFile(), "/public").getAbsolutePath();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -0,0 +1,21 @@
package tech.easyflow.admin.controller.ai;
import tech.easyflow.ai.entity.DocumentHistory;
import tech.easyflow.ai.service.DocumentHistoryService;
import tech.easyflow.common.web.controller.BaseCurdController;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 控制层。
*
* @author michael
* @since 2024-08-23
*/
@RestController
@RequestMapping("/api/v1/documentHistory")
public class DocumentHistoryController extends BaseCurdController<DocumentHistoryService, DocumentHistory> {
public DocumentHistoryController(DocumentHistoryService service) {
super(service);
}
}

View File

@@ -0,0 +1,73 @@
package tech.easyflow.admin.controller.ai;
import cn.dev33.satoken.annotation.SaIgnore;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.HandlerMapping;
import tech.easyflow.common.util.StringUtil;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
@RestController
public class FilePreviewController {
// 定义图片存储的基础路径
@Value("${easyflow.storage.local.root}")
private String fileUploadPath;
@SaIgnore
@GetMapping("/api/images/**")
public ResponseEntity<byte[]> getImage(HttpServletRequest request) throws IOException {
// 获取完整的路径
String fullPath = (String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE);
String basePath = "/api/images/";
String filePath = fullPath.substring(basePath.length()-1);
String imagePath = getRootPath() + filePath;
File imageFile = new File(imagePath);
if (!imageFile.exists()) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
// 读取文件内容
byte[] fileContent = Files.readAllBytes(imageFile.toPath());
// 返回文件内容
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_TYPE, getContentType(imageFile))
.body(fileContent);
}
// 根据文件扩展名获取 MIME 类型
private String getContentType(File file) {
String fileName = file.getName().toLowerCase();
if (fileName.endsWith(".jpg") || fileName.endsWith(".jpeg")) {
return "image/jpeg";
} else if (fileName.endsWith(".png")) {
return "image/png";
} else if (fileName.endsWith(".gif")) {
return "image/gif";
}
return "application/octet-stream"; // 默认类型
}
public String getRootPath() {
if (StringUtil.hasText(this.fileUploadPath)) {
return this.fileUploadPath;
}
ClassPathResource fileResource = new ClassPathResource("/");
try {
return new File(fileResource.getFile(), "/public").getAbsolutePath();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -0,0 +1,83 @@
package tech.easyflow.admin.controller.ai;
import com.mybatisflex.core.paginate.Page;
import com.mybatisflex.core.query.QueryWrapper;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import tech.easyflow.ai.entity.BotMcp;
import tech.easyflow.ai.entity.Mcp;
import tech.easyflow.ai.service.BotMcpService;
import tech.easyflow.ai.service.McpService;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.web.controller.BaseCurdController;
import tech.easyflow.common.web.jsonbody.JsonBody;
import javax.annotation.Resource;
import java.io.Serializable;
/**
* 控制层。
*
* @author wangGangQiang
* @since 2026-01-04
*/
@RestController
@RequestMapping("/api/v1/mcp")
public class McpController extends BaseCurdController<McpService, Mcp> {
public McpController(McpService service) {
super(service);
}
@Resource
private BotMcpService botMcpService;
@Override
public Result<?> save(Mcp entity) {
return service.saveMcp(entity);
}
@Override
public Result<?> update(Mcp entity) {
return service.updateMcp(entity);
}
@Override
@Transactional
public Result<?> remove(Serializable id) {
service.removeMcp(id);
botMcpService.remove(QueryWrapper.create().eq(BotMcp::getMcpId, id));
return Result.ok();
}
@Override
public Result<Page<Mcp>> page(HttpServletRequest request, String sortKey, String sortType, Long pageNumber, Long pageSize) {
Result<Page<Mcp>> page = super.page(request, sortKey, sortType, pageNumber, pageSize);
return service.pageMcp(page);
}
@PostMapping("/getMcpTools")
public Result<Mcp> getMcpTools(@JsonBody("id") String id) {
return Result.ok(service.getMcpTools(id));
}
@GetMapping("pageTools")
public Result<Page<Mcp>> pageTools(HttpServletRequest request, String sortKey, String sortType, Long pageNumber, Long pageSize) {
if (pageNumber == null || pageNumber < 1) {
pageNumber = 1L;
}
if (pageSize == null || pageSize < 1) {
pageSize = 10L;
}
QueryWrapper queryWrapper = buildQueryWrapper(request);
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
Page<Mcp> mcpPage = queryPage(new Page<>(pageNumber, pageSize), queryWrapper);
return Result.ok(service.pageTools(mcpPage));
}
}

View File

@@ -0,0 +1,174 @@
package tech.easyflow.admin.controller.ai;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.mybatisflex.core.BaseMapper;
import com.mybatisflex.core.query.QueryWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import tech.easyflow.ai.entity.Model;
import tech.easyflow.ai.entity.ModelProvider;
import tech.easyflow.ai.entity.table.ModelTableDef;
import tech.easyflow.ai.mapper.ModelMapper;
import tech.easyflow.ai.service.ModelService;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.common.tree.Tree;
import tech.easyflow.common.web.controller.BaseCurdController;
import tech.easyflow.common.web.jsonbody.JsonBody;
import javax.annotation.Resource;
import java.io.Serializable;
import java.math.BigInteger;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* 控制层。
*
* @author michael
* @since 2024-08-23
*/
@RestController
@RequestMapping("/api/v1/model")
public class ModelController extends BaseCurdController<ModelService, Model> {
public ModelController(ModelService service) {
super(service);
}
@Autowired
ModelService modelService;
@Resource
ModelMapper modelMapper;
@GetMapping("list")
@SaCheckPermission("/api/v1/model/query")
public Result<List<Model>> list(Model entity, Boolean asTree, String sortKey, String sortType) {
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
List<Model> list = Tree.tryToTree(modelMapper.selectListWithRelationsByQuery(queryWrapper), asTree);
list.forEach(item -> {
String providerName = Optional.ofNullable(item.getModelProvider())
.map(ModelProvider::getProviderName)
.orElse("-");
item.setTitle(providerName + "/" + item.getTitle());
});
return Result.ok(list);
}
@GetMapping("getList")
@SaCheckPermission("/api/v1/model/query")
public Result<Map<String, Map<String, List<Model>>>> getList(Model entity) {
return Result.ok(modelService.getList(entity));
}
@PostMapping("/addAiLlm")
@SaCheckPermission("/api/v1/model/save")
public Result<Boolean> addAiLlm(Model entity) {
LoginAccount account = SaTokenUtil.getLoginAccount();
commonFiled(entity, account.getId(), account.getTenantId(), account.getDeptId());
return Result.ok(modelService.addAiLlm(entity));
}
@GetMapping("verifyLlmConfig")
@SaCheckPermission("/api/v1/model/save")
public Result<?> verifyLlmConfig(@RequestParam BigInteger id) {
Model model = service.getModelInstance(id);
return Result.ok(modelService.verifyModelConfig(model));
}
@PostMapping("/removeByEntity")
@SaCheckPermission("/api/v1/model/remove")
public Result<?> removeByEntity(@RequestBody Model entity) {
modelService.removeByEntity(entity);
return Result.ok();
}
@PostMapping("/updateByEntity")
@SaCheckPermission("/api/v1/model/save")
public Result<?> updateByEntity(@RequestBody Model entity) {
modelService.updateByEntity(entity);
return Result.ok();
}
@GetMapping("/selectLlmByProviderCategory")
@SaCheckPermission("/api/v1/model/query")
public Result<Map<String, List<Model>>> selectLlmByProviderCategory(Model entity, String sortKey, String sortType) {
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
BaseMapper<Model> mapper = service.getMapper();
List<Model> totalList = mapper.selectListWithRelationsByQuery(queryWrapper);
Map<String, List<Model>> groupList = totalList.stream().collect(Collectors.groupingBy(Model::getGroupName));
return Result.ok(groupList);
}
@GetMapping("/selectLlmByProviderAndModelType")
@SaCheckPermission("/api/v1/model/query")
public Result<Map<String, List<Model>>> selectLlmByProviderAndModelType(
@RequestParam String modelType,
@RequestParam BigInteger providerId,
@RequestParam(required = false) String selectText
) {
QueryWrapper queryWrapper = QueryWrapper.create();
queryWrapper.eq(Model::getProviderId, providerId);
queryWrapper.eq(Model::getModelType, modelType);
if (StringUtils.hasLength(selectText)) {
queryWrapper.and(ModelTableDef.MODEL.TITLE.like(selectText).or(ModelTableDef.MODEL.MODEL_NAME.like(selectText)));
}
List<Model> totalList = service.getMapper().selectListWithRelationsByQuery(queryWrapper);
Map<String, List<Model>> groupList = totalList.stream().collect(Collectors.groupingBy(Model::getGroupName));
return Result.ok(groupList);
}
/**
* 添加所有模型
*
* @param entity
* @return
*/
@PostMapping("addAllLlm")
public Result<?> addAllLlm(@JsonBody Model entity) {
QueryWrapper queryWrapper = QueryWrapper.create().eq(Model::getProviderId, entity.getProviderId());
service.update(entity, queryWrapper);
return Result.ok();
}
@GetMapping("/selectLlmList")
@SaCheckPermission("/api/v1/model/query")
public Result<List<Model>> selectLlmList(Model entity, Boolean asTree, String sortKey, String sortType) {
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
List<Model> totalList = Tree.tryToTree(modelMapper.selectListWithRelationsByQuery(queryWrapper), asTree);
totalList.forEach(aiLlm -> {
aiLlm.setTitle(aiLlm.getModelProvider().getProviderName() + "/" + aiLlm.getTitle());
});
return Result.ok(totalList);
}
@Override
protected Result<?> onSaveOrUpdateBefore(Model entity, boolean isSave) {
if (isSave) {
entity.setWithUsed(true);
}
return super.onSaveOrUpdateBefore(entity, isSave);
}
@PostMapping("removeLlmByIds")
@Transactional
public Result<?> removeLlm(@JsonBody(value = "id", required = true) Serializable id) {
List<Serializable> ids = Collections.singletonList(id);
QueryWrapper queryWrapper = QueryWrapper.create().in(Model::getId, ids);
service.remove(queryWrapper);
return Result.ok();
}
}

View File

@@ -0,0 +1,48 @@
package tech.easyflow.admin.controller.ai;
import com.mybatisflex.core.query.QueryWrapper;
import org.springframework.transaction.annotation.Transactional;
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.entity.Model;
import tech.easyflow.ai.entity.ModelProvider;
import tech.easyflow.ai.service.ModelProviderService;
import tech.easyflow.ai.service.ModelService;
import tech.easyflow.common.annotation.UsePermission;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.web.controller.BaseCurdController;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.common.web.jsonbody.JsonBody;
import java.io.Serializable;
/**
* 控制层。
*
* @author 12076
* @since 2025-12-16
*/
@RestController
@RequestMapping("/api/v1/modelProvider")
@UsePermission(moduleName = "/api/v1/model")
public class ModelProviderController extends BaseCurdController<ModelProviderService, ModelProvider> {
private final ModelService modelService;
public ModelProviderController(ModelProviderService service, ModelService modelService) {
super(service);
this.modelService = modelService;
}
@Override
@PostMapping("remove")
@Transactional
public Result<?> remove(@JsonBody(value = "id", required = true) Serializable id) {
QueryWrapper modelQueryWrapper = QueryWrapper.create()
.in(Model::getProviderId, id);
if (modelService.count(modelQueryWrapper) > 0) {
throw new BusinessException("Delete all child models before removing this Model Provider");
}
return Result.ok(service.removeById(id));
}
}

View File

@@ -0,0 +1,40 @@
package tech.easyflow.admin.controller.ai;
import cn.dev33.satoken.annotation.SaCheckPermission;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import tech.easyflow.ai.entity.PluginCategory;
import tech.easyflow.ai.service.PluginCategoryService;
import tech.easyflow.common.annotation.UsePermission;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.web.controller.BaseCurdController;
import javax.annotation.Resource;
import java.math.BigInteger;
/**
* 控制层。
*
* @author wangGangQiang
* @since 2025-05-21
*/
@RestController
@RequestMapping("/api/v1/pluginCategory")
@UsePermission(moduleName = "/api/v1/plugin")
public class PluginCategoryController extends BaseCurdController<PluginCategoryService, PluginCategory> {
public PluginCategoryController(PluginCategoryService service) {
super(service);
}
@Resource
private PluginCategoryService pluginCategoryService;
@GetMapping("/doRemoveCategory")
@SaCheckPermission("/api/v1/plugin/remove")
public Result<Boolean> doRemoveCategory(@RequestParam("id") BigInteger id){
return Result.ok(pluginCategoryService.doRemoveCategory(id));
}
}

View File

@@ -0,0 +1,45 @@
package tech.easyflow.admin.controller.ai;
import org.springframework.web.bind.annotation.*;
import tech.easyflow.ai.entity.PluginCategory;
import tech.easyflow.ai.entity.PluginCategoryMapping;
import tech.easyflow.ai.service.PluginCategoryMappingService;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.web.controller.BaseCurdController;
import tech.easyflow.common.web.jsonbody.JsonBody;
import javax.annotation.Resource;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;
/**
* 控制层。
*
* @author wangGangQiang
* @since 2025-05-21
*/
@RestController
@RequestMapping("/api/v1/pluginCategoryMapping")
public class PluginCategoryMappingController extends BaseCurdController<PluginCategoryMappingService, PluginCategoryMapping> {
public PluginCategoryMappingController(PluginCategoryMappingService service) {
super(service);
}
@Resource
private PluginCategoryMappingService relationService;
@PostMapping("/updateRelation")
public Result<Boolean> updateRelation(
@JsonBody(value="pluginId") BigInteger pluginId,
@JsonBody(value="categoryIds") ArrayList<BigInteger> categoryIds
){
return Result.ok(relationService.updateRelation(pluginId, categoryIds));
}
@GetMapping("/getPluginCategories")
public Result<List<PluginCategory>> getPluginCategories(@RequestParam(value="pluginId") BigInteger pluginId
){
return Result.ok(relationService.getPluginCategories(pluginId));
}
}

View File

@@ -0,0 +1,90 @@
package tech.easyflow.admin.controller.ai;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.mybatisflex.core.paginate.Page;
import com.mybatisflex.core.query.QueryWrapper;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import tech.easyflow.ai.entity.Plugin;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.web.controller.BaseCurdController;
import tech.easyflow.ai.service.PluginService;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import tech.easyflow.common.web.jsonbody.JsonBody;
import javax.annotation.Resource;
import java.util.List;
/**
* 控制层。
*
* @author Administrator
* @since 2025-04-25
*/
@RestController
@RequestMapping("/api/v1/plugin")
public class PluginController extends BaseCurdController<PluginService, Plugin> {
public PluginController(PluginService service) {
super(service);
}
@Resource
PluginService pluginService;
@Override
protected Result<?> onSaveOrUpdateBefore(Plugin entity, boolean isSave) {
return super.onSaveOrUpdateBefore(entity, isSave);
}
@PostMapping("/plugin/save")
@SaCheckPermission("/api/v1/plugin/save")
public Result<Boolean> savePlugin(@JsonBody Plugin plugin){
return Result.ok(pluginService.savePlugin(plugin));
}
@PostMapping("/plugin/update")
@SaCheckPermission("/api/v1/plugin/save")
public Result<Boolean> updatePlugin(@JsonBody Plugin plugin){
return Result.ok(pluginService.updatePlugin(plugin));
}
@PostMapping("/plugin/remove")
@SaCheckPermission("/api/v1/plugin/remove")
public Result<Boolean> removePlugin(@JsonBody(value = "id", required = true) String id){
return Result.ok(pluginService.removePlugin(id));
}
@PostMapping("/getList")
@SaCheckPermission("/api/v1/plugin/query")
public Result<List<Plugin>> getList(){
return Result.ok(pluginService.getList());
}
@GetMapping("/pageByCategory")
@SaCheckPermission("/api/v1/plugin/query")
public Result<Page<Plugin>> pageByCategory(HttpServletRequest request, String sortKey, String sortType, Long pageNumber, Long pageSize, int category) {
if (pageNumber == null || pageNumber < 1) {
pageNumber = 1L;
}
if (pageSize == null || pageSize < 1) {
pageSize = 10L;
}
if (category == 0){
QueryWrapper queryWrapper = buildQueryWrapper(request);
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
return Result.ok(queryPage(new Page<>(pageNumber, pageSize), queryWrapper));
} else {
return pluginService.pageByCategory(pageNumber, pageSize, category);
}
}
@Override
protected Page<Plugin> queryPage(Page<Plugin> page, QueryWrapper queryWrapper) {
return service.getMapper().paginateWithRelations(page, queryWrapper);
}
}

View File

@@ -0,0 +1,150 @@
package tech.easyflow.admin.controller.ai;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.mybatisflex.core.query.QueryWrapper;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import tech.easyflow.ai.entity.BotPlugin;
import tech.easyflow.ai.entity.PluginItem;
import tech.easyflow.ai.service.BotPluginService;
import tech.easyflow.ai.service.PluginItemService;
import tech.easyflow.common.annotation.UsePermission;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.web.controller.BaseCurdController;
import tech.easyflow.common.web.jsonbody.JsonBody;
import javax.annotation.Resource;
import java.io.Serializable;
import java.math.BigInteger;
import java.util.Collection;
import java.util.List;
/**
* 控制层。
*
* @author WangGangqiang
* @since 2025-04-27
*/
@RestController
@RequestMapping("/api/v1/pluginItem")
@UsePermission(moduleName = "/api/v1/plugin")
public class PluginItemController extends BaseCurdController<PluginItemService, PluginItem> {
public PluginItemController(PluginItemService service) {
super(service);
}
@Resource
private PluginItemService pluginItemService;
@Resource
private BotPluginService botPluginService;
@PostMapping("/tool/save")
@SaCheckPermission("/api/v1/plugin/save")
public Result<Boolean> savePlugin(@JsonBody PluginItem pluginItem){
return Result.ok(pluginItemService.savePluginTool(pluginItem));
}
// 插件工具修改页面查询
@PostMapping("/tool/search")
@SaCheckPermission("/api/v1/plugin/query")
public Result<?> searchPlugin(@JsonBody(value = "aiPluginToolId", required = true) BigInteger aiPluginToolId){
return pluginItemService.searchPlugin(aiPluginToolId);
}
@PostMapping("/toolsList")
@SaCheckPermission("/api/v1/plugin/query")
public Result<List<PluginItem>> searchPluginToolByPluginId(@JsonBody(value = "pluginId", required = true) BigInteger pluginId,
@JsonBody(value = "botId", required = false) BigInteger botId){
return Result.ok(pluginItemService.searchPluginToolByPluginId(pluginId, botId));
}
@PostMapping("/tool/update")
@SaCheckPermission("/api/v1/plugin/save")
public Result<Boolean> updatePlugin(@JsonBody PluginItem pluginItem){
return Result.ok(pluginItemService.updatePlugin(pluginItem));
}
@PostMapping("/tool/list")
@SaCheckPermission("/api/v1/plugin/query")
public Result<List<PluginItem>> getPluginToolList(@JsonBody(value = "botId", required = true) BigInteger botId){
return Result.ok(pluginItemService.getPluginToolList(botId));
}
@GetMapping("/getTinyFlowData")
@SaCheckPermission("/api/v1/plugin/query")
public Result<?> getTinyFlowData(BigInteger id) {
JSONObject nodeData = new JSONObject();
PluginItem record = pluginItemService.getById(id);
if (record == null) {
return Result.ok(nodeData);
}
nodeData.put("pluginId", record.getId().toString());
nodeData.put("pluginName", record.getName());
JSONArray parameters = new JSONArray();
JSONArray outputDefs = new JSONArray();
String inputData = record.getInputData();
if (StrUtil.isNotEmpty(inputData)) {
JSONArray array = JSON.parseArray(inputData);
handleArray(array);
parameters = array;
}
String outputData = record.getOutputData();
if (StrUtil.isNotEmpty(outputData)) {
JSONArray array = JSON.parseArray(outputData);
handleArray(array);
outputDefs = array;
}
nodeData.put("parameters", parameters);
nodeData.put("outputDefs", outputDefs);
return Result.ok(nodeData);
}
/**
* 插件试运行接口
* @return
*/
@PostMapping("/test")
public Result<?> pluginToolTest(@JsonBody(value = "inputData", required = true) String inputData,
@JsonBody(value = "pluginToolId", required = true) BigInteger pluginToolId){
return pluginItemService.pluginToolTest(inputData, pluginToolId);
}
private void handleArray(JSONArray array) {
for (Object o : array) {
JSONObject obj = (JSONObject) o;
obj.put("id", IdUtil.simpleUUID());
obj.put("nameDisabled", true);
obj.put("dataTypeDisabled", true);
obj.put("deleteDisabled", true);
obj.put("addChildDisabled", true);
JSONArray children = obj.getJSONArray("children");
if (children != null) {
handleArray(children);
}
}
}
@Override
protected Result<?> onRemoveBefore(Collection<Serializable> ids) {
QueryWrapper queryWrapper = QueryWrapper.create();
queryWrapper.in(BotPlugin::getPluginItemId, ids);
boolean exists = botPluginService.exists(queryWrapper);
if (exists){
return Result.fail(1, "此工具还关联着bot请先取消关联");
}
return null;
}
}

View File

@@ -0,0 +1,22 @@
package tech.easyflow.admin.controller.ai;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import tech.easyflow.ai.entity.ResourceCategory;
import tech.easyflow.ai.service.ResourceCategoryService;
import tech.easyflow.common.annotation.UsePermission;
import tech.easyflow.common.web.controller.BaseCurdController;
/**
* 素材分类
*/
@RestController
@RequestMapping("/api/v1/resourceCategory")
@UsePermission(moduleName = "/api/v1/resource")
public class ResourceCategoryController extends BaseCurdController<ResourceCategoryService, ResourceCategory> {
public ResourceCategoryController(ResourceCategoryService service) {
super(service);
}
}

View File

@@ -0,0 +1,56 @@
package tech.easyflow.admin.controller.ai;
import cn.hutool.core.io.FileTypeUtil;
import cn.hutool.http.HttpUtil;
import com.mybatisflex.core.paginate.Page;
import com.mybatisflex.core.query.QueryWrapper;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import tech.easyflow.ai.entity.Resource;
import tech.easyflow.ai.service.ResourceService;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.common.web.controller.BaseCurdController;
import java.io.ByteArrayInputStream;
import java.math.BigInteger;
import java.util.Date;
/**
* 素材库
*
* @author ArkLight
* @since 2025-06-27
*/
@RestController
@RequestMapping("/api/v1/resource")
public class ResourceController extends BaseCurdController<ResourceService, Resource> {
public ResourceController(ResourceService service) {
super(service);
}
@Override
protected Result<?> onSaveOrUpdateBefore(Resource entity, boolean isSave) {
LoginAccount loginUser = SaTokenUtil.getLoginAccount();
if (isSave) {
String resourceUrl = entity.getResourceUrl();
byte[] bytes = HttpUtil.downloadBytes(resourceUrl);
ByteArrayInputStream stream = new ByteArrayInputStream(bytes);
String suffix = FileTypeUtil.getType(stream, resourceUrl);
entity.setSuffix(suffix);
entity.setFileSize(BigInteger.valueOf(bytes.length));
commonFiled(entity,loginUser.getId(),loginUser.getTenantId(), loginUser.getDeptId());
} else {
entity.setModified(new Date());
entity.setModifiedBy(loginUser.getId());
}
return super.onSaveOrUpdateBefore(entity, isSave);
}
@Override
protected Page<Resource> queryPage(Page<Resource> page, QueryWrapper queryWrapper) {
queryWrapper.eq(Resource::getCreatedBy, SaTokenUtil.getLoginAccount().getId().toString());
return super.queryPage(page, queryWrapper);
}
}

View File

@@ -0,0 +1,85 @@
package tech.easyflow.admin.controller.ai;
import cn.hutool.core.util.IdUtil;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.easyagents.flow.core.chain.ChainDefinition;
import com.easyagents.flow.core.chain.Node;
import com.easyagents.flow.core.node.ConfirmNode;
import com.easyagents.flow.core.node.EndNode;
import com.easyagents.flow.core.node.StartNode;
import com.easyagents.flow.core.parser.ChainParser;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import tech.easyflow.ai.entity.Workflow;
import tech.easyflow.ai.service.WorkflowService;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.web.exceptions.BusinessException;
import javax.annotation.Resource;
import java.util.List;
@RequestMapping("/api/v1/workflowNode")
@RestController
public class WorkFlowNodeController {
@Resource
private WorkflowService workflowService;
@Resource
private ChainParser chainParser;
@GetMapping("/getChainParams")
public Result<?> getChainParams(String currentId, String workflowId) {
if (workflowId.equals(currentId)) {
throw new BusinessException("工作流不能作为自身子节点");
}
JSONObject nodeData = new JSONObject();
Workflow workflow = workflowService.getById(workflowId);
if (workflow == null) {
throw new BusinessException("工作流不存在: " + workflowId);
}
nodeData.put("workflowId", workflow.getId());
nodeData.put("workflowName", workflow.getTitle());
ChainDefinition definition = chainParser.parse(workflow.getContent());
List<Node> nodes = definition.getNodes();
JSONArray inputs = new JSONArray();
JSONArray outputs = new JSONArray();
for (Node node : nodes) {
if (node instanceof StartNode) {
inputs = JSON.parseArray(JSON.toJSONString(node.getParameters()));
handleArray(inputs);
}
if (node instanceof EndNode) {
outputs = JSON.parseArray(JSON.toJSONString(((EndNode) node).getOutputDefs()));
handleArray(outputs);
}
if (node instanceof ConfirmNode) {
throw new BusinessException("工作流存在【确认节点】,暂不支持作为子节点");
}
}
nodeData.put("parameters", inputs);
nodeData.put("outputDefs", outputs);
return Result.ok(nodeData);
}
private void handleArray(JSONArray array) {
if (array != null) {
for (Object o : array) {
JSONObject obj = (JSONObject) o;
obj.put("id", IdUtil.simpleUUID());
obj.put("nameDisabled", true);
obj.put("dataTypeDisabled", true);
obj.put("deleteDisabled", true);
obj.put("addChildDisabled", true);
obj.put("refType", "ref");
JSONArray children = obj.getJSONArray("children");
if (children != null) {
handleArray(children);
}
}
}
}
}

View File

@@ -0,0 +1,25 @@
package tech.easyflow.admin.controller.ai;
import tech.easyflow.ai.entity.WorkflowCategory;
import tech.easyflow.ai.service.WorkflowCategoryService;
import tech.easyflow.common.annotation.UsePermission;
import tech.easyflow.common.web.controller.BaseCurdController;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 控制层。
*
* @author ArkLight
* @since 2025-12-11
*/
@RestController
@RequestMapping("/api/v1/workflowCategory")
@UsePermission(moduleName = "/api/v1/workflow")
public class WorkflowCategoryController extends BaseCurdController<WorkflowCategoryService, WorkflowCategory> {
public WorkflowCategoryController(WorkflowCategoryService service) {
super(service);
}
}

View File

@@ -0,0 +1,213 @@
package tech.easyflow.admin.controller.ai;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.IdUtil;
import com.mybatisflex.core.query.QueryWrapper;
import com.easyagents.flow.core.chain.*;
import com.easyagents.flow.core.chain.runtime.ChainExecutor;
import com.easyagents.flow.core.parser.ChainParser;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import tech.easyflow.ai.entity.Workflow;
import tech.easyflow.ai.service.BotWorkflowService;
import tech.easyflow.ai.service.ModelService;
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.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.controller.BaseCurdController;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.common.web.jsonbody.JsonBody;
import tech.easyflow.system.service.SysApiKeyService;
import javax.annotation.Resource;
import java.io.InputStream;
import java.io.Serializable;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.util.*;
/**
* 控制层。
*
* @author michael
* @since 2024-08-23
*/
@RestController
@RequestMapping("/api/v1/workflow")
public class WorkflowController extends BaseCurdController<WorkflowService, Workflow> {
private final ModelService modelService;
@Resource
private SysApiKeyService apiKeyService;
@Resource
private BotWorkflowService botWorkflowService;
@Resource
private ChainExecutor chainExecutor;
@Resource
private ChainParser chainParser;
@Resource
private TinyFlowService tinyFlowService;
public WorkflowController(WorkflowService service, ModelService modelService) {
super(service);
this.modelService = modelService;
}
/**
* 节点单独运行
*/
@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 = service.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 = service.getById(id);
if (workflow == null) {
throw new RuntimeException("工作流不存在");
}
if (StpUtil.isLogin()) {
variables.put(Constants.LOGIN_USER_KEY, SaTokenUtil.getLoginAccount());
}
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();
}
@PostMapping("/importWorkFlow")
@SaCheckPermission("/api/v1/workflow/save")
public Result<Void> importWorkFlow(Workflow workflow, MultipartFile jsonFile) throws Exception {
InputStream is = jsonFile.getInputStream();
String content = IoUtil.read(is, StandardCharsets.UTF_8);
workflow.setContent(content);
save(workflow);
return Result.ok();
}
@GetMapping("/exportWorkFlow")
@SaCheckPermission("/api/v1/workflow/save")
public Result<String> exportWorkFlow(BigInteger id) {
Workflow workflow = service.getById(id);
return Result.ok("", workflow.getContent());
}
@GetMapping("getRunningParameters")
@SaCheckPermission("/api/v1/workflow/query")
public Result<?> getRunningParameters(@RequestParam BigInteger id) {
Workflow workflow = service.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);
}
@Override
public Result<Workflow> detail(String id) {
Workflow workflow = service.getDetail(id);
return Result.ok(workflow);
}
@GetMapping("/copy")
@SaCheckPermission("/api/v1/workflow/save")
public Result<Void> copy(BigInteger id) {
LoginAccount account = SaTokenUtil.getLoginAccount();
Workflow workflow = service.getById(id);
workflow.setId(null);
workflow.setAlias(IdUtil.fastSimpleUUID());
commonFiled(workflow, account.getId(), account.getTenantId(), account.getDeptId());
service.save(workflow);
return Result.ok();
}
@Override
protected Result onSaveOrUpdateBefore(Workflow entity, boolean isSave) {
String alias = entity.getAlias();
if (StringUtils.hasLength(alias)) {
Workflow workflow = service.getByAlias(alias);
if (workflow == null) {
return null;
}
if (isSave) {
throw new BusinessException("别名已存在!");
}
BigInteger id = entity.getId();
if (id.compareTo(workflow.getId()) != 0) {
throw new BusinessException("别名已存在!");
}
} else {
entity.setAlias(null);
}
return null;
}
@Override
protected Result onRemoveBefore(Collection<Serializable> ids) {
QueryWrapper queryWrapper = QueryWrapper.create();
queryWrapper.in("workflow_id", ids);
boolean exists = botWorkflowService.exists(queryWrapper);
if (exists) {
return Result.fail(1, "此工作流还关联有bot请先取消关联后再删除");
}
return null;
}
}

View File

@@ -0,0 +1,64 @@
package tech.easyflow.admin.controller.ai;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.mybatisflex.core.paginate.Page;
import com.mybatisflex.core.query.QueryWrapper;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import tech.easyflow.ai.entity.WorkflowExecResult;
import tech.easyflow.ai.entity.WorkflowExecStep;
import tech.easyflow.ai.service.WorkflowExecResultService;
import tech.easyflow.ai.service.WorkflowExecStepService;
import tech.easyflow.ai.utils.WorkFlowUtil;
import tech.easyflow.common.annotation.UsePermission;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.common.web.controller.BaseCurdController;
import javax.annotation.Resource;
import java.math.BigInteger;
/**
* 工作流执行记录
*/
@RestController
@RequestMapping("/api/v1/workflowExecResult")
@UsePermission(moduleName = "/api/v1/workflow")
public class WorkflowExecResultController extends BaseCurdController<WorkflowExecResultService, WorkflowExecResult> {
@Resource
private WorkflowExecStepService recordStepService;
public WorkflowExecResultController(WorkflowExecResultService service) {
super(service);
}
@GetMapping("/del")
@Transactional(rollbackFor = Exception.class)
@SaCheckPermission("/api/v1/workflow/remove")
public Result<Void> del(BigInteger id) {
LoginAccount account = SaTokenUtil.getLoginAccount();
WorkflowExecResult record = service.getById(id);
if (!account.getId().toString().equals(record.getCreatedBy())) {
return Result.fail(1, "非法请求");
}
service.removeById(id);
QueryWrapper w = QueryWrapper.create();
w.eq(WorkflowExecStep::getRecordId, id);
recordStepService.remove(w);
return Result.ok();
}
@Override
protected Page<WorkflowExecResult> queryPage(Page<WorkflowExecResult> page, QueryWrapper queryWrapper) {
queryWrapper.eq(WorkflowExecResult::getCreatedBy, SaTokenUtil.getLoginAccount().getId().toString());
Page<WorkflowExecResult> res = super.queryPage(page, queryWrapper);
for (WorkflowExecResult record : res.getRecords()) {
record.setWorkflowJson(WorkFlowUtil.removeSensitiveInfo(record.getWorkflowJson()));
}
return res;
}
}

View File

@@ -0,0 +1,63 @@
package tech.easyflow.admin.controller.ai;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.mybatisflex.core.query.QueryWrapper;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import tech.easyflow.ai.entity.WorkflowExecResult;
import tech.easyflow.ai.entity.WorkflowExecStep;
import tech.easyflow.ai.service.WorkflowExecResultService;
import tech.easyflow.ai.service.WorkflowExecStepService;
import tech.easyflow.common.annotation.UsePermission;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.web.controller.BaseCurdController;
import tech.easyflow.common.web.exceptions.BusinessException;
import javax.annotation.Resource;
import java.math.BigInteger;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 执行记录步骤
*/
@RestController
@RequestMapping("/api/v1/workflowExecStep")
@UsePermission(moduleName = "/api/v1/workflow")
public class WorkflowExecStepController extends BaseCurdController<WorkflowExecStepService, WorkflowExecStep> {
@Resource
private WorkflowExecResultService execRecordService;
public WorkflowExecStepController(WorkflowExecStepService service) {
super(service);
}
@GetMapping("/getListByRecordId")
public Result<List<WorkflowExecStep>> getListByRecordId(BigInteger recordId) {
if (recordId == null) {
throw new BusinessException("recordId不能为空");
}
WorkflowExecResult record = execRecordService.getById(recordId);
String workflowJson = record.getWorkflowJson();
JSONObject workflow = JSON.parseObject(workflowJson);
Map<String, String> idTypeMap = new HashMap<>();
JSONArray nodes = workflow.getJSONArray("nodes");
for (Object node : nodes) {
JSONObject nodeObj = (JSONObject) node;
idTypeMap.put(nodeObj.getString("id"), nodeObj.getString("type"));
}
QueryWrapper w = QueryWrapper.create();
w.eq(WorkflowExecStep::getRecordId, recordId);
List<WorkflowExecStep> list = service.list(w);
for (WorkflowExecStep step : list) {
step.setNodeData(null);
step.setNodeType(idTypeMap.get(step.getNodeId()));
}
return Result.ok(list);
}
}

View File

@@ -0,0 +1,41 @@
package tech.easyflow.admin.controller.auth;
import org.springframework.web.bind.annotation.GetMapping;
import tech.easyflow.auth.entity.LoginDTO;
import tech.easyflow.auth.entity.LoginVO;
import tech.easyflow.auth.service.AuthService;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.web.jsonbody.JsonBody;
import cn.dev33.satoken.stp.StpUtil;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.List;
@RestController
@RequestMapping("/api/v1/auth/")
public class AuthController {
@Resource
private AuthService authService;
@PostMapping("login")
public Result<LoginVO> login(@JsonBody LoginDTO loginDTO) {
LoginVO res = authService.login(loginDTO);
return Result.ok(res);
}
@PostMapping("logout")
public Result<Void> logout() {
StpUtil.logout();
return Result.ok();
}
@GetMapping("getPermissions")
public Result<List<String>> getPermissions() {
List<String> permissionList = StpUtil.getPermissionList();
return Result.ok(permissionList);
}
}

View File

@@ -0,0 +1,40 @@
package tech.easyflow.admin.controller.common;
import jakarta.servlet.http.HttpServletRequest;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.dict.Dict;
import tech.easyflow.common.dict.DictItem;
import tech.easyflow.common.dict.DictLoader;
import tech.easyflow.common.dict.DictManager;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/v1/dict/")
public class DictController {
@Resource
DictManager dictManager;
@GetMapping("/items/{code}")
public Result<List<DictItem>> items(@PathVariable("code") String code, String keyword, HttpServletRequest request) {
DictLoader loader = dictManager.getLoader(code);
if (loader == null) {
return Result.ok(Collections.emptyList());
}
Map<String, String[]> parameterMap = request.getParameterMap();
Dict dict = loader.load(keyword, parameterMap);
if (dict == null) {
return Result.ok(Collections.emptyList());
}
return Result.ok(dict.getItems());
}
}

View File

@@ -0,0 +1,49 @@
package tech.easyflow.admin.controller.common;
import cloud.tianai.captcha.application.ImageCaptchaApplication;
import cloud.tianai.captcha.application.vo.ImageCaptchaVO;
import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
import cloud.tianai.captcha.common.response.ApiResponse;
import com.alibaba.fastjson2.JSON;
import org.springframework.web.bind.annotation.*;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.captcha.tainai.CaptchaData;
import tech.easyflow.system.entity.SysOption;
import tech.easyflow.system.service.SysOptionService;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
/**
* 公共接口
*/
@RestController
@RequestMapping("/api/v1/public")
public class PublicController {
@Resource
private ImageCaptchaApplication application;
@Resource
private SysOptionService sysOptionService;
/**
* 获取验证码
*/
@RequestMapping(value = "/getCaptcha", produces = "application/json")
public ApiResponse<ImageCaptchaVO> getCaptcha() {
return application.generateCaptcha(CaptchaTypeConstant.SLIDER);
}
/**
* 验证码校验
*/
@PostMapping(value = "/check", produces = "application/json")
public ApiResponse<String> checkCaptcha(@RequestBody CaptchaData data) {
ApiResponse<?> response = application.matching(data.getId(), data.getData());
if (!response.isSuccess()) {
return ApiResponse.ofError("验证码错误");
}
return ApiResponse.ofSuccess(data.getId());
}
}

View File

@@ -0,0 +1,47 @@
package tech.easyflow.admin.controller.common;
import cn.dev33.satoken.annotation.SaIgnore;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.vo.UploadResVo;
import tech.easyflow.common.filestorage.FileStorageService;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
@RestController
@RequestMapping("/api/v1/commons/")
public class UploadController {
@Resource(name = "default")
FileStorageService storageService;
@PostMapping(value = "/upload", produces = MediaType.APPLICATION_JSON_VALUE)
public Result<UploadResVo> upload(MultipartFile file) {
String path = storageService.save(file);
UploadResVo resVo = new UploadResVo();
resVo.setPath(path);
return Result.ok(resVo);
}
@PostMapping(value = "/uploadAntd", produces = MediaType.APPLICATION_JSON_VALUE)
public Result<UploadResVo> uploadAntd(MultipartFile file) {
String path = storageService.save(file);
UploadResVo resVo = new UploadResVo();
resVo.setPath(path);
return Result.ok(resVo);
}
@PostMapping(value = "/uploadPrePath",produces = MediaType.APPLICATION_JSON_VALUE)
@SaIgnore
public Result<UploadResVo> uploadPrePath(MultipartFile file, String prePath) {
String path = storageService.save(file,prePath);
UploadResVo resVo = new UploadResVo();
resVo.setPath(path);
return Result.ok(resVo);
}
}

View File

@@ -0,0 +1,195 @@
package tech.easyflow.admin.controller.datacenter;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.collection.CollectionUtil;
import cn.idev.excel.EasyExcel;
import cn.idev.excel.FastExcel;
import com.alibaba.fastjson2.JSONObject;
import com.mybatisflex.core.paginate.Page;
import com.mybatisflex.core.query.QueryWrapper;
import com.mybatisflex.core.row.Row;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.entity.DatacenterQuery;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.common.web.controller.BaseCurdController;
import tech.easyflow.datacenter.entity.DatacenterTable;
import tech.easyflow.datacenter.entity.DatacenterTableField;
import tech.easyflow.datacenter.entity.vo.HeaderVo;
import tech.easyflow.datacenter.excel.ReadDataListener;
import tech.easyflow.datacenter.excel.ReadResVo;
import tech.easyflow.datacenter.service.DatacenterTableFieldService;
import tech.easyflow.datacenter.service.DatacenterTableService;
import javax.annotation.Resource;
import java.io.InputStream;
import java.math.BigInteger;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
/**
* 数据中枢表 控制层。
*
* @author ArkLight
* @since 2025-07-10
*/
@RestController
@RequestMapping("/api/v1/datacenterTable")
public class DatacenterTableController extends BaseCurdController<DatacenterTableService, DatacenterTable> {
@Resource
private DatacenterTableFieldService fieldsService;
public DatacenterTableController(DatacenterTableService service) {
super(service);
}
@Override
protected Result onSaveOrUpdateBefore(DatacenterTable entity, boolean isSave) {
LoginAccount loginUser = SaTokenUtil.getLoginAccount();
if (isSave) {
commonFiled(entity, loginUser.getId(), loginUser.getTenantId(), loginUser.getDeptId());
} else {
entity.setModifiedBy(loginUser.getId());
}
return super.onSaveOrUpdateBefore(entity, isSave);
}
@PostMapping("/saveTable")
@SaCheckPermission("/api/v1/datacenterTable/save")
public Result<Void> saveTable(@RequestBody DatacenterTable entity) {
LoginAccount loginUser = SaTokenUtil.getLoginAccount();
List<DatacenterTableField> fields = entity.getFields();
if (CollectionUtil.isEmpty(fields)) {
return Result.fail(99, "字段不能为空");
}
BigInteger id = entity.getId();
if (id == null) {
commonFiled(entity, loginUser.getId(), loginUser.getTenantId(), loginUser.getDeptId());
} else {
entity.setModified(new Date());
entity.setModifiedBy(loginUser.getId());
}
service.saveTable(entity, loginUser);
return Result.ok();
}
@GetMapping("/detailInfo")
@SaCheckPermission("/api/v1/datacenterTable/query")
public Result<DatacenterTable> detailInfo(BigInteger tableId) {
DatacenterTable table = service.getById(tableId);
QueryWrapper wrapper = QueryWrapper.create();
wrapper.eq(DatacenterTableField::getTableId, tableId);
wrapper.orderBy("id");
List<DatacenterTableField> fields = fieldsService.list(wrapper);
table.setFields(fields);
return Result.ok(table);
}
@GetMapping("/removeTable")
@SaCheckPermission("/api/v1/datacenterTable/remove")
public Result<Void> removeTable(BigInteger tableId) {
service.removeTable(tableId);
return Result.ok();
}
@GetMapping("/getHeaders")
@SaCheckPermission("/api/v1/datacenterTable/query")
public Result<List<HeaderVo>> getHeaders(BigInteger tableId) {
List<HeaderVo> res = service.getHeaders(tableId);
return Result.ok(res);
}
@GetMapping("/getPageData")
@SaCheckPermission("/api/v1/datacenterTable/query")
public Result<Page<Row>> getPageData(DatacenterQuery where) {
Page<Row> res = service.getPageData(where);
return Result.ok(res);
}
@PostMapping("/saveValue")
@SaCheckPermission("/api/v1/datacenterTable/save")
public Result<Void> saveValue(@RequestParam Map<String, Object> map) {
JSONObject object = new JSONObject(map);
BigInteger tableId = object.getBigInteger("tableId");
LoginAccount account = SaTokenUtil.getLoginAccount();
if (tableId == null) {
return Result.fail(99, "参数错误");
}
service.saveValue(tableId, object, account);
return Result.ok();
}
@PostMapping("/removeValue")
@SaCheckPermission("/api/v1/datacenterTable/remove")
public Result<Void> removeValue(@RequestParam Map<String, Object> map) {
JSONObject object = new JSONObject(map);
BigInteger tableId = object.getBigInteger("tableId");
BigInteger id = object.getBigInteger("id");
if (tableId == null || id == null) {
return Result.fail(99, "参数错误");
}
LoginAccount account = SaTokenUtil.getLoginAccount();
service.removeValue(tableId, id, account);
return Result.ok();
}
/**
* 导入数据
*/
@PostMapping("/importData")
@SaCheckPermission("/api/v1/datacenterTable/save")
public Result<ReadResVo> importData(MultipartFile file, @RequestParam Map<String, Object> map) throws Exception {
Object tableId = map.get("tableId");
DatacenterTable record = service.getById(tableId.toString());
if (record == null) {
throw new RuntimeException("数据表不存在");
}
InputStream is = file.getInputStream();
List<DatacenterTableField> fields = service.getFields(record.getId());
LoginAccount account = SaTokenUtil.getLoginAccount();
ReadDataListener listener = new ReadDataListener(record.getId(), fields, account);
FastExcel.read(is, listener)
.sheet()
.doRead();
int totalCount = listener.getTotalCount();
int errorCount = listener.getErrorCount();
int successCount = listener.getSuccessCount();
List<JSONObject> errorRows = listener.getErrorRows();
return Result.ok(new ReadResVo(successCount, errorCount, totalCount, errorRows));
}
@GetMapping("/getTemplate")
public void getTemplate(BigInteger tableId, HttpServletResponse response) throws Exception {
List<DatacenterTableField> fields = service.getFields(tableId);
// 设置响应内容类型
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
// 设置文件名
String fileName = URLEncoder.encode("导入模板", "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
// 动态表头数据
List<List<String>> headList = new ArrayList<>();
for (DatacenterTableField field : fields) {
List<String> head = new ArrayList<>();
head.add(field.getFieldName());
headList.add(head);
}
// 写入Excel
EasyExcel.write(response.getOutputStream())
.head(headList)
.sheet("模板")
.doWrite(new ArrayList<>()); // 写入空数据,只生成模板
}
}

View File

@@ -0,0 +1,41 @@
package tech.easyflow.admin.controller.datacenter;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import tech.easyflow.common.annotation.UsePermission;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.common.web.controller.BaseCurdController;
import tech.easyflow.datacenter.entity.DatacenterTableField;
import tech.easyflow.datacenter.service.DatacenterTableFieldService;
import java.util.Date;
/**
* 控制层。
*
* @author ArkLight
* @since 2025-07-10
*/
@RestController
@RequestMapping("/api/v1/datacenterTableFields")
@UsePermission(moduleName = "/api/v1/datacenterTable")
public class DatacenterTableFieldsController extends BaseCurdController<DatacenterTableFieldService, DatacenterTableField> {
public DatacenterTableFieldsController(DatacenterTableFieldService service) {
super(service);
}
@Override
protected Result onSaveOrUpdateBefore(DatacenterTableField entity, boolean isSave) {
LoginAccount loginUser = SaTokenUtil.getLoginAccount();
if (isSave) {
commonFiled(entity, loginUser.getId(), loginUser.getTenantId(), loginUser.getDeptId());
} else {
entity.setModified(new Date());
entity.setModifiedBy(loginUser.getId());
}
return null;
}
}

View File

@@ -0,0 +1,91 @@
package tech.easyflow.admin.controller.job;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.date.DateUtil;
import org.quartz.CronExpression;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import tech.easyflow.common.constant.enums.EnumJobStatus;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.common.web.controller.BaseCurdController;
import tech.easyflow.job.entity.SysJob;
import tech.easyflow.job.service.SysJobService;
import tech.easyflow.common.entity.LoginAccount;
import java.io.Serializable;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
/**
* 系统任务表 控制层。
*
* @author xiaoma
* @since 2025-05-20
*/
@RestController
@RequestMapping("/api/v1/sysJob")
public class SysJobController extends BaseCurdController<SysJobService, SysJob> {
public SysJobController(SysJobService service) {
super(service);
}
@GetMapping("/start")
@SaCheckPermission("/api/v1/sysJob/save")
public Result<Void> start(BigInteger id) {
SysJob sysJob = service.getById(id);
sysJob.setStatus(EnumJobStatus.RUNNING.getCode());
service.addJob(sysJob);
service.updateById(sysJob);
return Result.ok();
}
@GetMapping("/stop")
@SaCheckPermission("/api/v1/sysJob/save")
public Result<Void> stop(BigInteger id) {
SysJob sysJob = new SysJob();
sysJob.setId(id);
sysJob.setStatus(EnumJobStatus.STOP.getCode());
ArrayList<Serializable> ids = new ArrayList<>();
ids.add(id);
service.deleteJob(ids);
service.updateById(sysJob);
return Result.ok();
}
@GetMapping("/getNextTimes")
public Result<List<String>> getNextTimes(String cronExpression) throws Exception{
CronExpression ex = new CronExpression(cronExpression);
List<String> times = new ArrayList<>();
Date date = new Date();
for (int i = 0; i < 5; i++) {
Date next = ex.getNextValidTimeAfter(date);
times.add(DateUtil.formatDateTime(next));
date = next;
}
return Result.ok(times);
}
@Override
protected Result onSaveOrUpdateBefore(SysJob entity, boolean isSave) {
LoginAccount loginUser = SaTokenUtil.getLoginAccount();
if (isSave) {
commonFiled(entity,loginUser.getId(),loginUser.getTenantId(), loginUser.getDeptId());
} else {
entity.setModified(new Date());
entity.setModifiedBy(loginUser.getId());
}
return super.onSaveOrUpdateBefore(entity, isSave);
}
@Override
protected Result onRemoveBefore(Collection<Serializable> ids) {
service.deleteJob(ids);
return super.onRemoveBefore(ids);
}
}

View File

@@ -0,0 +1,35 @@
package tech.easyflow.admin.controller.job;
import tech.easyflow.common.annotation.UsePermission;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.satoken.util.SaTokenUtil;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.web.controller.BaseCurdController;
import tech.easyflow.job.entity.SysJobLog;
import tech.easyflow.job.service.SysJobLogService;
/**
* 系统任务日志 控制层。
*
* @author xiaoma
* @since 2025-05-20
*/
@RestController
@RequestMapping("/api/v1/sysJobLog")
@UsePermission(moduleName = "/api/v1/sysJob")
public class SysJobLogController extends BaseCurdController<SysJobLogService, SysJobLog> {
public SysJobLogController(SysJobLogService service) {
super(service);
}
@Override
protected Result onSaveOrUpdateBefore(SysJobLog entity, boolean isSave) {
LoginAccount loginUser = SaTokenUtil.getLoginAccount();
if (isSave) {
commonFiled(entity,loginUser.getId(),loginUser.getTenantId(), loginUser.getDeptId());
}
return super.onSaveOrUpdateBefore(entity, isSave);
}
}

View File

@@ -0,0 +1,159 @@
package tech.easyflow.admin.controller.system;
import tech.easyflow.common.constant.enums.EnumAccountType;
import tech.easyflow.common.constant.enums.EnumDataStatus;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.util.StringUtil;
import tech.easyflow.common.web.controller.BaseCurdController;
import tech.easyflow.common.web.jsonbody.JsonBody;
import tech.easyflow.log.annotation.LogRecord;
import tech.easyflow.system.entity.SysAccount;
import tech.easyflow.system.service.SysAccountService;
import tech.easyflow.common.satoken.util.SaTokenUtil;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.crypto.digest.BCrypt;
import com.mybatisflex.core.paginate.Page;
import com.mybatisflex.core.query.QueryWrapper;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.Serializable;
import java.math.BigInteger;
import java.util.Collection;
import java.util.Date;
import java.util.List;
/**
* 用户表 控制层。
*
* @author ArkLight
* @since 2025-03-14
*/
@RestController("sysAccountController")
@RequestMapping("/api/v1/sysAccount")
public class SysAccountController extends BaseCurdController<SysAccountService, SysAccount> {
public SysAccountController(SysAccountService service) {
super(service);
}
@Override
@LogRecord("分页查询")
protected Page<SysAccount> queryPage(Page<SysAccount> page, QueryWrapper queryWrapper) {
return service.getMapper().paginateWithRelations(page, queryWrapper);
}
@Override
protected Result onSaveOrUpdateBefore(SysAccount entity, boolean isSave) {
LoginAccount loginUser = SaTokenUtil.getLoginAccount();
BigInteger tenantId = loginUser.getTenantId();
if (isSave) {
commonFiled(entity, loginUser.getId(), tenantId, loginUser.getDeptId());
// 查询用户名是否存在
// long count = Db.selectCount(SqlPrepare.COUNT_ACCOUNT_BY_UNI_KEY, entity.getLoginName(), tenantId);
QueryWrapper w = QueryWrapper.create();
w.eq(SysAccount::getLoginName, entity.getLoginName());
long count = service.count(w);
if (count > 0) {
return Result.fail(1, "用户名已存在");
}
String password = entity.getPassword();
if (StringUtil.hasText(password)) {
entity.setPassword(BCrypt.hashpw(password));
}
Integer status = entity.getStatus();
if (status == null) {
entity.setStatus(EnumDataStatus.AVAILABLE.getCode());
}
} else {
SysAccount record = service.getById(entity.getId());
// 如果修改了部门,就将用户踢下线,避免用户操作数据造成数据错误
if (record.getDeptId() != null && !record.getDeptId().equals(entity.getDeptId())) {
StpUtil.kickout(record.getId());
}
// 不让修改用户名/密码,浏览器记住密码有可能会带上来
entity.setLoginName(null);
entity.setPassword(null);
entity.setModified(new Date());
entity.setModifiedBy(loginUser.getId());
}
return null;
}
@Override
protected void onSaveOrUpdateAfter(SysAccount entity, boolean isSave) {
service.syncRelations(entity);
}
@Override
protected Result onRemoveBefore(Collection<Serializable> ids) {
List<SysAccount> sysAccounts = service.listByIds(ids);
for (SysAccount account : sysAccounts) {
Integer accountType = account.getAccountType();
if (EnumAccountType.SUPER_ADMIN.getCode().equals(accountType)) {
return Result.fail(1, "不能删除超级管理员");
}
if (EnumAccountType.TENANT_ADMIN.getCode().equals(accountType)) {
return Result.fail(1, "不能删除租户管理员");
}
}
return super.onRemoveBefore(ids);
}
@GetMapping("/myProfile")
public Result<SysAccount> myProfile() {
LoginAccount account = SaTokenUtil.getLoginAccount();
SysAccount sysAccount = service.getById(account.getId());
return Result.ok(sysAccount);
}
@PostMapping("/updateProfile")
public Result<Void> updateProfile(@JsonBody SysAccount account) {
BigInteger loginAccountId = SaTokenUtil.getLoginAccount().getId();
SysAccount update = new SysAccount();
update.setId(loginAccountId);
update.setNickname(account.getNickname());
update.setMobile(account.getMobile());
update.setEmail(account.getEmail());
update.setAvatar(account.getAvatar());
update.setModified(new Date());
update.setModifiedBy(loginAccountId);
service.updateById(update);
return Result.ok();
}
/**
* 修改密码,用于修改用户自己的密码
*
* @param password 用户的旧密码
* @param newPassword 新密码
* @param confirmPassword 确认密码
*/
@PostMapping("/updatePassword")
public Result<Void> updatePassword(@JsonBody(value = "password", required = true) String password,
@JsonBody(value = "newPassword", required = true) String newPassword,
@JsonBody(value = "confirmPassword", required = true) String confirmPassword) {
BigInteger loginAccountId = SaTokenUtil.getLoginAccount().getId();
SysAccount record = service.getById(loginAccountId);
if (record == null) {
return Result.fail("修改失败");
}
String pwdDb = record.getPassword();
if (!BCrypt.checkpw(password, pwdDb)) {
return Result.fail(1, "密码不正确");
}
if (!newPassword.equals(confirmPassword)) {
return Result.fail(2, "两次密码不一致");
}
SysAccount update = new SysAccount();
update.setId(loginAccountId);
update.setPassword(BCrypt.hashpw(newPassword));
update.setModified(new Date());
update.setModifiedBy(loginAccountId);
service.updateById(update);
return Result.ok();
}
}

View File

@@ -0,0 +1,101 @@
package tech.easyflow.admin.controller.system;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.mybatisflex.core.paginate.Page;
import com.mybatisflex.core.query.QueryWrapper;
import com.mybatisflex.core.table.TableInfo;
import com.mybatisflex.core.table.TableInfoFactory;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.common.util.IdUtil;
import tech.easyflow.common.vo.PkVo;
import tech.easyflow.common.web.controller.BaseCurdController;
import tech.easyflow.system.entity.SysApiKey;
import tech.easyflow.system.entity.SysApiKeyResourceMapping;
import tech.easyflow.system.service.SysApiKeyResourceMappingService;
import tech.easyflow.system.service.SysApiKeyService;
import javax.annotation.Resource;
import java.math.BigInteger;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.Date;
import java.util.List;
/**
* 控制层。
*
* @author wangGangQiang
* @since 2025-04-18
*/
@RestController
@RequestMapping("/api/v1/sysApiKey")
public class SysApiKeyController extends BaseCurdController<SysApiKeyService, SysApiKey> {
public SysApiKeyController(SysApiKeyService service) {
super(service);
}
@Resource
private SysApiKeyResourceMappingService sysApiKeyResourceMappingService;
/**
* 添加(保存)数据
*
* @return {@code Result.errorCode == 0} 添加成功,否则添加失败
*/
@PostMapping("/key/save")
@SaCheckPermission("/api/v1/sysApiKey/save")
public Result<PkVo> save() {
String apiKey = IdUtil.generateUUID();
SysApiKey entity = new SysApiKey();
entity.setApiKey(apiKey);
entity.setCreated(new Date());
entity.setStatus(1);
// 将Date转换为LocalDate
LocalDate localDate = new Date().toInstant()
.atZone(ZoneId.systemDefault())
.toLocalDate();
// 添加30天
LocalDate newLocalDate = localDate.plusDays(30);
// 转换回Date
Date expireDate = Date.from(newLocalDate.atStartOfDay()
.atZone(ZoneId.systemDefault())
.toInstant());
entity.setExpiredAt(expireDate);
LoginAccount loginAccount = SaTokenUtil.getLoginAccount();
commonFiled(entity,loginAccount.getId(),loginAccount.getTenantId(),loginAccount.getDeptId());
service.save(entity);
onSaveOrUpdateAfter(entity, true);
TableInfo tableInfo = TableInfoFactory.ofEntityClass(entity.getClass());
Object[] pkArgs = tableInfo.buildPkSqlArgs(entity);
return Result.ok(new PkVo(pkArgs));
}
@Override
protected void onSaveOrUpdateAfter(SysApiKey entity, boolean isSave) {
if (!isSave && entity.getPermissionIds() != null && !entity.getPermissionIds().isEmpty()) {
// 修改的时候绑定授权接口
sysApiKeyResourceMappingService.authInterface(entity);
}
}
@Override
@GetMapping("/page")
public Result<Page<SysApiKey>> page(HttpServletRequest request, String sortKey, String sortType, Long pageNumber, Long pageSize) {
Result<Page<SysApiKey>> pageResult = (Result<Page<SysApiKey>>) super.page(request, sortKey, sortType, pageNumber, pageSize);
Page<SysApiKey> data = pageResult.getData();
List<SysApiKey> records = data.getRecords();
records.forEach(record -> {
QueryWrapper queryWrapper = QueryWrapper.create().select(SysApiKeyResourceMapping::getApiKeyResourceId).eq(SysApiKeyResourceMapping::getApiKeyId, record.getId());
List<BigInteger> resourceIds = sysApiKeyResourceMappingService.listAs(queryWrapper, BigInteger.class);
record.setPermissionIds(resourceIds);
});
return pageResult;
}
}

View File

@@ -0,0 +1,23 @@
package tech.easyflow.admin.controller.system;
import tech.easyflow.common.annotation.UsePermission;
import tech.easyflow.common.web.controller.BaseCurdController;
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;
/**
* 请求接口表 控制层。
*
* @author 12076
* @since 2025-12-01
*/
@RestController
@RequestMapping("/api/v1/sysApiKeyResourcePermission")
@UsePermission(moduleName = "/api/v1/sysApiKey")
public class SysApiKeyResourceController extends BaseCurdController<SysApiKeyResourceService, SysApiKeyResource> {
public SysApiKeyResourceController(SysApiKeyResourceService service) {
super(service);
}
}

View File

@@ -0,0 +1,23 @@
package tech.easyflow.admin.controller.system;
import tech.easyflow.common.annotation.UsePermission;
import tech.easyflow.common.web.controller.BaseCurdController;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import tech.easyflow.system.entity.SysApiKeyResourceMapping;
import tech.easyflow.system.service.SysApiKeyResourceMappingService;
/**
* apikey-请求接口表 控制层。
*
* @author 12076
* @since 2025-12-01
*/
@RestController
@RequestMapping("/api/v1/sysApiKeyResourcePermissionRelationship")
@UsePermission(moduleName = "/api/v1/sysApiKey")
public class SysApiKeyResourceMappingController extends BaseCurdController<SysApiKeyResourceMappingService, SysApiKeyResourceMapping> {
public SysApiKeyResourceMappingController(SysApiKeyResourceMappingService service) {
super(service);
}
}

View File

@@ -0,0 +1,92 @@
package tech.easyflow.admin.controller.system;
import tech.easyflow.common.constant.Constants;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.tree.Tree;
import tech.easyflow.common.web.controller.BaseCurdController;
import tech.easyflow.system.entity.SysAccount;
import tech.easyflow.system.entity.SysDept;
import tech.easyflow.system.service.SysAccountService;
import tech.easyflow.system.service.SysDeptService;
import tech.easyflow.common.satoken.util.SaTokenUtil;
import com.mybatisflex.core.query.QueryWrapper;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.io.Serializable;
import java.math.BigInteger;
import java.util.Collection;
import java.util.Date;
import java.util.List;
/**
* 部门表 控制层。
*
* @author ArkLight
* @since 2025-03-14
*/
@RestController("sysDeptController")
@RequestMapping("/api/v1/sysDept")
public class SysDeptController extends BaseCurdController<SysDeptService, SysDept> {
@Resource
private SysAccountService sysAccountService;
public SysDeptController(SysDeptService service) {
super(service);
}
@Override
protected String getDefaultOrderBy() {
return "sort_no asc";
}
@Override
@GetMapping("list")
public Result<List<SysDept>> list(SysDept entity, Boolean asTree, String sortKey, String sortType) {
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
List<SysDept> sysMenus = service.list(queryWrapper);
return Result.ok(Tree.tryToTree(sysMenus, "id", "parentId"));
}
@Override
protected Result onSaveOrUpdateBefore(SysDept entity, boolean isSave) {
LoginAccount loginUser = SaTokenUtil.getLoginAccount();
BigInteger parentId = entity.getParentId();
if (parentId.equals(BigInteger.ZERO)) {
entity.setAncestors(parentId.toString());
} else {
SysDept parent = service.getById(parentId);
entity.setAncestors(parent.getAncestors() + "," + parentId);
}
if (isSave) {
commonFiled(entity,loginUser.getId(),loginUser.getTenantId(), loginUser.getDeptId());
} else {
entity.setModified(new Date());
entity.setModifiedBy(loginUser.getId());
}
return null;
}
@Override
protected Result onRemoveBefore(Collection<Serializable> ids) {
List<SysDept> records = service.listByIds(ids);
for (SysDept dept : records) {
if (Constants.ROOT_DEPT.equals(dept.getDeptCode())) {
return Result.fail(1, "无法删除根部门");
}
}
QueryWrapper w = QueryWrapper.create();
w.in(SysAccount::getDeptId, ids);
long count = sysAccountService.count(w);
if (count > 0) {
return Result.fail(1, "该部门下有员工,不能删除");
}
return super.onRemoveBefore(ids);
}
}

View File

@@ -0,0 +1,47 @@
package tech.easyflow.admin.controller.system;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.util.SpringContextUtil;
import tech.easyflow.common.web.controller.BaseCurdController;
import tech.easyflow.common.dict.DictManager;
import tech.easyflow.system.entity.SysDict;
import tech.easyflow.system.service.SysDictService;
import com.mybatisflex.core.query.QueryWrapper;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.Serializable;
import java.util.Collection;
import java.util.List;
/**
* 系统配置表 控制层。
*
* @author michael
* @since 2024-03-05
*/
@RestController
@RequestMapping("/api/v1/sysDict")
public class SysDictController extends BaseCurdController<SysDictService, SysDict> {
public SysDictController(SysDictService service) {
super(service);
}
@Override
protected void onSaveOrUpdateAfter(SysDict entity, boolean isSave) {
DictManager dictManager = SpringContextUtil.getBean(DictManager.class);
dictManager.putLoader(entity.buildLoader());
}
@Override
protected Result onRemoveBefore(Collection<Serializable> ids) {
List<SysDict> sysDicts = service.list(QueryWrapper.create().in("id", ids));
if (sysDicts != null) {
DictManager dictManager = SpringContextUtil.getBean(DictManager.class);
sysDicts.forEach(sysDict -> dictManager.removeLoader(sysDict.getCode()));
}
return null;
}
}

View File

@@ -0,0 +1,23 @@
package tech.easyflow.admin.controller.system;
import tech.easyflow.common.annotation.UsePermission;
import tech.easyflow.common.web.controller.BaseCurdController;
import tech.easyflow.system.entity.SysDictItem;
import tech.easyflow.system.service.SysDictItemService;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 数据字典内容 控制层。
*
* @author michael
* @since 2024-03-06
*/
@RestController
@RequestMapping("/api/v1/sysDictItem")
@UsePermission(moduleName = "/api/v1/sysDict")
public class SysDictItemController extends BaseCurdController<SysDictItemService, SysDictItem> {
public SysDictItemController(SysDictItemService service) {
super(service);
}
}

View File

@@ -0,0 +1,27 @@
package tech.easyflow.admin.controller.system;
import cn.dev33.satoken.stp.SaLoginModel;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import tech.easyflow.common.util.IdUtil;
@RestController
@RequestMapping("/api/v1/token")
public class SysGenerateTokenController {
// 手动生成 Token 并绑定账号
@GetMapping("/generateToken")
public SaResult generateToken() {
long loginId = StpUtil.getLoginIdAsLong(); // 假设这是你要绑定的账号ID
String customToken = IdUtil.generateUUID();; // 自定义的 Token 字符串
SaLoginModel saLoginModel = new SaLoginModel();
saLoginModel.setToken(customToken);
saLoginModel.setTimeout(-1);
// 将 loginId 与 customToken 关联,并设置有效期(单位:秒)
StpUtil.createLoginSession(loginId, saLoginModel); // 24小时有效期
System.out.println("生成了token: " + customToken);
return SaResult.ok("Token 已生成").setData(customToken);
}
}

View File

@@ -0,0 +1,34 @@
package tech.easyflow.admin.controller.system;
import tech.easyflow.common.web.controller.BaseCurdController;
import tech.easyflow.log.annotation.LogRecord;
import tech.easyflow.system.entity.SysLog;
import tech.easyflow.system.service.SysLogService;
import com.mybatisflex.core.paginate.Page;
import com.mybatisflex.core.query.QueryWrapper;
import com.mybatisflex.core.relation.RelationManager;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Collections;
/**
* 操作日志表 控制层。
*
* @author michael
* @since 2024-03-06
*/
@RestController
@RequestMapping("/api/v1/sysLog")
public class SysLogController extends BaseCurdController<SysLogService, SysLog> {
public SysLogController(SysLogService service) {
super(service);
}
@Override
@LogRecord("分页查询")
protected Page<SysLog> queryPage(Page<SysLog> page, QueryWrapper queryWrapper) {
RelationManager.setQueryRelations(Collections.singleton("account"));
return service.getMapper().paginateWithRelations(page, queryWrapper);
}
}

View File

@@ -0,0 +1,161 @@
package tech.easyflow.admin.controller.system;
import tech.easyflow.common.constant.Constants;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.tree.Tree;
import tech.easyflow.common.vo.MenuVo;
import tech.easyflow.common.web.controller.BaseCurdController;
import tech.easyflow.system.entity.SysMenu;
import tech.easyflow.system.entity.SysRoleMenu;
import tech.easyflow.system.service.SysAccountRoleService;
import tech.easyflow.system.service.SysMenuService;
import tech.easyflow.system.service.SysRoleMenuService;
import tech.easyflow.common.satoken.util.SaTokenUtil;
import cn.hutool.core.collection.CollectionUtil;
import com.mybatisflex.core.query.QueryWrapper;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.io.Serializable;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
/**
* 菜单表 控制层。
*
* @author ArkLight
* @since 2025-03-14
*/
@RestController("sysMenuController")
@RequestMapping("/api/v1/sysMenu")
public class SysMenuController extends BaseCurdController<SysMenuService, SysMenu> {
@Resource
private SysRoleMenuService sysRoleMenuService;
@Resource
private SysAccountRoleService sysAccountRoleService;
public SysMenuController(SysMenuService service) {
super(service);
}
@Override
protected String getDefaultOrderBy() {
return "sort_no asc";
}
@Override
@GetMapping("list")
public Result<List<SysMenu>> list(SysMenu entity, Boolean asTree, String sortKey, String sortType) {
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
List<SysMenu> sysMenus = service.list(queryWrapper);
return Result.ok(Tree.tryToTree(sysMenus, "id", "parentId"));
}
@GetMapping("tree")
public Result<List<SysMenu>> tree(SysMenu entity) {
LoginAccount account = SaTokenUtil.getLoginAccount();
BigInteger accountId = account.getId();
List<SysMenu> sysMenus = service.getMenusByAccountId(entity,accountId);
return Result.ok(Tree.tryToTree(sysMenus, "id", "parentId"));
}
@GetMapping("treeV2")
public Result<List<MenuVo>> treeV2(SysMenu entity) {
LoginAccount account = SaTokenUtil.getLoginAccount();
BigInteger accountId = account.getId();
List<SysMenu> sysMenus = service.getMenusByAccountId(entity,accountId);
List<MenuVo> menuVos = buildMenuVos(sysMenus);
return Result.ok(Tree.tryToTree(menuVos, "id", "parentId"));
}
/**
* 根据角色id获取菜单树
*/
@GetMapping("getCheckedByRoleId/{roleId}")
public Result<List<BigInteger>> getCheckedByRoleId(@PathVariable BigInteger roleId) {
QueryWrapper rmWrapper = QueryWrapper.create();
rmWrapper.eq("role_id", roleId);
List<SysRoleMenu> list = sysRoleMenuService.list(rmWrapper);
List<BigInteger> menuIds = list.stream().map(SysRoleMenu::getMenuId).collect(Collectors.toList());
if (CollectionUtil.isEmpty(menuIds)) {
return Result.ok(new ArrayList<>());
}
QueryWrapper wrapper = QueryWrapper.create();
wrapper.in(SysMenu::getId,menuIds);
List<SysMenu> sysMenus = service.list(wrapper);
return Result.ok(sysMenus.stream().map(SysMenu::getId).collect(Collectors.toList()));
}
/**
* 保存后,给超级管理员角色添加权限
*/
@Override
protected void onSaveOrUpdateAfter(SysMenu entity, boolean isSave) {
if (isSave) {
SysRoleMenu admin = new SysRoleMenu();
admin.setRoleId(Constants.SUPER_ADMIN_ROLE_ID);
admin.setMenuId(entity.getId());
sysRoleMenuService.save(admin);
}
}
/**
* 删除后,删除角色菜单对应关系
*/
@Override
protected void onRemoveAfter(Collection<Serializable> ids) {
QueryWrapper w = QueryWrapper.create();
w.in(SysRoleMenu::getMenuId, ids);
sysRoleMenuService.remove(w);
}
@Override
protected Result onSaveOrUpdateBefore(SysMenu entity, boolean isSave) {
LoginAccount loginUser = SaTokenUtil.getLoginAccount();
if (isSave) {
commonFiled(entity,loginUser.getId(),loginUser.getTenantId(), loginUser.getDeptId());
} else {
entity.setModified(new Date());
entity.setModifiedBy(loginUser.getId());
}
return null;
}
private List<MenuVo> buildMenuVos(List<SysMenu> sysMenus) {
List<MenuVo> menuVos = new ArrayList<>();
for (SysMenu sysMenu : sysMenus) {
if (sysMenu.getIsShow() != 1) {
continue;
}
if (sysMenu.getMenuType() == 1) {
continue;
}
MenuVo menuVo = new MenuVo();
menuVo.setId(sysMenu.getId());
menuVo.setParentId(sysMenu.getParentId());
MenuVo.MetaVo metaVo = new MenuVo.MetaVo();
metaVo.setTitle(sysMenu.getMenuTitle());
metaVo.setIcon(sysMenu.getMenuIcon());
metaVo.setOrder(sysMenu.getSortNo());
menuVo.setMeta(metaVo);
menuVo.setName(sysMenu.getId().toString());
menuVo.setPath(sysMenu.getMenuUrl());
menuVo.setComponent(sysMenu.getComponent());
menuVos.add(menuVo);
}
return menuVos;
}
}

View File

@@ -0,0 +1,84 @@
package tech.easyflow.admin.controller.system;
import cn.dev33.satoken.annotation.SaCheckPermission;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.options.SysOptions;
import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.common.web.controller.BaseController;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.common.web.jsonbody.JsonBody;
import tech.easyflow.system.entity.SysOption;
import tech.easyflow.system.service.SysOptionService;
import com.mybatisflex.core.query.QueryWrapper;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 系统配置信息表。 控制层。
*
* @author michael
* @since 2024-03-13
*/
@RestController
@RequestMapping("/api/v1/sysOption")
public class SysOptionController extends BaseController {
@Resource
private SysOptionService service;
@GetMapping("/list")
public Result<Map<String, Object>> list(String[] keys) {
Map<String, Object> data = new HashMap<>();
if (keys == null || keys.length == 0) {
return Result.ok(data);
}
List<SysOption> list = service.list(QueryWrapper.create().in(SysOption::getKey, (Object[]) keys));
for (SysOption sysOption : list) {
data.put(sysOption.getKey(), sysOption.getValue());
}
return Result.ok(data);
}
@PostMapping("/save")
public Result<Void> save(@JsonBody Map<String, String> map) {
if (map == null || map.isEmpty()) {
return Result.ok();
}
map.forEach(SysOptions::set);
return Result.ok();
}
@PostMapping("/saveOption")
@SaCheckPermission("/api/v1/sysOption/save")
public Result<Void> saveOption(@JsonBody SysOption sysOption) {
String key = sysOption.getKey();
if (key == null || key.isEmpty()) {
throw new BusinessException("key is empty");
}
sysOption.setTenantId(SaTokenUtil.getLoginAccount().getTenantId());
SysOption record = service.getByOptionKey(key);
if (record == null) {
service.save(sysOption);
} else {
QueryWrapper w = QueryWrapper.create();
w.eq(SysOption::getKey, key);
service.update(sysOption, w);
}
return Result.ok();
}
@GetMapping("/getByKey")
public Result<SysOption> getByKey(String key) {
if (key == null || key.isEmpty()) {
throw new BusinessException("key is empty");
}
return Result.ok(service.getByOptionKey(key));
}
}

View File

@@ -0,0 +1,132 @@
package tech.easyflow.admin.controller.system;
import com.mybatisflex.core.paginate.Page;
import com.mybatisflex.core.query.QueryWrapper;
import com.mybatisflex.core.util.StringUtil;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.common.web.controller.BaseCurdController;
import tech.easyflow.common.web.jsonbody.JsonBody;
import tech.easyflow.system.entity.SysPosition;
import tech.easyflow.system.service.SysPositionService;
import java.util.Date;
import java.util.Map;
import static tech.easyflow.system.entity.table.SysPositionTableDef.SYS_POSITION;
/**
* 职位表 控制层。
* <p>
* 提供岗位的增删改查及状态管理功能。
* </p>
*
* @author ArkLight
* @since 2025-03-14
*/
@RestController("sysPositionController")
@RequestMapping("/api/v1/sysPosition")
public class SysPositionController extends BaseCurdController<SysPositionService, SysPosition> {
public SysPositionController(SysPositionService service) {
super(service);
}
/**
* 分页查询岗位列表
* <p>
* 支持按岗位名称模糊查询,按状态、编码精确查询。
* </p>
*
* @param request 请求对象
* @param sortKey 排序字段
* @param sortType 排序类型 (asc/desc)
* @param pageNumber 当前页码
* @param pageSize 每页条数
* @return 分页结果
*/
@Override
@GetMapping("page")
public Result<Page<SysPosition>> page(HttpServletRequest request, String sortKey, String sortType, Long pageNumber, Long pageSize) {
if (pageNumber == null || pageNumber < 1) {
pageNumber = 1L;
}
if (pageSize == null || pageSize < 1) {
pageSize = 10L;
}
// 构建自定义查询条件
QueryWrapper queryWrapper = QueryWrapper.create()
.select(SYS_POSITION.ALL_COLUMNS)
.from(SYS_POSITION);
// 获取查询参数
String positionName = request.getParameter("positionName");
String positionCode = request.getParameter("positionCode");
String status = request.getParameter("status");
// 岗位名称 - 模糊查询
if (StringUtil.hasText(positionName)) {
queryWrapper.where(SYS_POSITION.POSITION_NAME.like(positionName));
}
// 岗位编码 - 精确查询
if (StringUtil.hasText(positionCode)) {
queryWrapper.where(SYS_POSITION.POSITION_CODE.eq(positionCode));
}
// 状态 - 精确查询
if (StringUtil.hasText(status)) {
queryWrapper.where(SYS_POSITION.STATUS.eq(status));
}
// 处理排序
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
return Result.ok(service.page(new Page<>(pageNumber, pageSize), queryWrapper));
}
/**
* 修改岗位状态(启用/禁用)
*
* @param body 包含 id 和 status 的 JSON 对象
* @return 操作结果
*/
@PostMapping("changeStatus")
public Result<?> changeStatus(@RequestBody Map<String, Object> body) {
String idStr = (String) body.get("id");
Integer status = (Integer) body.get("status");
if (StringUtil.noText(idStr) || status == null) {
return Result.fail("参数错误id 和 status 不能为空");
}
SysPosition position = new SysPosition();
position.setId(new java.math.BigInteger(idStr));
position.setStatus(status);
// 设置修改信息
LoginAccount loginUser = SaTokenUtil.getLoginAccount();
position.setModified(new Date());
position.setModifiedBy(loginUser.getId());
boolean success = service.updateById(position);
return success ? Result.ok() : Result.fail("状态修改失败");
}
@Override
protected Result onSaveOrUpdateBefore(SysPosition entity, boolean isSave) {
LoginAccount loginUser = SaTokenUtil.getLoginAccount();
if (isSave) {
commonFiled(entity, loginUser.getId(), loginUser.getTenantId(), entity.getDeptId());
} else {
entity.setModified(new Date());
entity.setModifiedBy(loginUser.getId());
}
return null;
}
}

View File

@@ -0,0 +1,118 @@
package tech.easyflow.admin.controller.system;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.mybatisflex.core.query.QueryWrapper;
import org.springframework.web.bind.annotation.*;
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.controller.BaseCurdController;
import tech.easyflow.common.web.jsonbody.JsonBody;
import tech.easyflow.system.entity.SysRole;
import tech.easyflow.system.entity.SysRoleDept;
import tech.easyflow.system.entity.SysRoleMenu;
import tech.easyflow.system.service.SysRoleDeptService;
import tech.easyflow.system.service.SysRoleMenuService;
import tech.easyflow.system.service.SysRoleService;
import javax.annotation.Resource;
import java.io.Serializable;
import java.math.BigInteger;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
/**
* 系统角色 控制层。
*
* @author ArkLight
* @since 2025-03-14
*/
@RestController("sysRoleController")
@RequestMapping("/api/v1/sysRole/")
public class SysRoleController extends BaseCurdController<SysRoleService, SysRole> {
@Resource
private SysRoleMenuService sysRoleMenuService;
@Resource
private SysRoleDeptService sysRoleDeptService;
public SysRoleController(SysRoleService service) {
super(service);
}
@PostMapping("saveRoleMenu/{roleId}")
@SaCheckPermission("/api/v1/sysRole/save")
@Deprecated
public Result<Void> saveRoleMenu(@PathVariable("roleId") BigInteger roleId, @JsonBody List<String> keys) {
service.saveRoleMenu(roleId, keys);
return Result.ok();
}
/**
* 获取角色菜单id
*/
@GetMapping("/getRoleMenuIds")
@SaCheckPermission("/api/v1/sysRole/query")
public Result<List<BigInteger>> getRoleMenuIds(BigInteger roleId) {
QueryWrapper w = QueryWrapper.create();
w.eq("role_id", roleId);
List<BigInteger> res = sysRoleMenuService.list(w).stream().map(SysRoleMenu::getMenuId).collect(Collectors.toList());
return Result.ok(res);
}
/**
* 获取角色部门id
*/
@GetMapping("/getRoleDeptIds")
@SaCheckPermission("/api/v1/sysRole/query")
public Result<List<BigInteger>> getRoleDeptIds(BigInteger roleId) {
QueryWrapper w = QueryWrapper.create();
w.eq("role_id", roleId);
List<BigInteger> res = sysRoleDeptService.list(w).stream().map(SysRoleDept::getDeptId).collect(Collectors.toList());
return Result.ok(res);
}
/**
* 保存角色
*/
@PostMapping("saveRole")
@SaCheckPermission("/api/v1/sysRole/save")
public Result<Void> saveRole(@JsonBody SysRole entity) {
LoginAccount loginUser = SaTokenUtil.getLoginAccount();
if (entity.getId() == null) {
commonFiled(entity, loginUser.getId(), loginUser.getTenantId(), loginUser.getDeptId());
}
service.saveRole(entity);
return Result.ok();
}
@Override
protected Result onSaveOrUpdateBefore(SysRole entity, boolean isSave) {
LoginAccount loginUser = SaTokenUtil.getLoginAccount();
if (isSave) {
commonFiled(entity, loginUser.getId(), loginUser.getTenantId(), loginUser.getDeptId());
} else {
entity.setModified(new Date());
entity.setModifiedBy(loginUser.getId());
}
return null;
}
@Override
protected Result onRemoveBefore(Collection<Serializable> ids) {
List<SysRole> sysRoles = service.listByIds(ids);
for (SysRole sysRole : sysRoles) {
String roleKey = sysRole.getRoleKey();
if (Constants.SUPER_ADMIN_ROLE_CODE.equals(roleKey)) {
return Result.fail(1, "超级管理员角色不能删除");
}
if (Constants.TENANT_ADMIN_ROLE_CODE.equals(roleKey)) {
return Result.fail(1, "租户管理员角色不能删除");
}
}
return super.onRemoveBefore(ids);
}
}

View File

@@ -0,0 +1,31 @@
package tech.easyflow.admin.controller.system;
import cn.dev33.satoken.annotation.SaIgnore;
import cn.dev33.satoken.stp.StpUtil;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import tech.easyflow.common.constant.Constants;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.entity.LoginAccount;
import java.math.BigInteger;
@RestController
@RequestMapping("/api/temp-token")
public class SysTempTokenController {
@GetMapping("/create")
@SaIgnore
public Result<String> createTempToken() {
StpUtil.login(0);
String tokenValue = StpUtil.getTokenValue();
LoginAccount loginAccount = new LoginAccount();
loginAccount.setId(BigInteger.valueOf(0));
loginAccount.setLoginName("匿名用户");
StpUtil.getSession().set(Constants.LOGIN_USER_KEY, loginAccount);
return Result.ok("", tokenValue);
}
}

View File

@@ -0,0 +1,36 @@
package tech.easyflow.admin.controller.system;
import cn.dev33.satoken.stp.StpUtil;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.web.controller.BaseCurdController;
import tech.easyflow.system.entity.SysUserFeedback;
import tech.easyflow.system.service.SysUserFeedbackService;
import java.math.BigInteger;
import java.util.Date;
/**
* 控制层。
*
* @author 12076
* @since 2025-12-30
*/
@RestController
@RequestMapping("/api/v1/sysUserFeedback")
public class SysUserFeedbackController extends BaseCurdController<SysUserFeedbackService, SysUserFeedback> {
public SysUserFeedbackController(SysUserFeedbackService service) {
super(service);
}
@Override
protected Result<?> onSaveOrUpdateBefore(SysUserFeedback entity, boolean isSave) {
if (!isSave) {
entity.setHandlerId(new BigInteger(StpUtil.getLoginIdAsString()));
entity.setModified(new Date());
entity.setHandleTime(new Date());
}
return super.onSaveOrUpdateBefore(entity, isSave);
}
}

View File

@@ -0,0 +1,14 @@
<?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-mcp</artifactId>
</project>

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

View File

@@ -0,0 +1,29 @@
<?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-usercenter</artifactId>
<dependencies>
<dependency>
<groupId>tech.easyflow</groupId>
<artifactId>easyflow-module-auth</artifactId>
</dependency>
<dependency>
<groupId>tech.easyflow</groupId>
<artifactId>easyflow-module-ai</artifactId>
</dependency>
<dependency>
<groupId>tech.easyflow</groupId>
<artifactId>easyflow-common-captcha</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,23 @@
package tech.easyflow.usercenter.controller.ai;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import tech.easyflow.ai.entity.BotCategory;
import tech.easyflow.ai.service.BotCategoryService;
import tech.easyflow.common.annotation.UsePermission;
import tech.easyflow.common.web.controller.BaseCurdController;
/**
* bot分类 控制层。
*
* @author ArkLight
* @since 2025-12-18
*/
@RestController
@RequestMapping("/userCenter/botCategory")
@UsePermission(moduleName = "/api/v1/bot")
public class UcBotCategoryController extends BaseCurdController<BotCategoryService, BotCategory> {
public UcBotCategoryController(BotCategoryService service) {
super(service);
}
}

View File

@@ -0,0 +1,276 @@
package tech.easyflow.usercenter.controller.ai;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.annotation.SaIgnore;
import com.alicp.jetcache.Cache;
import com.mybatisflex.core.keygen.impl.SnowFlakeIDKeyGenerator;
import com.mybatisflex.core.query.QueryWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import tech.easyflow.ai.entity.*;
import tech.easyflow.ai.service.*;
import tech.easyflow.ai.service.impl.BotServiceImpl;
import tech.easyflow.common.annotation.UsePermission;
import tech.easyflow.common.audio.core.AudioServiceManager;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.common.web.controller.BaseCurdController;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.common.web.jsonbody.JsonBody;
import javax.annotation.Resource;
import java.io.Serializable;
import java.math.BigInteger;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 控制层。
*
* @author michael
* @since 2024-08-23
*/
@RestController
@RequestMapping("/userCenter/bot")
@UsePermission(moduleName = "/api/v1/bot")
public class UcBotController extends BaseCurdController<BotService, Bot> {
private final ModelService modelService;
private final BotWorkflowService botWorkflowService;
private final BotDocumentCollectionService botDocumentCollectionService;
@Resource
private BotService botService;
@Autowired
@Qualifier("defaultCache") // 指定 Bean 名称
private Cache<String, Object> cache;
@Resource
private AudioServiceManager audioServiceManager;
public UcBotController(BotService service, ModelService modelService, BotWorkflowService botWorkflowService,
BotDocumentCollectionService botDocumentCollectionService) {
super(service);
this.modelService = modelService;
this.botWorkflowService = botWorkflowService;
this.botDocumentCollectionService = botDocumentCollectionService;
}
@Resource
private BotPluginService botPluginService;
@Resource
private BotConversationService conversationMessageService;
@GetMapping("/generateConversationId")
public Result<Long> generateConversationId() {
long nextId = new SnowFlakeIDKeyGenerator().nextId();
return Result.ok(nextId);
}
@PostMapping("updateOptions")
@SaCheckPermission("/api/v1/bot/save")
public Result<Void> updateOptions(@JsonBody("id") BigInteger id,
@JsonBody("options") Map<String, Object> options) {
Bot aiBot = service.getById(id);
Map<String, Object> existOptions = aiBot.getOptions();
if (existOptions == null) {
existOptions = new HashMap<>();
}
if (options != null) {
existOptions.putAll(options);
}
aiBot.setOptions(existOptions);
service.updateById(aiBot);
return Result.ok();
}
@PostMapping("updateLlmOptions")
@SaCheckPermission("/api/v1/bot/save")
public Result<Void> updateLlmOptions(@JsonBody("id")
BigInteger id, @JsonBody("llmOptions")
Map<String, Object> llmOptions) {
Bot aiBot = service.getById(id);
Map<String, Object> existLlmOptions = aiBot.getModelOptions();
if (existLlmOptions == null) {
existLlmOptions = new HashMap<>();
}
if (llmOptions != null) {
existLlmOptions.putAll(llmOptions);
}
aiBot.setModelOptions(existLlmOptions);
service.updateById(aiBot);
return Result.ok();
}
@PostMapping("voiceInput")
@SaIgnore
public Result<String> voiceInput(@RequestParam("audio")
MultipartFile audioFile) {
String recognize = null;
try {
recognize = audioServiceManager.audioToText(audioFile.getInputStream());
} catch (Exception e) {
throw new RuntimeException(e);
}
return Result.ok("", recognize);
}
/**
* 处理聊天请求的接口方法
*
* @param prompt 用户输入的聊天内容,必须提供
* @param botId 聊天机器人的唯一标识符,必须提供
* @param conversationId 会话ID用于标识当前对话会话必须提供
* @param messages 历史消息,用于提供上下文,可选
* @return 返回SseEmitter对象用于服务器向客户端推送聊天响应数据
*/
@PostMapping("chat")
@SaIgnore
public SseEmitter chat(
@JsonBody(value = "prompt", required = true) String prompt,
@JsonBody(value = "botId", required = true) BigInteger botId,
@JsonBody(value = "conversationId", required = true) BigInteger conversationId,
@JsonBody(value = "messages") List<Map<String, String>> messages,
@JsonBody(value = "attachments") List<String> attachments
) {
BotServiceImpl.ChatCheckResult chatCheckResult = new BotServiceImpl.ChatCheckResult();
// 前置校验失败则直接返回错误SseEmitter
SseEmitter errorEmitter = botService.checkChatBeforeStart(botId, prompt, conversationId.toString(), chatCheckResult);
if (errorEmitter != null) {
return errorEmitter;
}
BotConversation conversation = conversationMessageService.getById(conversationId);
if (conversation == null) {
conversation = new BotConversation();
conversation.setId(conversationId);
if (prompt.length() > 200) {
conversation.setTitle(prompt.substring(0, 200));
} else {
conversation.setTitle(prompt);
}
conversation.setBotId(botId);
conversation.setAccountId(SaTokenUtil.getLoginAccount().getId());
commonFiled(conversation, SaTokenUtil.getLoginAccount().getId(), SaTokenUtil.getLoginAccount().getTenantId(), SaTokenUtil.getLoginAccount().getDeptId());
conversationMessageService.save(conversation);
}
return botService.startChat(botId, prompt, conversationId, messages, chatCheckResult, attachments);
}
@PostMapping("updateLlmId")
@SaCheckPermission("/api/v1/bot/save")
public Result<Void> updateBotLlmId(@RequestBody
Bot aiBot) {
service.updateBotLlmId(aiBot);
return Result.ok();
}
@GetMapping("getDetail")
@SaIgnore
public Result<Bot> getDetail(String id) {
return Result.ok(botService.getDetail(id));
}
@Override
@SaIgnore
public Result<Bot> detail(String id) {
Bot data = botService.getDetail(id);
if (data == null) {
return Result.ok(data);
}
Map<String, Object> llmOptions = data.getModelOptions();
if (llmOptions == null) {
llmOptions = new HashMap<>();
}
if (data.getModelId() == null) {
return Result.ok(data);
}
BigInteger llmId = data.getModelId();
Model llm = modelService.getById(llmId);
if (llm == null) {
data.setModelId(null);
return Result.ok(data);
}
Map<String, Object> options = llm.getOptions();
if (options != null && !options.isEmpty()) {
// 获取是否多模态
Boolean multimodal = (Boolean) options.get("multimodal");
llmOptions.put("multimodal", multimodal != null && multimodal);
}
return Result.ok(data);
}
@Override
protected Result<?> onSaveOrUpdateBefore(Bot entity, boolean isSave) {
String alias = entity.getAlias();
if (StringUtils.hasLength(alias)) {
Bot aiBot = service.getByAlias(alias);
if (aiBot != null && isSave) {
throw new BusinessException("别名已存在!");
}
if (aiBot != null && aiBot.getId().compareTo(entity.getId()) != 0) {
throw new BusinessException("别名已存在!");
}
}
if (isSave) {
// 设置默认值
entity.setModelOptions(getDefaultLlmOptions());
}
return super.onSaveOrUpdateBefore(entity, isSave);
}
private Map<String, Object> getDefaultLlmOptions() {
Map<String, Object> defaultLlmOptions = new HashMap<>();
defaultLlmOptions.put("temperature", 0.7);
defaultLlmOptions.put("topK", 4);
defaultLlmOptions.put("maxReplyLength", 2048);
defaultLlmOptions.put("topP", 0.7);
defaultLlmOptions.put("maxMessageCount", 10);
return defaultLlmOptions;
}
private Map<String, Object> errorRespnseMsg(int errorCode, String message) {
HashMap<String, Object> result = new HashMap<>();
result.put("error", errorCode);
result.put("message", message);
return result;
}
@Override
protected Result<?> onRemoveBefore(Collection<Serializable> ids) {
QueryWrapper queryWrapperKnowledge = QueryWrapper.create().in(BotDocumentCollection::getBotId, ids);
botDocumentCollectionService.remove(queryWrapperKnowledge);
QueryWrapper queryWrapperBotWorkflow = QueryWrapper.create().in(BotWorkflow::getBotId, ids);
botWorkflowService.remove(queryWrapperBotWorkflow);
QueryWrapper queryWrapperBotPlugins = QueryWrapper.create().in(BotPlugin::getBotId, ids);
botPluginService.remove(queryWrapperBotPlugins);
return super.onRemoveBefore(ids);
}
}

View File

@@ -0,0 +1,108 @@
package tech.easyflow.usercenter.controller.ai;
import cn.dev33.satoken.annotation.SaIgnore;
import com.mybatisflex.core.paginate.Page;
import com.mybatisflex.core.query.QueryWrapper;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import tech.easyflow.ai.entity.BotConversation;
import tech.easyflow.ai.service.BotConversationService;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.common.web.controller.BaseCurdController;
import tech.easyflow.common.web.exceptions.BusinessException;
import javax.annotation.Resource;
import java.util.Date;
import java.util.List;
@RestController
@RequestMapping("/userCenter/botConversation")
@SaIgnore
public class UcBotConversationController extends BaseCurdController<BotConversationService, BotConversation> {
@Resource
private BotConversationService conversationMessageService;
public UcBotConversationController(BotConversationService service) {
super(service);
}
/**
* 删除指定会话
*/
@GetMapping("/deleteConversation")
public Result<Void> deleteConversation(String botId, String conversationId) {
LoginAccount account = SaTokenUtil.getLoginAccount();
conversationMessageService.deleteConversation(botId, conversationId, account.getId());
return Result.ok();
}
/**
* 更新会话标题
*/
@GetMapping("/updateConversation")
public Result<Void> updateConversation(String botId, String conversationId, String title) {
LoginAccount account = SaTokenUtil.getLoginAccount();
conversationMessageService.updateConversation(botId, conversationId, title, account.getId());
return Result.ok();
}
@Override
public Result<List<BotConversation>> list(BotConversation entity, Boolean asTree, String sortKey, String sortType) {
entity.setAccountId(SaTokenUtil.getLoginAccount().getId());
sortKey = "created";
sortType = "desc";
return super.list(entity, asTree, sortKey, sortType);
}
@Override
protected Result<?> onSaveOrUpdateBefore(BotConversation entity, boolean isSave) {
entity.setAccountId(SaTokenUtil.getLoginAccount().getId());
entity.setCreated(new Date());
return super.onSaveOrUpdateBefore(entity, isSave);
}
/**
* 分页查询会话列表
*
* @param request 查询数据
* @param sortKey 排序字段
* @param sortType 排序方式 asc | desc
* @param pageNumber 当前页码
* @param pageSize 每页的数据量
* @return
*/
@GetMapping("pageList")
public Result<Page<BotConversation>> page(HttpServletRequest request, String sortKey, String sortType, Long pageNumber, Long pageSize) {
if (pageNumber == null || pageNumber < 1) {
pageNumber = 1L;
}
if (pageSize == null || pageSize < 1) {
pageSize = 10L;
}
QueryWrapper queryWrapper = buildQueryWrapper(request);
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
Page<BotConversation> botConversationPage = service.getMapper().paginateWithRelations(pageNumber, pageSize, queryWrapper);
return Result.ok(botConversationPage);
}
/**
* 根据表主键查询数据详情。
*
* @param id 主键值
* @return 内容详情
*/
@GetMapping("detail")
@SaIgnore
public Result<BotConversation> detail(String id) {
if (tech.easyflow.common.util.StringUtil.noText(id)) {
throw new BusinessException("id must not be null");
}
return Result.ok(service.getMapper().selectOneWithRelationsById(id));
}
}

View File

@@ -0,0 +1,60 @@
package tech.easyflow.usercenter.controller.ai;
import cn.dev33.satoken.annotation.SaIgnore;
import cn.hutool.core.collection.CollectionUtil;
import com.alibaba.fastjson.JSON;
import com.mybatisflex.core.query.QueryWrapper;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import tech.easyflow.ai.entity.BotMessage;
import tech.easyflow.ai.service.BotMessageService;
import tech.easyflow.ai.vo.ChatMessageVO;
import tech.easyflow.common.annotation.UsePermission;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.common.web.controller.BaseCurdController;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;
/**
* Bot 消息记录表 控制层。
*
* @author michael
* @since 2024-11-04
*/
@RestController
@RequestMapping("/userCenter/botMessage")
@UsePermission(moduleName = "/api/v1/bot")
public class UcBotMessageController extends BaseCurdController<BotMessageService, BotMessage> {
private final BotMessageService botMessageService;
public UcBotMessageController(BotMessageService service, BotMessageService botMessageService) {
super(service);
this.botMessageService = botMessageService;
}
@GetMapping("/getMessages")
@SaIgnore
public Result<List<ChatMessageVO>> getMessages(BigInteger botId, BigInteger conversationId) {
List<ChatMessageVO> res = new ArrayList<>();
QueryWrapper w = QueryWrapper.create();
w.eq(BotMessage::getBotId, botId);
w.eq(BotMessage::getConversationId, conversationId);
List<BotMessage> list = botMessageService.list(w);
if (CollectionUtil.isNotEmpty(list)) {
for (BotMessage message : list) {
ChatMessageVO vo = new ChatMessageVO();
vo.setKey(message.getId().toString());
vo.setRole(message.getRole());
vo.setContent(JSON.parseObject(message.getContent()).getString("textContent"));
vo.setPlacement("user".equals(message.getRole()) ? "end" : "start");
vo.setCreated(message.getCreated());
res.add(vo);
}
}
return Result.ok(res);
}
}

View File

@@ -0,0 +1,84 @@
package tech.easyflow.usercenter.controller.ai;
import cn.hutool.core.collection.CollectionUtil;
import com.mybatisflex.core.query.QueryWrapper;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import tech.easyflow.ai.entity.Bot;
import tech.easyflow.ai.entity.BotRecentlyUsed;
import tech.easyflow.ai.service.BotRecentlyUsedService;
import tech.easyflow.ai.service.BotService;
import tech.easyflow.common.annotation.UsePermission;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.common.web.controller.BaseCurdController;
import javax.annotation.Resource;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
/**
* 最近使用 控制层。
*
* @author ArkLight
* @since 2025-12-18
*/
@RestController
@RequestMapping("/userCenter/botRecentlyUsed")
@UsePermission(moduleName = "/api/v1/bot")
public class UcBotRecentlyUsedController extends BaseCurdController<BotRecentlyUsedService, BotRecentlyUsed> {
@Resource
private BotService botService;
public UcBotRecentlyUsedController(BotRecentlyUsedService service) {
super(service);
}
@GetMapping("/getRecentlyBot")
public Result<List<Bot>> getRecentlyBot() {
LoginAccount account = SaTokenUtil.getLoginAccount();
QueryWrapper w = QueryWrapper.create();
w.eq(BotRecentlyUsed::getCreatedBy,account.getId());
w.orderBy(BotRecentlyUsed::getSortNo,true);
List<BotRecentlyUsed> list = service.list(w);
if (CollectionUtil.isNotEmpty(list)) {
List<BigInteger> botIds = list.stream().map(BotRecentlyUsed::getBotId).collect(Collectors.toList());
QueryWrapper botQw = QueryWrapper.create();
botQw.in(Bot::getId,botIds);
List<Bot> listBot = botService.list(botQw);
listBot.sort(Comparator.comparing(bot -> botIds.indexOf(bot.getId())));
return Result.ok(listBot);
}
return Result.ok(new ArrayList<>());
}
@GetMapping("/removeByBotId")
public Result<Void> removeByBotId(BigInteger botId) {
QueryWrapper w = QueryWrapper.create();
w.eq(BotRecentlyUsed::getBotId,botId);
w.eq(BotRecentlyUsed::getCreatedBy,SaTokenUtil.getLoginAccount().getId());
service.remove(w);
return Result.ok();
}
@Override
public Result<List<BotRecentlyUsed>> list(BotRecentlyUsed entity, Boolean asTree, String sortKey, String sortType) {
LoginAccount account = SaTokenUtil.getLoginAccount();
entity.setCreatedBy(account.getId());
return super.list(entity, asTree, sortKey, sortType);
}
@Override
protected Result<?> onSaveOrUpdateBefore(BotRecentlyUsed entity, boolean isSave) {
entity.setCreated(new Date());
entity.setCreatedBy(SaTokenUtil.getLoginAccount().getId());
return super.onSaveOrUpdateBefore(entity, isSave);
}
}

View File

@@ -0,0 +1,58 @@
package tech.easyflow.usercenter.controller.ai;
import cn.hutool.core.io.FileTypeUtil;
import cn.hutool.http.HttpUtil;
import com.mybatisflex.core.paginate.Page;
import com.mybatisflex.core.query.QueryWrapper;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import tech.easyflow.ai.entity.Resource;
import tech.easyflow.ai.service.ResourceService;
import tech.easyflow.common.annotation.UsePermission;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.common.web.controller.BaseCurdController;
import java.io.ByteArrayInputStream;
import java.math.BigInteger;
import java.util.Date;
/**
* 素材库
*
* @author ArkLight
* @since 2025-06-27
*/
@RestController
@RequestMapping("/userCenter/resource")
@UsePermission(moduleName = "/api/v1/resource")
public class UcResourceController extends BaseCurdController<ResourceService, Resource> {
public UcResourceController(ResourceService service) {
super(service);
}
@Override
protected Result<?> onSaveOrUpdateBefore(Resource entity, boolean isSave) {
LoginAccount loginUser = SaTokenUtil.getLoginAccount();
if (isSave) {
String resourceUrl = entity.getResourceUrl();
byte[] bytes = HttpUtil.downloadBytes(resourceUrl);
ByteArrayInputStream stream = new ByteArrayInputStream(bytes);
String suffix = FileTypeUtil.getType(stream, resourceUrl);
entity.setSuffix(suffix);
entity.setFileSize(BigInteger.valueOf(bytes.length));
commonFiled(entity,loginUser.getId(),loginUser.getTenantId(), loginUser.getDeptId());
} else {
entity.setModified(new Date());
entity.setModifiedBy(loginUser.getId());
}
return super.onSaveOrUpdateBefore(entity, isSave);
}
@Override
protected Page<Resource> queryPage(Page<Resource> page, QueryWrapper queryWrapper) {
queryWrapper.eq(Resource::getCreatedBy, SaTokenUtil.getLoginAccount().getId().toString());
return super.queryPage(page, queryWrapper);
}
}

View File

@@ -0,0 +1,38 @@
package tech.easyflow.usercenter.controller.ai;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import tech.easyflow.ai.entity.WorkflowCategory;
import tech.easyflow.ai.service.WorkflowCategoryService;
import tech.easyflow.common.annotation.UsePermission;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.web.controller.BaseCurdController;
import java.io.Serializable;
import java.util.Collection;
/**
* 工作流分类
*
* @author ArkLight
* @since 2025-12-11
*/
@RestController
@RequestMapping("/userCenter/workflowCategory")
@UsePermission(moduleName = "/api/v1/workflow")
public class UcWorkflowCategoryController extends BaseCurdController<WorkflowCategoryService, WorkflowCategory> {
public UcWorkflowCategoryController(WorkflowCategoryService service) {
super(service);
}
@Override
protected Result<?> onSaveOrUpdateBefore(WorkflowCategory entity, boolean isSave) {
return Result.fail("-");
}
@Override
protected Result<?> onRemoveBefore(Collection<Serializable> ids) {
return Result.fail("-");
}
}

View File

@@ -0,0 +1,139 @@
package tech.easyflow.usercenter.controller.ai;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.stp.StpUtil;
import com.easyagents.flow.core.chain.*;
import com.easyagents.flow.core.chain.runtime.ChainExecutor;
import com.easyagents.flow.core.parser.ChainParser;
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.annotation.UsePermission;
import tech.easyflow.common.constant.Constants;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.common.web.controller.BaseCurdController;
import tech.easyflow.common.web.jsonbody.JsonBody;
import javax.annotation.Resource;
import java.io.Serializable;
import java.math.BigInteger;
import java.util.*;
/**
* 工作流
*/
@RestController
@RequestMapping("/userCenter/workflow")
@UsePermission(moduleName = "/api/v1/workflow")
public class UcWorkflowController extends BaseCurdController<WorkflowService, Workflow> {
@Resource
private ChainExecutor chainExecutor;
@Resource
private ChainParser chainParser;
@Resource
private TinyFlowService tinyFlowService;
public UcWorkflowController(WorkflowService service) {
super(service);
}
/**
* 节点单独运行
*/
@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 = service.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 = service.getById(id);
if (workflow == null) {
throw new RuntimeException("工作流不存在");
}
if (StpUtil.isLogin()) {
variables.put(Constants.LOGIN_USER_KEY, SaTokenUtil.getLoginAccount());
}
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();
}
/**
* 获取工作流参数 - v2
*/
@GetMapping("getRunningParameters")
@SaCheckPermission("/api/v1/workflow/query")
public Result<?> getRunningParameters(@RequestParam BigInteger id) {
Workflow workflow = service.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);
}
@Override
protected Result<?> onSaveOrUpdateBefore(Workflow entity, boolean isSave) {
return Result.fail("-");
}
@Override
protected Result<?> onRemoveBefore(Collection<Serializable> ids) {
return Result.fail("-");
}
}

View File

@@ -0,0 +1,92 @@
package tech.easyflow.usercenter.controller.ai;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.util.StrUtil;
import com.mybatisflex.core.paginate.Page;
import com.mybatisflex.core.query.QueryWrapper;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import tech.easyflow.ai.entity.WorkflowExecResult;
import tech.easyflow.ai.entity.WorkflowExecStep;
import tech.easyflow.ai.service.WorkflowExecResultService;
import tech.easyflow.ai.service.WorkflowExecStepService;
import tech.easyflow.ai.utils.WorkFlowUtil;
import tech.easyflow.common.annotation.UsePermission;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.common.web.controller.BaseCurdController;
import javax.annotation.Resource;
import java.math.BigInteger;
/**
* 工作流执行记录
*/
@RestController
@RequestMapping("/userCenter/workflowExecResult")
@UsePermission(moduleName = "/api/v1/workflow")
public class UcWorkflowExecResultController extends BaseCurdController<WorkflowExecResultService, WorkflowExecResult> {
@Resource
private WorkflowExecStepService recordStepService;
public UcWorkflowExecResultController(WorkflowExecResultService service) {
super(service);
}
/**
* 删除
*/
@GetMapping("/del")
@Transactional(rollbackFor = Exception.class)
@SaCheckPermission("/api/v1/workflow/remove")
public Result<Void> del(BigInteger id) {
LoginAccount account = SaTokenUtil.getLoginAccount();
WorkflowExecResult record = service.getById(id);
if (!account.getId().toString().equals(record.getCreatedBy())) {
return Result.fail(1, "非法请求");
}
service.removeById(id);
QueryWrapper w = QueryWrapper.create();
w.eq(WorkflowExecStep::getRecordId, id);
recordStepService.remove(w);
return Result.ok();
}
@GetMapping("getPage")
public Result<Page<WorkflowExecResult>> getPage(HttpServletRequest request,
String sortKey,
String sortType,
Long pageNumber,
Long pageSize,
String queryBegin,
String queryEnd) {
if (pageNumber == null || pageNumber < 1) {
pageNumber = 1L;
}
if (pageSize == null || pageSize < 1) {
pageSize = 10L;
}
QueryWrapper queryWrapper = buildQueryWrapper(request);
if (StrUtil.isNotEmpty(queryBegin) && StrUtil.isNotEmpty(queryEnd)) {
queryWrapper.between(WorkflowExecResult::getStartTime, queryBegin, queryEnd);
}
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
return Result.ok(queryPage(new Page<>(pageNumber, pageSize), queryWrapper));
}
@Override
protected Page<WorkflowExecResult> queryPage(Page<WorkflowExecResult> page, QueryWrapper queryWrapper) {
queryWrapper.eq(WorkflowExecResult::getCreatedBy, SaTokenUtil.getLoginAccount().getId().toString());
Page<WorkflowExecResult> res = super.queryPage(page, queryWrapper);
for (WorkflowExecResult record : res.getRecords()) {
record.setWorkflowJson(WorkFlowUtil.removeSensitiveInfo(record.getWorkflowJson()));
}
return res;
}
}

View File

@@ -0,0 +1,66 @@
package tech.easyflow.usercenter.controller.ai;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.mybatisflex.core.query.QueryWrapper;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import tech.easyflow.ai.entity.WorkflowExecResult;
import tech.easyflow.ai.entity.WorkflowExecStep;
import tech.easyflow.ai.service.WorkflowExecResultService;
import tech.easyflow.ai.service.WorkflowExecStepService;
import tech.easyflow.common.annotation.UsePermission;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.web.controller.BaseCurdController;
import tech.easyflow.common.web.exceptions.BusinessException;
import javax.annotation.Resource;
import java.math.BigInteger;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 执行记录步骤
*/
@RestController
@RequestMapping("/userCenter/workflowExecStep")
@UsePermission(moduleName = "/api/v1/workflow")
public class UcWorkflowExecStepController extends BaseCurdController<WorkflowExecStepService, WorkflowExecStep> {
@Resource
private WorkflowExecResultService execRecordService;
public UcWorkflowExecStepController(WorkflowExecStepService service) {
super(service);
}
/**
* 根据执行记录id获取执行记录步骤列表
*/
@GetMapping("/getListByRecordId")
public Result<List<WorkflowExecStep>> getListByRecordId(BigInteger recordId) {
if (recordId == null) {
throw new BusinessException("recordId不能为空");
}
WorkflowExecResult record = execRecordService.getById(recordId);
String workflowJson = record.getWorkflowJson();
JSONObject workflow = JSON.parseObject(workflowJson);
Map<String, String> idTypeMap = new HashMap<>();
JSONArray nodes = workflow.getJSONArray("nodes");
for (Object node : nodes) {
JSONObject nodeObj = (JSONObject) node;
idTypeMap.put(nodeObj.getString("id"), nodeObj.getString("type"));
}
QueryWrapper w = QueryWrapper.create();
w.eq(WorkflowExecStep::getRecordId, recordId);
List<WorkflowExecStep> list = service.list(w);
for (WorkflowExecStep step : list) {
step.setNodeData(null);
step.setNodeType(idTypeMap.get(step.getNodeId()));
}
return Result.ok(list);
}
}

View File

@@ -0,0 +1,51 @@
package tech.easyflow.usercenter.controller.auth;
import cn.dev33.satoken.stp.StpUtil;
import org.springframework.web.bind.annotation.*;
import tech.easyflow.auth.entity.LoginDTO;
import tech.easyflow.auth.entity.LoginVO;
import tech.easyflow.auth.service.AuthService;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.web.jsonbody.JsonBody;
import javax.annotation.Resource;
import java.util.List;
/**
* 认证
*/
@RestController
@RequestMapping("/userCenter/auth/")
public class UcAuthController {
@Resource
private AuthService authService;
/**
* 登录
* @param loginDTO 登录参数
*/
@PostMapping("login")
public Result<LoginVO> login(@JsonBody LoginDTO loginDTO) {
LoginVO res = authService.login(loginDTO);
return Result.ok(res);
}
/**
* 登出
*/
@PostMapping("logout")
public Result<Void> logout() {
StpUtil.logout();
return Result.ok();
}
/**
* 获取权限
*/
@GetMapping("getPermissions")
public Result<List<String>> getPermissions() {
List<String> permissionList = StpUtil.getPermissionList();
return Result.ok(permissionList);
}
}

View File

@@ -0,0 +1,46 @@
package tech.easyflow.usercenter.controller.common;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.dict.Dict;
import tech.easyflow.common.dict.DictItem;
import tech.easyflow.common.dict.DictLoader;
import tech.easyflow.common.dict.DictManager;
import javax.annotation.Resource;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* 字典
*/
@RestController
@RequestMapping("/userCenter/dict/")
public class UcDictController {
@Resource
DictManager dictManager;
/**
* 获取字典项
*/
@GetMapping("/items/{code}")
public Result<List<DictItem>> items(@PathVariable("code") String code, String keyword, HttpServletRequest request) {
DictLoader loader = dictManager.getLoader(code);
if (loader == null) {
return Result.ok(Collections.emptyList());
}
Map<String, String[]> parameterMap = request.getParameterMap();
Dict dict = loader.load(keyword, parameterMap);
if (dict == null) {
return Result.ok(Collections.emptyList());
}
return Result.ok(dict.getItems());
}
}

View File

@@ -0,0 +1,44 @@
package tech.easyflow.usercenter.controller.common;
import cloud.tianai.captcha.application.ImageCaptchaApplication;
import cloud.tianai.captcha.application.vo.ImageCaptchaVO;
import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
import cloud.tianai.captcha.common.response.ApiResponse;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import tech.easyflow.common.captcha.tainai.CaptchaData;
import javax.annotation.Resource;
/**
* 公共接口
*/
@RestController
@RequestMapping("/userCenter/public")
public class UcPublicController {
@Resource
private ImageCaptchaApplication application;
/**
* 获取验证码
*/
@RequestMapping(value = "/getCaptcha", produces = "application/json")
public ApiResponse<ImageCaptchaVO> getCaptcha() {
return application.generateCaptcha(CaptchaTypeConstant.SLIDER);
}
/**
* 验证码校验
*/
@PostMapping(value = "/check", produces = "application/json")
public ApiResponse<String> checkCaptcha(@RequestBody CaptchaData data) {
ApiResponse<?> response = application.matching(data.getId(), data.getData());
if (!response.isSuccess()) {
return ApiResponse.ofError("验证码错误");
}
return ApiResponse.ofSuccess(data.getId());
}
}

View File

@@ -0,0 +1,58 @@
package tech.easyflow.usercenter.controller.common;
import cn.dev33.satoken.annotation.SaIgnore;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.vo.UploadResVo;
import tech.easyflow.common.filestorage.FileStorageService;
import javax.annotation.Resource;
/**
* 文件上传
*/
@RestController
@RequestMapping("/userCenter/commons/")
public class UcUploadController {
@Resource(name = "default")
private FileStorageService storageService;
/**
* 上传
*/
@PostMapping(value = "/upload", produces = MediaType.APPLICATION_JSON_VALUE)
public Result<UploadResVo> upload(MultipartFile file) {
String path = storageService.save(file);
UploadResVo resVo = new UploadResVo();
resVo.setPath(path);
return Result.ok(resVo);
}
/**
* @ignore
*/
@PostMapping(value = "/uploadAntd", produces = MediaType.APPLICATION_JSON_VALUE)
public Result<UploadResVo> uploadAntd(MultipartFile file) {
String path = storageService.save(file);
UploadResVo resVo = new UploadResVo();
resVo.setPath(path);
return Result.ok(resVo);
}
/**
* @ignore
*/
@PostMapping(value = "/uploadPrePath",produces = MediaType.APPLICATION_JSON_VALUE)
@SaIgnore
public Result<UploadResVo> uploadPrePath(MultipartFile file, String prePath) {
String path = storageService.save(file,prePath);
UploadResVo resVo = new UploadResVo();
resVo.setPath(path);
return Result.ok(resVo);
}
}

View File

@@ -0,0 +1,91 @@
package tech.easyflow.usercenter.controller.system;
import cn.hutool.crypto.digest.BCrypt;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.common.web.jsonbody.JsonBody;
import tech.easyflow.system.entity.SysAccount;
import tech.easyflow.system.service.SysAccountService;
import javax.annotation.Resource;
import java.math.BigInteger;
import java.util.Date;
/**
* 用户
*
* @author ArkLight
* @since 2025-03-14
*/
@RestController
@RequestMapping("/userCenter/sysAccount")
public class UcSysAccountController {
@Resource
private SysAccountService service;
/**
* 获取用户的信息
*/
@GetMapping("/myProfile")
public Result<SysAccount> myProfile() {
LoginAccount account = SaTokenUtil.getLoginAccount();
SysAccount sysAccount = service.getById(account.getId());
return Result.ok(sysAccount);
}
/**
* 修改用户信息
*/
@PostMapping("/updateProfile")
public Result<Void> updateProfile(@JsonBody SysAccount account) {
BigInteger loginAccountId = SaTokenUtil.getLoginAccount().getId();
SysAccount update = new SysAccount();
update.setId(loginAccountId);
update.setNickname(account.getNickname());
update.setMobile(account.getMobile());
update.setEmail(account.getEmail());
update.setAvatar(account.getAvatar());
update.setModified(new Date());
update.setModifiedBy(loginAccountId);
service.updateById(update);
return Result.ok();
}
/**
* 修改密码
*
* @param password 用户的旧密码
* @param newPassword 新密码
* @param confirmPassword 确认密码
*/
@PostMapping("/updatePassword")
public Result<Void> updatePassword(@JsonBody(value = "password", required = true) String password,
@JsonBody(value = "newPassword", required = true) String newPassword,
@JsonBody(value = "confirmPassword", required = true) String confirmPassword) {
BigInteger loginAccountId = SaTokenUtil.getLoginAccount().getId();
SysAccount record = service.getById(loginAccountId);
if (record == null) {
return Result.fail("修改失败");
}
String pwdDb = record.getPassword();
if (!BCrypt.checkpw(password, pwdDb)) {
return Result.fail(1, "密码不正确");
}
if (!newPassword.equals(confirmPassword)) {
return Result.fail(2, "两次密码不一致");
}
SysAccount update = new SysAccount();
update.setId(loginAccountId);
update.setPassword(BCrypt.hashpw(newPassword));
update.setModified(new Date());
update.setModifiedBy(loginAccountId);
service.updateById(update);
return Result.ok();
}
}

View File

@@ -0,0 +1,36 @@
package tech.easyflow.usercenter.controller.system;
import cn.dev33.satoken.annotation.SaIgnore;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import tech.easyflow.common.annotation.UsePermission;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.web.controller.BaseCurdController;
import tech.easyflow.common.web.jsonbody.JsonBody;
import tech.easyflow.system.entity.SysUserFeedback;
import tech.easyflow.system.service.SysUserFeedbackService;
/**
* 控制层。
*
* @author 12076
* @since 2025-12-30
*/
@RestController
@RequestMapping("/userCenter/sysUserFeedback")
@UsePermission(moduleName = "/api/v1/sysUserFeedback")
public class UcUserFeedbackController extends BaseCurdController<SysUserFeedbackService, SysUserFeedback> {
@PostMapping("/save")
@SaIgnore
public Result<?> save(@JsonBody SysUserFeedback entity) {
return super.save(entity);
}
public UcUserFeedbackController(SysUserFeedbackService service) {
super(service);
}
}

22
easyflow-api/pom.xml Normal file
View File

@@ -0,0 +1,22 @@
<?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</artifactId>
<version>${revision}</version>
</parent>
<artifactId>easyflow-api</artifactId>
<packaging>pom</packaging>
<modules>
<module>easyflow-api-admin</module>
<module>easyflow-api-mcp</module>
<module>easyflow-api-public</module>
<module>easyflow-api-usercenter</module>
</modules>
</project>

418
easyflow-chat-protocol.md Normal file
View File

@@ -0,0 +1,418 @@
# EasyFlow Chat Protocol Specification v1.1
* **Protocol Name:** `easyflow-chat`
* **Version:** `1.1`
* **Status:** Draft / Recommended
* **Transport:** Server-Sent Events (SSE)
* **Encoding:** UTF-8
## 1. 设计背景与目标
本协议用于描述 **EasyFlow 对话系统中的服务端事件流通信规范**,支持:
* AI 对话的 **流式输出**
* 模型 **思考过程Thinking**
* **工具调用Tool Calling**
* **系统 / 业务错误**
* **工作流 / Agent 状态**
* **对话中的用户交互(表单、确认等)**
* **中断与恢复Suspend / Resume**
设计目标:
* 前后端解耦
* 协议长期可扩展
* 不绑定具体模型厂商
* 易于与 Workflow / Agent / Chain 架构集成
## 2. 传输层规范Transport
* 使用 HTTP + SSE支持未来扩展为其他协议比如 WebSocket 等)
* Response Header
```http
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
```
* 通信方向:**Server → Client**
* 所有业务数据通过 `data` 字段传输,格式为 **JSON 字符串**
## 3. SSE Event 级别规范
### 3.1 Event Name固定
| event | 含义 |
| - |-------|
| message | 正常业务事件 |
| error | 错误事件 |
| done | 流结束事件 |
> ⚠️ **禁止在 event name 中承载业务语义**
## 4. 统一 Envelope 结构(核心)
### 4.1 基本结构
```json
{
"protocol": "easyflow-chat",
"version": "1.1",
"domain": "llm | tool | system | business | workflow | interaction | debug",
"type": "string",
"conversation_id": "string",
"message_id": "string",
"index": 0,
"payload": {},
"meta": {}
}
```
### 4.2 字段说明
| 字段 | 类型 | 必填 | 说明 |
| -- |---------| -- |------------------------|
| protocol | string | ✔ | 固定值 `easyflow-chat` |
| version | string | ✔ | 协议版本 |
| domain | string | ✔ | 事件所属领域 |
| type | string | ✔ | 领域内事件类型 |
| conversation_id | string | ✔ | 会话唯一标识 |
| message_id | string | ✖ | assistant 消息 ID |
| index | number | ✖ | 流式输出序号 |
| payload | object | ✔ | 事件数据 |
| meta | object | ✖ | 元信息token、耗时等 |
## 5. Domain 定义
| Domain | 说明 |
| -- | -- |
| llm | 模型语义输出 |
| tool | 工具调用与结果 |
| system | 系统级事件 |
| business | 业务规则 |
| workflow | 工作流 / Agent 状态 |
| interaction | 用户交互(表单等) |
| debug | 调试信息 |
## 6. llm Domain
### 6.1 thinking
表示模型的思考过程。
#### 流式输出delta
```json
{
"domain": "llm",
"type": "thinking",
"payload": {
"delta": "分析用户需求"
}
}
```
#### 完整输出(可选)
```json
{
"domain": "llm",
"type": "message",
"payload": {
"content": "这是一个完整的回答"
}
}
```
### 6.2 message
#### 流式输出delta
```json
{
"domain": "llm",
"type": "message",
"index": 12,
"payload": {
"delta": "这是一个"
}
}
```
#### 完整输出(可选)
```json
{
"domain": "llm",
"type": "message",
"payload": {
"content": "这是一个完整的回答"
}
}
```
## 7. tool Domain
### 7.1 tool_call
```json
{
"domain": "tool",
"type": "tool_call",
"payload": {
"tool_call_id": "call_1",
"name": "search",
"arguments": {
"query": "SSE 协议设计"
}
}
}
```
### 7.2 tool_result
```json
{
"domain": "tool",
"type": "tool_result",
"payload": {
"tool_call_id": "call_1",
"status": "success | error",
"result": {}
}
}
```
## 8. system Domain
### 8.1 error
```json
{
"domain": "system",
"type": "error",
"payload": {
"code": "MODEL_CONFIG_INVALID",
"message": "模型配置错误",
"retryable": false,
"detail": {}
}
}
```
### 8.2 status
```json
{
"domain": "system",
"type": "status",
"payload": {
"state": "initializing | running | suspended | resumed"
}
}
```
## 9. business Domain
```json
{
"domain": "business",
"type": "error",
"payload": {
"code": "QUOTA_EXCEEDED",
"message": "配额不足"
}
}
```
## 10. workflow Domain
```json
{
"domain": "workflow",
"type": "status",
"payload": {
"node_id": "node_1",
"state": "start | suspend | resume | end",
"reason": "interaction"
}
}
```
## 11. interaction Domain对话内交互
### 11.1 form_request
表示请求用户填写表单,对话进入挂起状态。
```json
{
"domain": "interaction",
"type": "form_request",
"payload": {
"form_id": "user_info_form",
"title": "补充信息",
"description": "请填写以下信息以继续",
"schema": {
"type": "object",
"required": ["age", "email"],
"properties": {
"age": {
"type": "number",
"title": "年龄"
},
"email": {
"type": "string",
"title": "邮箱",
"format": "email"
}
}
},
"ui": {
"submit_text": "继续",
"cancel_text": "取消"
}
}
}
```
> 表单 schema **符合 JSON Schema 标准**
### 11.2 form_cancel
```json
{
"domain": "interaction",
"type": "form_cancel",
"payload": {
"form_id": "user_info_form"
}
}
```
## 12. 表单提交与恢复(非 SSE
表单提交通过 **普通 HTTP / WebSocket 请求**
```json
{
"conversation_id": "conv_1",
"form_id": "user_info_form",
"values": {
"age": 30,
"email": "a@b.com"
}
}
```
成功后服务端恢复 SSE 流。
## 13. done 事件(流结束)
```json
{
"domain": "system",
"type": "done",
"meta": {
"prompt_tokens": 1234,
"completion_tokens": 456,
"latency_ms": 2300
}
}
```
## 14. 错误处理规则
* 收到 `event: error` 后客户端应终止流
* 错误语义由:
```
domain + type + payload.code
```
共同决定
## 15. 状态机视角(推荐)
```text
RUNNING
LLM_OUTPUT
INTERACTION_REQUESTED
SUSPENDED
FORM_SUBMITTED
RESUMED
RUNNING
DONE
```
## 16. 扩展与兼容规则
1. 可新增 domain
2. 可新增 type
3. 不允许删除已有字段
4. payload 可自由扩展
5. 1.x 版本保持向后兼容
## 17. 设计原则
> * SSE 只负责事件流
> * domain 定义责任边界
> * type 定义语义动作
> * payload 定义数据结构
> * 前端不依赖 event name 判断业务,不依赖协议本身,支持其他协议的扩展

View File

@@ -0,0 +1,128 @@
<?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-commons</artifactId>
<version>${revision}</version>
</parent>
<name>easyflow-common-ai</name>
<artifactId>easyflow-common-ai</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.commonmark</groupId>
<artifactId>commonmark</artifactId>
</dependency>
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>com.github.jsqlparser</groupId>
<artifactId>jsqlparser</artifactId>
<version>4.9</version>
</dependency>
<dependency>
<groupId>com.easyagents</groupId>
<artifactId>easy-agents-bom</artifactId>
<exclusions>
<exclusion>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
</exclusion>
<exclusion>
<groupId>com.squareup.okhttp</groupId>
<artifactId>okhttp</artifactId>
</exclusion>
<exclusion>
<groupId>com.squareup.okio</groupId>
<artifactId>okio</artifactId>
</exclusion>
<exclusion>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
</exclusion>
<exclusion>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.poi</groupId>
<artifactId>ooxml-schemas</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
</dependency>
<dependency>
<groupId>tech.easyflow</groupId>
<artifactId>easyflow-common-base</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>tech.easyflow</groupId>
<artifactId>easyflow-common-options</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-http</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-json</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>cn.idev.excel</groupId>
<artifactId>fastexcel</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,55 @@
package tech.easyflow.common.ai;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
/**
* 聊天专用SSE发射器
*/
public class ChatSseEmitter extends SseEmitter {
// 默认超时时间30分钟
public static final long DEFAULT_TIMEOUT = 5 * 60 * 1000L;
public ChatSseEmitter() {
super(DEFAULT_TIMEOUT);
// 注册基础的超时/异常回调(避免连接泄漏)
registerBasicCallbacks();
}
public ChatSseEmitter(long timeout) {
super(timeout);
registerBasicCallbacks();
}
/**
* 基础回调:仅做简单的连接清理
*/
private void registerBasicCallbacks() {
// 超时回调
onTimeout(this::complete);
// 异常回调
onError(e -> complete());
}
public static ChatSseEmitter create() {
return new ChatSseEmitter();
}
/**
* 发送普通消息(最常用的核心方法)
* @param message 消息内容
* @throws IOException 发送失败时抛出IO异常
*/
public void sendMessage(String message) throws IOException {
// 封装标准的SSE消息格式
send(SseEmitter.event().name("message").data(message));
}
/**
* 快速创建发射器
*/
public ChatSseEmitter createChatSseEmitter() {
return new ChatSseEmitter();
}
}

View File

@@ -0,0 +1,37 @@
package tech.easyflow.common.ai.inteceptor;
import com.easyagents.core.model.chat.tool.GlobalToolInterceptors;
import com.easyagents.core.model.chat.tool.ToolChain;
import com.easyagents.core.model.chat.tool.ToolContext;
import com.easyagents.core.model.chat.tool.ToolInterceptor;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.Map;
@Component
public class ToolLoggingInterceptor implements ToolInterceptor {
@PostConstruct
public void init() {
GlobalToolInterceptors.addInterceptor(this);
}
@Override
public Object intercept(ToolContext context, ToolChain chain) throws Exception {
String toolName = context.getTool().getName();
Map<String, Object> args = context.getArgsMap();
System.out.println("▶ 调用工具: " + toolName + ", 参数: " + args);
long start = System.currentTimeMillis();
try {
Object result = chain.proceed(context);
System.out.println("✅ 工具返回: " + result);
return result;
} finally {
long duration = System.currentTimeMillis() - start;
System.out.println("⏱️ 耗时: " + duration + "ms");
}
}
}

View File

@@ -0,0 +1,49 @@
package tech.easyflow.common.ai.plugin;
import java.util.*;
public class NestedParamConverter {
public static Map<String, Object> convertToNestedParamMap(List<PluginParam> pluginParams) {
Map<String, Object> result = new LinkedHashMap<>();
if (pluginParams == null || pluginParams.isEmpty()) return result;
for (PluginParam param : pluginParams) {
if (!param.isEnabled()) continue;
result.put(param.getName(), buildValue(param));
}
return result;
}
private static Object buildValue(PluginParam param) {
if ("String".equalsIgnoreCase(param.getType())) {
return param.getDefaultValue();
} else if ("Object".equalsIgnoreCase(param.getType())) {
Map<String, Object> objMap = new LinkedHashMap<>();
if (param.getChildren() != null && !param.getChildren().isEmpty()) {
for (PluginParam child : param.getChildren()) {
objMap.put(child.getName(), buildValue(child));
}
}
return objMap;
} else if ("Array".equalsIgnoreCase(param.getType())) {
if (param.getChildren() != null && !param.getChildren().isEmpty()) {
PluginParam arrayItemTemplate = param.getChildren().get(0);
if ("Array[Object]".equalsIgnoreCase(arrayItemTemplate.getType())) {
List<Map<String, Object>> list = new ArrayList<>();
Map<String, Object> item = new LinkedHashMap<>();
for (PluginParam child : arrayItemTemplate.getChildren()) {
item.put(child.getName(), buildValue(child));
}
list.add(item); // 示例中只添加一个对象
return list;
}
}
return Collections.EMPTY_LIST;
}
return null;
}
}

View File

@@ -0,0 +1,182 @@
package tech.easyflow.common.ai.plugin;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.http.*;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import org.springframework.web.multipart.MultipartFile;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
public class PluginHttpClient {
private static final int TIMEOUT = 10_000;
public static JSONObject sendRequest(String url, String method,
Map<String, Object> headers,
List<PluginParam> pluginParams) {
// 1. 处理路径参数
String processedUrl = replacePathVariables(url, pluginParams);
// 2. 初始化请求
Method httpMethod = Method.valueOf(method.toUpperCase());
HttpRequest request = HttpRequest.of(processedUrl)
.method(httpMethod)
.timeout(TIMEOUT);
// 3. 处理请求头(合并默认头和参数头)
processHeaders(request, headers, pluginParams);
// 4. 处理查询参数和请求体
processQueryAndBodyParams(request, httpMethod, pluginParams);
// 5. 执行请求
HttpResponse response = request.execute();
return JSONUtil.parseObj(response.body());
}
/**
* 处理请求头(合并默认头和参数头)
*/
private static void processHeaders(HttpRequest request,
Map<String, Object> defaultHeaders,
List<PluginParam> params) {
// 添加默认头
if (ObjectUtil.isNotEmpty(defaultHeaders)) {
defaultHeaders.forEach((k, v) -> request.header(k, v.toString()));
}
// 添加参数中指定的头
params.stream()
.filter(p -> "header".equalsIgnoreCase(p.getMethod()) && p.isEnabled())
.forEach(p -> request.header(p.getName(), p.getDefaultValue().toString()));
}
/**
* 处理查询参数和请求体
*/
/**
* 处理查询参数和请求体(新增文件参数支持)
*/
private static void processQueryAndBodyParams(HttpRequest request,
Method httpMethod,
List<PluginParam> params) {
Map<String, Object> queryParams = new HashMap<>();
Map<String, Object> bodyParams = new HashMap<>();
// 标记是否包含文件参数
AtomicBoolean hasMultipartFile = new AtomicBoolean(false);
// 分类参数(同时检测是否有文件)
params.stream()
.filter(PluginParam::isEnabled)
.forEach(p -> {
String methodType = p.getMethod().toLowerCase();
Object paramValue = buildNestedParamValue(p);
// 检测是否为文件参数MultipartFile 类型)
if (paramValue instanceof org.springframework.web.multipart.MultipartFile) {
hasMultipartFile.set(true);
}
switch (methodType) {
case "query":
queryParams.put(p.getName(), paramValue);
break;
case "body":
bodyParams.put(p.getName(), paramValue);
break;
}
});
// 1. 设置查询参数(原有逻辑不变)
if (!queryParams.isEmpty()) {
request.form(queryParams);
}
// 2. 设置请求体(分两种情况:有文件 vs 无文件)
if (!bodyParams.isEmpty() && (httpMethod == Method.POST || httpMethod == Method.PUT)) {
if (hasMultipartFile.get()) {
// 2.1 包含文件参数 → 用 multipart/form-data 格式
processMultipartBody(request, bodyParams);
} else {
// 2.2 无文件参数 → 保持原有 JSON 格式
request.body(JSONUtil.toJsonStr(bodyParams))
.header(Header.CONTENT_TYPE, ContentType.JSON.getValue());
}
}
}
/**
* 递归构建嵌套参数值
* @param param 当前参数
* @return 如果是 Object 类型,返回 Map否则返回 defaultValue
*/
private static Object buildNestedParamValue(PluginParam param) {
// 如果不是 Object 类型,直接返回默认值
if (!"Object".equalsIgnoreCase(param.getType())) {
return param.getDefaultValue();
}
// 如果是 Object 类型,递归处理子参数
Map<String, Object> nestedParams = new HashMap<>();
if (param.getChildren() != null) {
param.getChildren().stream()
.filter(PluginParam::isEnabled)
.forEach(child -> {
Object childValue = buildNestedParamValue(child); // 递归处理子参数
nestedParams.put(child.getName(), childValue);
});
}
return nestedParams;
}
/**
* 替换URL中的路径变量 {xxx}
*/
private static String replacePathVariables(String url, List<PluginParam> params) {
String result = url;
// 收集路径参数
Map<String, Object> pathParams = new HashMap<>();
params.stream()
.filter(p -> "path".equalsIgnoreCase(p.getMethod()) && p.isEnabled())
.forEach(p -> pathParams.put(p.getName(), p.getDefaultValue()));
// 替换变量
for (Map.Entry<String, Object> entry : pathParams.entrySet()) {
result = result.replaceAll("\\{" + entry.getKey() + "\\}",
entry.getValue().toString());
}
return result;
}
private static void processMultipartBody(HttpRequest request, Map<String, Object> bodyParams) {
// 手动设置 Content-Type 为 multipart/form-data
request.header(Header.CONTENT_TYPE, "multipart/form-data");
for (Map.Entry<String, Object> entry : bodyParams.entrySet()) {
String paramName = entry.getKey();
Object paramValue = entry.getValue();
if (paramValue instanceof MultipartFile) {
MultipartFile file = (MultipartFile) paramValue;
try {
request.form(paramName, file.getBytes(), file.getOriginalFilename());
} catch (Exception e) {
throw new RuntimeException(String.format("文件参数处理失败:参数名=%s文件名=%s",
paramName, file.getOriginalFilename()), e);
}
} else {
// 处理普通参数
String valueStr;
if (paramValue instanceof String || paramValue instanceof Number || paramValue instanceof Boolean) {
valueStr = paramValue.toString();
} else {
valueStr = JSONUtil.toJsonStr(paramValue);
}
request.form(paramName, valueStr);
}
}
}
}

View File

@@ -0,0 +1,86 @@
package tech.easyflow.common.ai.plugin;
import java.util.List;
public class PluginParam {
private String name;
private String description;
private String type;
private String method; // Query / Body / Header / PathVariable 等
private Object defaultValue;
private boolean required;
private boolean enabled;
private String key;
private List<PluginParam> children;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getMethod() {
return method;
}
public void setMethod(String method) {
this.method = method;
}
public Object getDefaultValue() {
return defaultValue;
}
public void setDefaultValue(Object defaultValue) {
this.defaultValue = defaultValue;
}
public boolean isRequired() {
return required;
}
public void setRequired(boolean required) {
this.required = required;
}
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public List<PluginParam> getChildren() {
return children;
}
public void setChildren(List<PluginParam> children) {
this.children = children;
}
}

View File

@@ -0,0 +1,79 @@
package tech.easyflow.common.ai.plugin;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class PluginParamConverter {
private static final ObjectMapper objectMapper = new ObjectMapper();
/**
* 将JSON字符串转换为PluginParam对象列表
* @param jsonStr 数据库中的JSON字符串
* @return PluginParam对象列表
*/
public static List<PluginParam> convertFromJson(String jsonStr) {
try {
// 将JSON字符串解析为List<Map>结构
List<Map<String, Object>> paramMaps = objectMapper.readValue(
jsonStr,
new TypeReference<List<Map<String, Object>>>(){}
);
List<PluginParam> result = new ArrayList<>();
for (Map<String, Object> paramMap : paramMaps) {
result.add(convertMapToPluginParam(paramMap));
}
return result;
} catch (Exception e) {
throw new RuntimeException("Failed to convert JSON to PluginParam", e);
}
}
/**
* 递归将Map转换为PluginParam对象
*/
private static PluginParam convertMapToPluginParam(Map<String, Object> map) {
PluginParam param = new PluginParam();
param.setKey(getStringValue(map, "key"));
param.setName(getStringValue(map, "name"));
param.setDescription(getStringValue(map, "description"));
param.setType(getStringValue(map, "type"));
param.setMethod(getStringValue(map, "method"));
param.setDefaultValue(map.get("defaultValue"));
param.setRequired(getBooleanValue(map, "required"));
param.setEnabled(getBooleanValue(map, "enabled"));
// 处理子节点
if (map.containsKey("children")) {
Object childrenObj = map.get("children");
if (childrenObj instanceof List) {
List<Map<String, Object>> childrenMaps = (List<Map<String, Object>>) childrenObj;
List<PluginParam> children = new ArrayList<>();
for (Map<String, Object> childMap : childrenMaps) {
children.add(convertMapToPluginParam(childMap));
}
param.setChildren(children);
}
}
return param;
}
private static String getStringValue(Map<String, Object> map, String key) {
Object value = map.get(key);
return value != null ? value.toString() : null;
}
private static boolean getBooleanValue(Map<String, Object> map, String key) {
Object value = map.get(key);
if (value instanceof Boolean) {
return (Boolean) value;
}
return false;
}
}

View File

@@ -0,0 +1,103 @@
package tech.easyflow.common.ai.rag;
import com.easyagents.core.document.Document;
import com.easyagents.core.document.DocumentSplitter;
import com.easyagents.core.document.id.DocumentIdGenerator;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.TypeReference;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class ExcelDocumentSplitter implements DocumentSplitter {
private int rowsPerChunk;
private boolean includeHeader;
public ExcelDocumentSplitter(int rowsPerChunk) {
if (rowsPerChunk <= 0) {
throw new IllegalArgumentException("rows must be greater than 0");
}
this.rowsPerChunk = rowsPerChunk;
}
public ExcelDocumentSplitter(int rowsPerChunk, boolean includeHeader) {
if (rowsPerChunk <= 0) {
throw new IllegalArgumentException("rows must be greater than 0");
}
this.rowsPerChunk = rowsPerChunk;
this.includeHeader = includeHeader;
}
@Override
public List<Document> split(Document document, DocumentIdGenerator idGenerator) {
if (document == null || document.getContent() == null) {
return Collections.emptyList();
}
// 解析JSON数据为表格结构
List<List<String>> tableData = JSON.parseObject(document.getContent(),
new TypeReference<List<List<String>>>() {});
if (tableData == null || tableData.isEmpty()) {
return Collections.emptyList();
}
List<Document> chunks = new ArrayList<>();
List<String> headers = includeHeader ? tableData.get(0) : null;
int startRow = includeHeader ? 1 : 0;
// 按照指定行数分割数据
for (int i = startRow; i < tableData.size(); i += rowsPerChunk) {
int endRow = Math.min(i + rowsPerChunk, tableData.size());
// 构建当前分块的Markdown表格
StringBuilder sb = new StringBuilder();
// 添加表头(如果包含)
if (headers != null) {
sb.append("| ").append(String.join(" | ", headers)).append(" |\n");
sb.append("|");
for (int j = 0; j < headers.size(); j++) {
sb.append(" --- |");
}
sb.append("\n");
}
// 添加数据行
for (int j = i; j < endRow; j++) {
List<String> row = tableData.get(j);
sb.append("| ").append(String.join(" | ", row)).append(" |\n");
}
// 创建新文档
Document newDocument = new Document();
newDocument.addMetadata(document.getMetadataMap());
newDocument.setContent(sb.toString());
if (idGenerator != null) {
newDocument.setId(idGenerator.generateId(newDocument));
}
chunks.add(newDocument);
}
return chunks;
}
public int getRowsPerChunk() {
return rowsPerChunk;
}
public void setRowsPerChunk(int rowsPerChunk) {
this.rowsPerChunk = rowsPerChunk;
}
public boolean isIncludeHeader() {
return includeHeader;
}
public void setIncludeHeader(boolean includeHeader) {
this.includeHeader = includeHeader;
}
}

View File

@@ -0,0 +1,44 @@
<?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-commons</artifactId>
<version>${revision}</version>
</parent>
<name>easyflow-common-all</name>
<artifactId>easyflow-common-all</artifactId>
<dependencies>
<dependency>
<groupId>tech.easyflow</groupId>
<artifactId>easyflow-common-ai</artifactId>
</dependency>
<dependency>
<groupId>tech.easyflow</groupId>
<artifactId>easyflow-common-base</artifactId>
</dependency>
<dependency>
<groupId>tech.easyflow</groupId>
<artifactId>easyflow-common-cache</artifactId>
</dependency>
<dependency>
<groupId>tech.easyflow</groupId>
<artifactId>easyflow-common-file-storage</artifactId>
</dependency>
<dependency>
<groupId>tech.easyflow</groupId>
<artifactId>easyflow-common-options</artifactId>
</dependency>
<dependency>
<groupId>tech.easyflow</groupId>
<artifactId>easyflow-common-captcha</artifactId>
</dependency>
<dependency>
<groupId>tech.easyflow</groupId>
<artifactId>easyflow-common-web</artifactId>
</dependency>
</dependencies>
</project>

Some files were not shown because too many files have changed in this diff Show More