Compare commits
81 Commits
99f792f6de
...
v1.0
| Author | SHA1 | Date | |
|---|---|---|---|
| cb379e071c | |||
| 8b80770960 | |||
| c316eff5be | |||
| 1ea863cb2c | |||
| 0f4d10c43c | |||
| cc3bb9cff0 | |||
| e39f7521e2 | |||
| 1c205c3720 | |||
| 11e595b088 | |||
| 72df00f25b | |||
| 6c3d98eaac | |||
| b7f3ae2854 | |||
| 2907acac95 | |||
| 0947009ee6 | |||
| a186066641 | |||
| 1a6ea64e80 | |||
| da58077d59 | |||
| 47c2bad839 | |||
| 2ad8935a61 | |||
| 21b1bc82f6 | |||
| 4a15124183 | |||
| e27834ee0c | |||
| c1590b0d8a | |||
| ff863e3c27 | |||
| 516d43ce7d | |||
| 8d07b306e5 | |||
| ba70fec9a5 | |||
| 31b0e21d3d | |||
| 5827ecde42 | |||
| 1d8b9d9662 | |||
| a5aab86de2 | |||
| 51198ff492 | |||
| 9feb889637 | |||
| 8546d927bc | |||
| 4130381658 | |||
| ad67ba85ad | |||
| 2689adfa40 | |||
| a41b50959e | |||
| 855e93ecbf | |||
| ae10383f17 | |||
| 31a755a8bc | |||
| 8cfe5400fe | |||
| 47655a728b | |||
| 6da90e2296 | |||
| cfbeaf11fe | |||
| 4e565aef99 | |||
| 81125ce55c | |||
| 3f128e977a | |||
| 7e7c236c2a | |||
| bb72e19c84 | |||
| b5dd427920 | |||
| 2592a1f09d | |||
| a402d920be | |||
| 58d6fb4e63 | |||
| 029f290c1b | |||
| acbf087bf3 | |||
| a4f75a5e4c | |||
| 25e80433a5 | |||
| 1ecc28e498 | |||
| 798effbd5b | |||
| b6213d0933 | |||
| 22ceabff96 | |||
| f49d94e2fe | |||
| aaf4c61ff8 | |||
| b777cb3641 | |||
| c78db961c5 | |||
| da536ea742 | |||
| 799174406e | |||
| 6e1bd73cd8 | |||
| d510034abb | |||
| b1a16ccf18 | |||
| 07d8193e80 | |||
| 39a6daf8fe | |||
| cff4fe8da9 | |||
| 5d3c7d8692 | |||
| 14c78d54f5 | |||
| b5ba6912eb | |||
| cac0fdf858 | |||
| 373d7f8201 | |||
| 219fa566ef | |||
| 0a8a7c8046 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -42,6 +42,7 @@ build/
|
|||||||
.jlsp/
|
.jlsp/
|
||||||
.arts/
|
.arts/
|
||||||
luceneKnowledge
|
luceneKnowledge
|
||||||
|
**/*.lic
|
||||||
|
|
||||||
# v1
|
# v1
|
||||||
/easyflow-ui-react
|
/easyflow-ui-react
|
||||||
45
Dockerfile
45
Dockerfile
@@ -1,25 +1,56 @@
|
|||||||
FROM eclipse-temurin:17-jre
|
# 后端构建脚本
|
||||||
|
FROM --platform=linux/amd64 swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/eclipse-temurin:17-jre
|
||||||
|
|
||||||
ENV LANG=C.UTF-8
|
ENV LANG=C.UTF-8
|
||||||
ENV LC_ALL=C.UTF-8
|
ENV LC_ALL=C.UTF-8
|
||||||
ENV TZ=Asia/Shanghai
|
ENV TZ=Asia/Shanghai
|
||||||
ENV JAVA_OPTS=""
|
ENV JAVA_OPTS=""
|
||||||
ENV EASYFLOW_JAR_PATH=/app/easyflow.jar
|
ENV EASYFLOW_JAR_PATH=/app/artifacts/easyflow.jar
|
||||||
ENV EASYFLOW_CONFIG_PATH=file:/app/application.yml
|
ENV EASYFLOW_CONFIG_PATH=file:/app/application.yml
|
||||||
ENV EASYFLOW_LOG_FILE=/app/logs/app.log
|
ENV EASYFLOW_LOG_FILE=/app/logs/app.log
|
||||||
|
ENV EASYFLOW_JAR_RESTART_GRACE_SECONDS=30
|
||||||
|
ENV NPM_CONFIG_REGISTRY=https://registry.npmmirror.com
|
||||||
|
ENV PIP_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple
|
||||||
|
ENV PIP_TRUSTED_HOST=pypi.tuna.tsinghua.edu.cn
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN useradd --system --create-home easyflow && \
|
RUN useradd --system --create-home easyflow && \
|
||||||
apt-get update && \
|
apt-get update && \
|
||||||
apt-get install -y --no-install-recommends python3 && \
|
apt-get install -y --no-install-recommends \
|
||||||
|
ca-certificates \
|
||||||
|
curl \
|
||||||
|
gnupg && \
|
||||||
|
mkdir -p /etc/apt/keyrings && \
|
||||||
|
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key -o /tmp/nodesource.gpg.key && \
|
||||||
|
gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg /tmp/nodesource.gpg.key && \
|
||||||
|
chmod 644 /etc/apt/keyrings/nodesource.gpg && \
|
||||||
|
printf "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_24.x nodistro main\n" > /etc/apt/sources.list.d/nodesource.list && \
|
||||||
|
rm -f /tmp/nodesource.gpg.key && \
|
||||||
|
apt-get update && \
|
||||||
|
apt-get install -y --no-install-recommends \
|
||||||
|
inotify-tools \
|
||||||
|
nodejs \
|
||||||
|
python3 \
|
||||||
|
python3-pip \
|
||||||
|
python3-venv \
|
||||||
|
tini && \
|
||||||
|
ln -sf /usr/bin/python3 /usr/local/bin/python && \
|
||||||
|
ln -sf /usr/bin/pip3 /usr/local/bin/pip && \
|
||||||
|
npm config set registry "${NPM_CONFIG_REGISTRY}" && \
|
||||||
|
printf "registry=%s\n" "${NPM_CONFIG_REGISTRY}" > /etc/npmrc && \
|
||||||
|
npm install -g pnpm@10.17.1 && \
|
||||||
|
pnpm config set registry "${NPM_CONFIG_REGISTRY}" && \
|
||||||
|
mkdir -p /etc/pip && \
|
||||||
|
printf "[global]\nindex-url = %s\ntrusted-host = %s\n" "${PIP_INDEX_URL}" "${PIP_TRUSTED_HOST}" > /etc/pip.conf && \
|
||||||
rm -rf /var/lib/apt/lists/* && \
|
rm -rf /var/lib/apt/lists/* && \
|
||||||
mkdir -p /app/logs && \
|
mkdir -p /app/logs /app/artifacts /app/data && \
|
||||||
chown -R easyflow:easyflow /app
|
chown -R easyflow:easyflow /app
|
||||||
|
|
||||||
USER easyflow
|
COPY docker-entrypoint.sh /usr/local/bin/easyflow-entrypoint.sh
|
||||||
|
RUN chmod 755 /usr/local/bin/easyflow-entrypoint.sh
|
||||||
|
|
||||||
VOLUME ["/app/logs"]
|
VOLUME ["/app/logs", "/app/data"]
|
||||||
EXPOSE 8111
|
EXPOSE 8111
|
||||||
|
|
||||||
ENTRYPOINT ["sh", "-c", "if [ ! -f \"${EASYFLOW_JAR_PATH}\" ]; then echo \"ERROR: easyflow jar not found: ${EASYFLOW_JAR_PATH}\"; exit 1; fi; java ${JAVA_OPTS} -Djava.security.egd=file:/dev/./urandom -jar \"${EASYFLOW_JAR_PATH}\" --spring.config.location=\"${EASYFLOW_CONFIG_PATH}\" --logging.file.name=\"${EASYFLOW_LOG_FILE}\""]
|
ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/easyflow-entrypoint.sh"]
|
||||||
|
|||||||
17
README.md
17
README.md
@@ -28,9 +28,14 @@ EasyFlow 是一个面向企业场景的 Java AI 应用开发平台,提供智
|
|||||||
## 快速启动(开发环境)
|
## 快速启动(开发环境)
|
||||||
|
|
||||||
### 1. 初始化数据库
|
### 1. 初始化数据库
|
||||||
在 MySQL 中导入:
|
只需要提前创建空库,例如:
|
||||||
- `sql/01-easyflow-v2.ddl.sql`
|
|
||||||
- `sql/02-easyflow-v2.data.sql`
|
```sql
|
||||||
|
CREATE DATABASE easyflow CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||||
|
```
|
||||||
|
|
||||||
|
表结构、Quartz 表和基础数据会在后端启动时由 Flyway 自动迁移完成。
|
||||||
|
当前初始化脚本仅保留 3 段:`V1__quartz.sql`、`V2__easyflow_schema.sql`、`V3__easyflow_seed.sql`。
|
||||||
|
|
||||||
### 2. 启动后端
|
### 2. 启动后端
|
||||||
在项目根目录执行:
|
在项目根目录执行:
|
||||||
@@ -42,6 +47,8 @@ java -jar easyflow-starter/easyflow-starter-all/target/easyflow-starter-all-0.0.
|
|||||||
|
|
||||||
默认端口:`8111`(见 `easyflow-starter/easyflow-starter-all/src/main/resources/application.yml`)。
|
默认端口:`8111`(见 `easyflow-starter/easyflow-starter-all/src/main/resources/application.yml`)。
|
||||||
|
|
||||||
|
首次连接空库时,应用会自动执行 `db/migration` 下的迁移脚本并初始化默认管理员、菜单、角色和模型服务商数据。
|
||||||
|
|
||||||
### 3. 启动前端
|
### 3. 启动前端
|
||||||
管理后台:
|
管理后台:
|
||||||
|
|
||||||
@@ -59,7 +66,7 @@ pnpm install
|
|||||||
pnpm dev
|
pnpm dev
|
||||||
```
|
```
|
||||||
|
|
||||||
默认测试账号:`admin / 123456`
|
默认测试账号:`admin / Easy@2026`
|
||||||
|
|
||||||
## 后端 Jar 包构建与部署
|
## 后端 Jar 包构建与部署
|
||||||
|
|
||||||
@@ -78,6 +85,8 @@ mvn -DskipTests -Dmaven.javadoc.skip=true clean package
|
|||||||
java -jar easyflow-starter/easyflow-starter-all/target/easyflow-starter-all-0.0.1.jar --spring.profiles.active=prod
|
java -jar easyflow-starter/easyflow-starter-all/target/easyflow-starter-all-0.0.1.jar --spring.profiles.active=prod
|
||||||
```
|
```
|
||||||
|
|
||||||
|
生产环境同样只需要保证目标数据库已创建为空库,Flyway 会在应用启动时自动完成迁移。
|
||||||
|
|
||||||
可通过环境变量覆盖关键配置(示例):
|
可通过环境变量覆盖关键配置(示例):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
61
config/proguard/common-keep.pro
Normal file
61
config/proguard/common-keep.pro
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
-dontshrink
|
||||||
|
-dontoptimize
|
||||||
|
-dontpreverify
|
||||||
|
-ignorewarnings
|
||||||
|
-dontnote
|
||||||
|
|
||||||
|
-libraryjars <java.home>/jmods/java.base.jmod(!**.jar;!module-info.class)
|
||||||
|
-libraryjars <java.home>/jmods/java.compiler.jmod(!**.jar;!module-info.class)
|
||||||
|
-libraryjars <java.home>/jmods/java.datatransfer.jmod(!**.jar;!module-info.class)
|
||||||
|
-libraryjars <java.home>/jmods/java.desktop.jmod(!**.jar;!module-info.class)
|
||||||
|
-libraryjars <java.home>/jmods/java.instrument.jmod(!**.jar;!module-info.class)
|
||||||
|
-libraryjars <java.home>/jmods/java.logging.jmod(!**.jar;!module-info.class)
|
||||||
|
-libraryjars <java.home>/jmods/java.management.jmod(!**.jar;!module-info.class)
|
||||||
|
-libraryjars <java.home>/jmods/java.naming.jmod(!**.jar;!module-info.class)
|
||||||
|
-libraryjars <java.home>/jmods/java.net.http.jmod(!**.jar;!module-info.class)
|
||||||
|
-libraryjars <java.home>/jmods/java.prefs.jmod(!**.jar;!module-info.class)
|
||||||
|
-libraryjars <java.home>/jmods/java.rmi.jmod(!**.jar;!module-info.class)
|
||||||
|
-libraryjars <java.home>/jmods/java.scripting.jmod(!**.jar;!module-info.class)
|
||||||
|
-libraryjars <java.home>/jmods/java.security.jgss.jmod(!**.jar;!module-info.class)
|
||||||
|
-libraryjars <java.home>/jmods/java.security.sasl.jmod(!**.jar;!module-info.class)
|
||||||
|
-libraryjars <java.home>/jmods/java.sql.jmod(!**.jar;!module-info.class)
|
||||||
|
-libraryjars <java.home>/jmods/java.transaction.xa.jmod(!**.jar;!module-info.class)
|
||||||
|
-libraryjars <java.home>/jmods/java.xml.jmod(!**.jar;!module-info.class)
|
||||||
|
-libraryjars <java.home>/jmods/java.xml.crypto.jmod(!**.jar;!module-info.class)
|
||||||
|
|
||||||
|
-keepattributes RuntimeVisibleAnnotations,RuntimeInvisibleAnnotations,RuntimeVisibleParameterAnnotations,RuntimeInvisibleParameterAnnotations,AnnotationDefault,Signature,InnerClasses,EnclosingMethod,Record,SourceFile,LineNumberTable,MethodParameters
|
||||||
|
|
||||||
|
-keep @org.springframework.stereotype.Controller class * { *; }
|
||||||
|
-keep @org.springframework.web.bind.annotation.RestController class * { *; }
|
||||||
|
-keep @org.springframework.context.annotation.Configuration class * { *; }
|
||||||
|
-keep @org.springframework.boot.context.properties.ConfigurationProperties class * { *; }
|
||||||
|
-keep @org.springframework.boot.autoconfigure.SpringBootApplication class * { *; }
|
||||||
|
|
||||||
|
-keep class **.*Controller { *; }
|
||||||
|
-keep class **.*Mapper { *; }
|
||||||
|
-keep class **.mapper.** { *; }
|
||||||
|
-keep class **.entity.** { *; }
|
||||||
|
-keep class **.dto.** { *; }
|
||||||
|
-keep class **.vo.** { *; }
|
||||||
|
-keep class **.model.** { *; }
|
||||||
|
-keep class **.config.** { *; }
|
||||||
|
-keep class **.enums.** { *; }
|
||||||
|
-keep class **.annotation.** { *; }
|
||||||
|
-keep class **.*Exception { *; }
|
||||||
|
-keep class **.*ErrorCode { *; }
|
||||||
|
-keep class **.*Properties { *; }
|
||||||
|
-keep class **.*Config { *; }
|
||||||
|
-keep class **.*Configuration { *; }
|
||||||
|
-keep interface tech.easyflow.** { *; }
|
||||||
|
-keep enum tech.easyflow.** { *; }
|
||||||
|
|
||||||
|
-keepclassmembers class * {
|
||||||
|
@jakarta.annotation.Resource <fields>;
|
||||||
|
@org.springframework.beans.factory.annotation.Autowired <fields>;
|
||||||
|
@org.springframework.beans.factory.annotation.Value <fields>;
|
||||||
|
@org.springframework.context.annotation.Bean <methods>;
|
||||||
|
}
|
||||||
|
|
||||||
|
-keepclassmembers class * {
|
||||||
|
public <init>(...);
|
||||||
|
}
|
||||||
28
config/proguard/easyflow-module-ai.pro
Normal file
28
config/proguard/easyflow-module-ai.pro
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
-include ../../config/proguard/common-keep.pro
|
||||||
|
|
||||||
|
-keep class tech.easyflow.ai.chattime.** { *; }
|
||||||
|
-keep class tech.easyflow.ai.constants.** { *; }
|
||||||
|
-keep class tech.easyflow.ai.document.** { *; }
|
||||||
|
-keep class tech.easyflow.ai.documentimport.** { *; }
|
||||||
|
-keep class tech.easyflow.ai.easyagents.** { *; }
|
||||||
|
-keep class tech.easyflow.ai.exception.** { *; }
|
||||||
|
-keep class tech.easyflow.ai.mcp.** { *; }
|
||||||
|
-keep class tech.easyflow.ai.node.** { *; }
|
||||||
|
-keep class tech.easyflow.ai.permission.** { *; }
|
||||||
|
-keep class tech.easyflow.ai.plugin.** { *; }
|
||||||
|
-keep class tech.easyflow.ai.publish.** { *; }
|
||||||
|
-keep class tech.easyflow.ai.rag.** { *; }
|
||||||
|
-keep class tech.easyflow.ai.service.** { *; }
|
||||||
|
-keep class tech.easyflow.ai.support.** { *; }
|
||||||
|
-keep class tech.easyflow.ai.utils.** { *; }
|
||||||
|
-keep class tech.easyflow.ai.invoke.service.** { *; }
|
||||||
|
-keep class tech.easyflow.ai.invoke.model.** { *; }
|
||||||
|
-keep class tech.easyflow.ai.invoke.protocol.** { *; }
|
||||||
|
-keep class tech.easyflow.ai.invoke.exception.** { *; }
|
||||||
|
-keep class tech.easyflow.ai.invoke.mapper.OpenAiProtocolMapper { *; }
|
||||||
|
-keep class tech.easyflow.ai.invoke.provider.ModelProviderGateway { *; }
|
||||||
|
-keep class tech.easyflow.ai.invoke.provider.UnifiedChatChunkObserver { *; }
|
||||||
|
-keep class tech.easyflow.ai.easyagentsflow.config.** { *; }
|
||||||
|
-keep class tech.easyflow.ai.easyagentsflow.entity.** { *; }
|
||||||
|
-keep class tech.easyflow.ai.easyagentsflow.service.** { *; }
|
||||||
|
-keep class tech.easyflow.ai.easyagentsflow.support.** { *; }
|
||||||
5
config/proguard/easyflow-module-autoconfig.pro
Normal file
5
config/proguard/easyflow-module-autoconfig.pro
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
-include ../../config/proguard/common-keep.pro
|
||||||
|
|
||||||
|
-keep class tech.easyflow.autoconfig.license.EasyflowLicenseBootstrapValidator { *; }
|
||||||
|
-keep class tech.easyflow.autoconfig.license.EasyflowLicenseProperties { *; }
|
||||||
|
-keep class tech.easyflow.autoconfig.license.EasyflowLicenseVerificationResult { *; }
|
||||||
10
config/proguard/easyflow-module-datacenter.pro
Normal file
10
config/proguard/easyflow-module-datacenter.pro
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
-include ../../config/proguard/common-keep.pro
|
||||||
|
|
||||||
|
-keep class tech.easyflow.datacenter.connector.DatacenterConnector { *; }
|
||||||
|
-keep class tech.easyflow.datacenter.connector.QueryExecutor { *; }
|
||||||
|
-keep class tech.easyflow.datacenter.connector.WriteExecutor { *; }
|
||||||
|
-keep class tech.easyflow.datacenter.connector.MetadataExplorer { *; }
|
||||||
|
-keep class tech.easyflow.datacenter.connector.SourceHealthChecker { *; }
|
||||||
|
-keep class tech.easyflow.datacenter.connector.SqlDialect { *; }
|
||||||
|
-keep class tech.easyflow.datacenter.execution.model.** { *; }
|
||||||
|
-keep class tech.easyflow.datacenter.meta.enums.** { *; }
|
||||||
@@ -13,18 +13,17 @@ services:
|
|||||||
- --max_connections=500
|
- --max_connections=500
|
||||||
- --sql_mode=STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION
|
- --sql_mode=STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION
|
||||||
environment:
|
environment:
|
||||||
TZ: ${TZ:-Asia/Shanghai}
|
TZ: Asia/Shanghai
|
||||||
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-root}
|
MYSQL_ROOT_PASSWORD: root
|
||||||
MYSQL_DATABASE: ${MYSQL_DATABASE:-easyflow}
|
MYSQL_DATABASE: easyflow
|
||||||
MYSQL_USER: ${MYSQL_USER:-easyflow}
|
MYSQL_USER: easyflow
|
||||||
MYSQL_PASSWORD: ${MYSQL_PASSWORD:-123456}
|
MYSQL_PASSWORD: "123456"
|
||||||
ports:
|
ports:
|
||||||
- "${MYSQL_PORT:-3306}:3306"
|
- "3306:3306"
|
||||||
volumes:
|
volumes:
|
||||||
- ./data/mysql:/var/lib/mysql
|
- ./data/mysql:/var/lib/mysql
|
||||||
- ./easyflow/sql/initdb:/docker-entrypoint-initdb.d:ro
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-p${MYSQL_ROOT_PASSWORD:-root}"]
|
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-proot"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 10
|
retries: 10
|
||||||
@@ -34,13 +33,13 @@ services:
|
|||||||
image: swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/kubesphere/redis:7.2.4-alpine-linuxarm64
|
image: swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/kubesphere/redis:7.2.4-alpine-linuxarm64
|
||||||
container_name: easyflow-redis
|
container_name: easyflow-redis
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command: ["redis-server", "--appendonly", "yes", "--requirepass", "${REDIS_PASSWORD:-123456}"]
|
command: ["redis-server", "--appendonly", "yes", "--requirepass", "123456"]
|
||||||
ports:
|
ports:
|
||||||
- "${REDIS_PORT:-6379}:6379"
|
- "6379:6379"
|
||||||
volumes:
|
volumes:
|
||||||
- ./data/redis:/data
|
- ./data/redis:/data
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD:-123456}", "ping"]
|
test: ["CMD", "redis-cli", "-a", "123456", "ping"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 10
|
retries: 10
|
||||||
@@ -51,7 +50,7 @@ services:
|
|||||||
container_name: easyflow-etcd
|
container_name: easyflow-etcd
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
TZ: ${TZ:-Asia/Shanghai}
|
TZ: Asia/Shanghai
|
||||||
ETCD_AUTO_COMPACTION_MODE: revision
|
ETCD_AUTO_COMPACTION_MODE: revision
|
||||||
ETCD_AUTO_COMPACTION_RETENTION: "1000"
|
ETCD_AUTO_COMPACTION_RETENTION: "1000"
|
||||||
ETCD_QUOTA_BACKEND_BYTES: "4294967296"
|
ETCD_QUOTA_BACKEND_BYTES: "4294967296"
|
||||||
@@ -70,12 +69,12 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command: server /data --address ":9000" --console-address ":9001"
|
command: server /data --address ":9000" --console-address ":9001"
|
||||||
environment:
|
environment:
|
||||||
TZ: ${TZ:-Asia/Shanghai}
|
TZ: Asia/Shanghai
|
||||||
MINIO_ROOT_USER: ${MINIO_ROOT_USER:-easyflowadmin}
|
MINIO_ROOT_USER: easyflowadmin
|
||||||
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-easyflowadmin123}
|
MINIO_ROOT_PASSWORD: easyflowadmin123
|
||||||
ports:
|
ports:
|
||||||
- "${MINIO_PORT:-9000}:9000"
|
- "9000:9000"
|
||||||
- "${MINIO_CONSOLE_PORT:-9001}:9001"
|
- "9001:9001"
|
||||||
volumes:
|
volumes:
|
||||||
- ./data/minio:/data
|
- ./data/minio:/data
|
||||||
|
|
||||||
@@ -86,12 +85,12 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- minio
|
- minio
|
||||||
environment:
|
environment:
|
||||||
MINIO_ROOT_USER: ${MINIO_ROOT_USER:-easyflowadmin}
|
MINIO_ROOT_USER: easyflowadmin
|
||||||
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-easyflowadmin123}
|
MINIO_ROOT_PASSWORD: easyflowadmin123
|
||||||
MINIO_ENDPOINT: ${MINIO_ENDPOINT:-http://minio:9000}
|
MINIO_ENDPOINT: http://minio:9000
|
||||||
MINIO_BUCKETS: ${MINIO_BUCKETS:-easyflow,milvus}
|
MINIO_BUCKETS: easyflow,milvus
|
||||||
MINIO_PUBLIC_BUCKETS: ${MINIO_PUBLIC_BUCKETS:-easyflow}
|
MINIO_PUBLIC_BUCKETS: easyflow
|
||||||
MINIO_ALIAS: ${MINIO_ALIAS:-local}
|
MINIO_ALIAS: local
|
||||||
volumes:
|
volumes:
|
||||||
- ./scripts/minio/init-minio.sh:/scripts/init-minio.sh:ro
|
- ./scripts/minio/init-minio.sh:/scripts/init-minio.sh:ro
|
||||||
entrypoint: ["/bin/sh", "/scripts/init-minio.sh"]
|
entrypoint: ["/bin/sh", "/scripts/init-minio.sh"]
|
||||||
@@ -106,11 +105,11 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
ETCD_ENDPOINTS: etcd:2379
|
ETCD_ENDPOINTS: etcd:2379
|
||||||
COMMON_STORAGETYPE: minio
|
COMMON_STORAGETYPE: minio
|
||||||
MINIO_ADDRESS: ${MILVUS_MINIO_ADDRESS:-minio:9000}
|
MINIO_ADDRESS: minio:9000
|
||||||
MINIO_ACCESS_KEY_ID: ${MINIO_ROOT_USER:-easyflowadmin}
|
MINIO_ACCESS_KEY_ID: easyflowadmin
|
||||||
MINIO_SECRET_ACCESS_KEY: ${MINIO_ROOT_PASSWORD:-easyflowadmin123}
|
MINIO_SECRET_ACCESS_KEY: easyflowadmin123
|
||||||
MINIO_USE_SSL: "false"
|
MINIO_USE_SSL: "false"
|
||||||
MINIO_BUCKET_NAME: ${MILVUS_MINIO_BUCKET:-milvus}
|
MINIO_BUCKET_NAME: milvus
|
||||||
depends_on:
|
depends_on:
|
||||||
etcd:
|
etcd:
|
||||||
condition: service_started
|
condition: service_started
|
||||||
@@ -119,7 +118,32 @@ services:
|
|||||||
minio-init:
|
minio-init:
|
||||||
condition: service_completed_successfully
|
condition: service_completed_successfully
|
||||||
ports:
|
ports:
|
||||||
- "${MILVUS_GRPC_PORT:-19530}:19530"
|
- "39530:19530"
|
||||||
- "${MILVUS_HTTP_PORT:-9091}:9091"
|
- "9091:9091"
|
||||||
volumes:
|
volumes:
|
||||||
- ./data/milvus:/var/lib/milvus
|
- ./data/milvus:/var/lib/milvus
|
||||||
|
|
||||||
|
elasticsearch:
|
||||||
|
image: docker.elastic.co/elasticsearch/elasticsearch:8.15.5
|
||||||
|
container_name: easyflow-elasticsearch
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
TZ: Asia/Shanghai
|
||||||
|
discovery.type: single-node
|
||||||
|
ELASTIC_PASSWORD: elastic
|
||||||
|
xpack.security.enabled: "true"
|
||||||
|
ES_JAVA_OPTS: -Xms1g -Xmx1g
|
||||||
|
ports:
|
||||||
|
- "39200:9200"
|
||||||
|
volumes:
|
||||||
|
- ./data/elasticsearch:/usr/share/elasticsearch/data
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
[
|
||||||
|
"CMD-SHELL",
|
||||||
|
"curl -fsS -u elastic:elastic http://127.0.0.1:9200 >/dev/null || exit 1",
|
||||||
|
]
|
||||||
|
interval: 15s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 20
|
||||||
|
start_period: 60s
|
||||||
|
|||||||
@@ -61,10 +61,6 @@ services:
|
|||||||
MYSQL_DATABASE: easyflow
|
MYSQL_DATABASE: easyflow
|
||||||
ports:
|
ports:
|
||||||
- "3306:3306"
|
- "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:
|
networks:
|
||||||
- easyflow-net
|
- easyflow-net
|
||||||
healthcheck:
|
healthcheck:
|
||||||
|
|||||||
175
docker-entrypoint.sh
Normal file
175
docker-entrypoint.sh
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
JAR_PATH="${EASYFLOW_JAR_PATH:-/app/artifacts/easyflow.jar}"
|
||||||
|
CONFIG_PATH="${EASYFLOW_CONFIG_PATH:-file:/app/application.yml}"
|
||||||
|
LOG_FILE="${EASYFLOW_LOG_FILE:-/app/logs/app.log}"
|
||||||
|
RESTART_GRACE_SECONDS="${EASYFLOW_JAR_RESTART_GRACE_SECONDS:-30}"
|
||||||
|
ENTRYPOINT_PRIVS_DROPPED="${EASYFLOW_ENTRYPOINT_PRIVS_DROPPED:-0}"
|
||||||
|
|
||||||
|
JAVA_PID=""
|
||||||
|
WATCHER_PID=""
|
||||||
|
STOP_REQUESTED=0
|
||||||
|
RESTART_REASON_FILE="/tmp/easyflow-restart-reason"
|
||||||
|
|
||||||
|
log() {
|
||||||
|
printf '%s %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_runtime_permissions() {
|
||||||
|
for path in /app/logs /app/data; do
|
||||||
|
mkdir -p "$path"
|
||||||
|
chown -R easyflow:easyflow "$path"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
drop_privileges_if_needed() {
|
||||||
|
if [ "$(id -u)" -ne 0 ] || [ "$ENTRYPOINT_PRIVS_DROPPED" = "1" ]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
ensure_runtime_permissions
|
||||||
|
export EASYFLOW_ENTRYPOINT_PRIVS_DROPPED=1
|
||||||
|
exec setpriv --reuid=easyflow --regid=easyflow --init-groups "$0" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_prerequisites() {
|
||||||
|
if ! command -v inotifywait >/dev/null 2>&1; then
|
||||||
|
log "ERROR: inotifywait not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "$JAR_PATH" ]; then
|
||||||
|
log "ERROR: easyflow jar not found: $JAR_PATH"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$(dirname "$LOG_FILE")"
|
||||||
|
}
|
||||||
|
|
||||||
|
compute_jar_digest() {
|
||||||
|
sha256sum "$JAR_PATH" | awk '{print $1}'
|
||||||
|
}
|
||||||
|
|
||||||
|
stop_watcher() {
|
||||||
|
if [ -n "$WATCHER_PID" ] && kill -0 "$WATCHER_PID" 2>/dev/null; then
|
||||||
|
kill "$WATCHER_PID" 2>/dev/null || true
|
||||||
|
wait "$WATCHER_PID" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
WATCHER_PID=""
|
||||||
|
}
|
||||||
|
|
||||||
|
stop_java_process() {
|
||||||
|
if [ -z "$JAVA_PID" ] || ! kill -0 "$JAVA_PID" 2>/dev/null; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
kill -TERM "$JAVA_PID" 2>/dev/null || true
|
||||||
|
|
||||||
|
remaining="$RESTART_GRACE_SECONDS"
|
||||||
|
while kill -0 "$JAVA_PID" 2>/dev/null; do
|
||||||
|
if [ "$remaining" -le 0 ]; then
|
||||||
|
log "java process did not stop within ${RESTART_GRACE_SECONDS}s, forcing shutdown"
|
||||||
|
kill -KILL "$JAVA_PID" 2>/dev/null || true
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
remaining=$((remaining - 1))
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_shutdown() {
|
||||||
|
STOP_REQUESTED=1
|
||||||
|
printf '%s' "shutdown" >"$RESTART_REASON_FILE"
|
||||||
|
stop_watcher
|
||||||
|
stop_java_process
|
||||||
|
}
|
||||||
|
|
||||||
|
trap 'handle_shutdown' TERM INT
|
||||||
|
|
||||||
|
start_java() {
|
||||||
|
log "starting jar: $JAR_PATH"
|
||||||
|
# JAVA_OPTS needs shell word splitting to preserve user-provided JVM args.
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
java ${JAVA_OPTS:-} -Djava.security.egd=file:/dev/./urandom -jar "$JAR_PATH" \
|
||||||
|
--spring.config.location="$CONFIG_PATH" \
|
||||||
|
--logging.file.name="$LOG_FILE" &
|
||||||
|
JAVA_PID=$!
|
||||||
|
}
|
||||||
|
|
||||||
|
start_watcher() {
|
||||||
|
jar_digest="$1"
|
||||||
|
jar_dir="$(dirname "$JAR_PATH")"
|
||||||
|
jar_name="$(basename "$JAR_PATH")"
|
||||||
|
|
||||||
|
(
|
||||||
|
while true; do
|
||||||
|
event_line="$(inotifywait --quiet --event close_write,moved_to --format '%e %f' "$jar_dir")" || exit 0
|
||||||
|
event_name="${event_line%% *}"
|
||||||
|
event_file="${event_line#* }"
|
||||||
|
|
||||||
|
if [ "$event_file" != "$jar_name" ]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "$JAR_PATH" ]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
new_digest="$(compute_jar_digest)"
|
||||||
|
if [ "$new_digest" = "$jar_digest" ]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "detected jar update (${event_name}), restarting java process"
|
||||||
|
printf '%s' "jar_changed" >"$RESTART_REASON_FILE"
|
||||||
|
stop_java_process
|
||||||
|
exit 0
|
||||||
|
done
|
||||||
|
) &
|
||||||
|
|
||||||
|
WATCHER_PID=$!
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
drop_privileges_if_needed "$@"
|
||||||
|
ensure_prerequisites
|
||||||
|
|
||||||
|
current_digest="$(compute_jar_digest)"
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
rm -f "$RESTART_REASON_FILE"
|
||||||
|
start_java
|
||||||
|
start_watcher "$current_digest"
|
||||||
|
|
||||||
|
set +e
|
||||||
|
wait "$JAVA_PID"
|
||||||
|
java_status=$?
|
||||||
|
set -e
|
||||||
|
|
||||||
|
stop_watcher
|
||||||
|
JAVA_PID=""
|
||||||
|
|
||||||
|
restart_reason=""
|
||||||
|
if [ -f "$RESTART_REASON_FILE" ]; then
|
||||||
|
restart_reason="$(cat "$RESTART_REASON_FILE")"
|
||||||
|
rm -f "$RESTART_REASON_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$STOP_REQUESTED" -eq 1 ] || [ "$restart_reason" = "shutdown" ]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$restart_reason" = "jar_changed" ]; then
|
||||||
|
ensure_prerequisites
|
||||||
|
current_digest="$(compute_jar_digest)"
|
||||||
|
log "restarting java with updated jar"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "java process exited unexpectedly with code $java_status"
|
||||||
|
exit "$java_status"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
### 1. 产品主题与总规范
|
### 1. 产品主题与总规范
|
||||||
|
|
||||||
- [easyflow-product-design-spec.md](./easyflow-product-design-spec.md)
|
- [easyflow-product-design.md](easyflow-product-design.md)
|
||||||
|
|
||||||
适用场景:
|
适用场景:
|
||||||
|
|
||||||
|
|||||||
@@ -12,10 +12,22 @@
|
|||||||
<artifactId>easyflow-api-admin</artifactId>
|
<artifactId>easyflow-api-admin</artifactId>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>tech.easyflow</groupId>
|
||||||
|
<artifactId>easyflow-module-approval</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>tech.easyflow</groupId>
|
<groupId>tech.easyflow</groupId>
|
||||||
<artifactId>easyflow-module-ai</artifactId>
|
<artifactId>easyflow-module-ai</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>tech.easyflow</groupId>
|
||||||
|
<artifactId>easyflow-module-agent</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>tech.easyflow</groupId>
|
||||||
|
<artifactId>easyflow-module-chatlog</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>tech.easyflow</groupId>
|
<groupId>tech.easyflow</groupId>
|
||||||
<artifactId>easyflow-module-auth</artifactId>
|
<artifactId>easyflow-module-auth</artifactId>
|
||||||
@@ -28,5 +40,17 @@
|
|||||||
<groupId>tech.easyflow</groupId>
|
<groupId>tech.easyflow</groupId>
|
||||||
<artifactId>easyflow-common-captcha</artifactId>
|
<artifactId>easyflow-common-captcha</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>junit</groupId>
|
||||||
|
<artifactId>junit</artifactId>
|
||||||
|
<version>${junit.version}</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.mockito</groupId>
|
||||||
|
<artifactId>mockito-core</artifactId>
|
||||||
|
<version>5.12.0</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</project>
|
</project>
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
package tech.easyflow.admin.controller.agent;
|
||||||
|
|
||||||
|
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.agent.entity.Agent;
|
||||||
|
import tech.easyflow.agent.entity.AgentCategory;
|
||||||
|
import tech.easyflow.agent.mapper.AgentMapper;
|
||||||
|
import tech.easyflow.agent.service.AgentCategoryService;
|
||||||
|
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.system.entity.vo.RoleCategoryAccessSnapshot;
|
||||||
|
import tech.easyflow.system.enums.CategoryResourceType;
|
||||||
|
import tech.easyflow.system.service.CategoryPermissionService;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Agent 分类管理控制器。
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/agentCategory")
|
||||||
|
@UsePermission(moduleName = "/api/v1/agent")
|
||||||
|
public class AgentCategoryController extends BaseCurdController<AgentCategoryService, AgentCategory> {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private AgentMapper agentMapper;
|
||||||
|
@Resource
|
||||||
|
private CategoryPermissionService categoryPermissionService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建 Agent 分类管理控制器。
|
||||||
|
*
|
||||||
|
* @param service Agent 分类服务
|
||||||
|
*/
|
||||||
|
public AgentCategoryController(AgentCategoryService service) {
|
||||||
|
super(service);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询当前用户可见的 Agent 分类。
|
||||||
|
*
|
||||||
|
* @param entity 查询条件
|
||||||
|
* @param asTree 是否转树
|
||||||
|
* @param sortKey 排序字段
|
||||||
|
* @param sortType 排序方式
|
||||||
|
* @return 可见分类列表
|
||||||
|
*/
|
||||||
|
@GetMapping("visibleList")
|
||||||
|
public Result<List<AgentCategory>> visibleList(AgentCategory entity, Boolean asTree, String sortKey, String sortType) {
|
||||||
|
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
|
||||||
|
RoleCategoryAccessSnapshot access = categoryPermissionService.getCurrentAccess(CategoryResourceType.AGENT.getCode());
|
||||||
|
if (access.isRestricted()) {
|
||||||
|
if (access.getCategoryIds().isEmpty()) {
|
||||||
|
return Result.ok(Collections.emptyList());
|
||||||
|
}
|
||||||
|
queryWrapper.in("id", access.getCategoryIds());
|
||||||
|
}
|
||||||
|
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||||
|
return Result.ok(service.list(queryWrapper));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除分类前校验是否仍被 Agent 使用。
|
||||||
|
*
|
||||||
|
* @param ids 分类 ID 集合
|
||||||
|
* @return 校验结果
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected Result<?> onRemoveBefore(Collection<Serializable> ids) {
|
||||||
|
for (Serializable id : ids) {
|
||||||
|
QueryWrapper queryWrapper = QueryWrapper.create().eq(Agent::getCategoryId, id);
|
||||||
|
List<Agent> agents = agentMapper.selectListByQuery(queryWrapper);
|
||||||
|
if (agents != null && !agents.isEmpty()) {
|
||||||
|
throw new BusinessException("请先删除该分类下的所有 Agent");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.onRemoveBefore(ids);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,351 @@
|
|||||||
|
package tech.easyflow.admin.controller.agent;
|
||||||
|
|
||||||
|
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 org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.context.request.RequestContextHolder;
|
||||||
|
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||||
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
|
import tech.easyflow.admin.controller.ai.support.AiResourceCreatorNameSupport;
|
||||||
|
import tech.easyflow.agent.entity.Agent;
|
||||||
|
import tech.easyflow.agent.entity.AgentKnowledgeBinding;
|
||||||
|
import tech.easyflow.agent.entity.AgentToolBinding;
|
||||||
|
import tech.easyflow.agent.publish.AgentPublishAppService;
|
||||||
|
import tech.easyflow.agent.runtime.AgentChatRequest;
|
||||||
|
import tech.easyflow.agent.runtime.AgentDraftChatRequest;
|
||||||
|
import tech.easyflow.agent.runtime.AgentRunService;
|
||||||
|
import tech.easyflow.agent.service.AgentApprovalStateService;
|
||||||
|
import tech.easyflow.agent.service.AgentKnowledgeBindingService;
|
||||||
|
import tech.easyflow.agent.service.AgentService;
|
||||||
|
import tech.easyflow.agent.service.AgentToolBindingService;
|
||||||
|
import tech.easyflow.ai.enums.PublishStatus;
|
||||||
|
import tech.easyflow.approval.entity.vo.ApprovalActionResult;
|
||||||
|
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.vo.RoleCategoryAccessSnapshot;
|
||||||
|
import tech.easyflow.system.enums.CategoryResourceType;
|
||||||
|
import tech.easyflow.system.enums.ResourceAction;
|
||||||
|
import tech.easyflow.system.service.CategoryPermissionService;
|
||||||
|
import tech.easyflow.system.service.ResourceAccessService;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static tech.easyflow.agent.entity.table.AgentTableDef.AGENT;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Agent 管理端控制器。
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/agent")
|
||||||
|
public class AgentController extends BaseCurdController<AgentService, Agent> {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private AgentToolBindingService agentToolBindingService;
|
||||||
|
@Resource
|
||||||
|
private AgentKnowledgeBindingService agentKnowledgeBindingService;
|
||||||
|
@Resource
|
||||||
|
private AgentRunService agentRunService;
|
||||||
|
@Resource
|
||||||
|
private AgentPublishAppService agentPublishAppService;
|
||||||
|
@Resource
|
||||||
|
private ResourceAccessService resourceAccessService;
|
||||||
|
@Resource
|
||||||
|
private CategoryPermissionService categoryPermissionService;
|
||||||
|
@Resource
|
||||||
|
private AgentApprovalStateService agentApprovalStateService;
|
||||||
|
@Resource
|
||||||
|
private AiResourceCreatorNameSupport aiResourceCreatorNameSupport;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建 Agent 控制器。
|
||||||
|
*
|
||||||
|
* @param service Agent 服务
|
||||||
|
*/
|
||||||
|
public AgentController(AgentService service) {
|
||||||
|
super(service);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 Agent 详情。
|
||||||
|
*
|
||||||
|
* @param id Agent ID
|
||||||
|
* @return Agent 详情
|
||||||
|
*/
|
||||||
|
@GetMapping("/getDetail")
|
||||||
|
public Result<Agent> getDetail(BigInteger id) {
|
||||||
|
Agent agent = service.getDetail(id);
|
||||||
|
agentApprovalStateService.fillAgentApprovalState(agent);
|
||||||
|
return Result.ok(agent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存 Agent 草稿。
|
||||||
|
*
|
||||||
|
* @param agent Agent 草稿
|
||||||
|
* @return Agent 详情
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@PostMapping("save")
|
||||||
|
public Result<?> save(@JsonBody Agent agent) {
|
||||||
|
return Result.ok(service.saveDraft(agent));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新 Agent 草稿。
|
||||||
|
*
|
||||||
|
* @param agent Agent 草稿
|
||||||
|
* @return Agent 详情
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@PostMapping("update")
|
||||||
|
public Result<?> update(@JsonBody Agent agent) {
|
||||||
|
return Result.ok(service.updateDraft(agent));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询 Agent 列表。
|
||||||
|
*
|
||||||
|
* @param entity 查询条件
|
||||||
|
* @param asTree 是否转树
|
||||||
|
* @param sortKey 排序字段
|
||||||
|
* @param sortType 排序方式
|
||||||
|
* @return Agent 列表
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Result<List<Agent>> list(Agent entity, Boolean asTree, String sortKey, String sortType) {
|
||||||
|
HttpServletRequest request = currentRequest();
|
||||||
|
QueryWrapper queryWrapper = request == null ? QueryWrapper.create() : buildQueryWrapper(request);
|
||||||
|
if (!applyCategoryPermission(queryWrapper)) {
|
||||||
|
return Result.ok(Collections.emptyList());
|
||||||
|
}
|
||||||
|
applyPublishedOnlyFilter(queryWrapper);
|
||||||
|
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||||
|
List<Agent> agents = service.list(queryWrapper);
|
||||||
|
if (isPublishedOnlyRequest()) {
|
||||||
|
agents = agents.stream().map(agent -> service.fromSnapshot(agent.getPublishedSnapshotJson())).toList();
|
||||||
|
}
|
||||||
|
agentApprovalStateService.fillAgentApprovalState(agents);
|
||||||
|
aiResourceCreatorNameSupport.fillAgentCreatorNames(agents);
|
||||||
|
return Result.ok(agents);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运行 Agent 纯文本聊天。
|
||||||
|
*
|
||||||
|
* @param request 聊天请求
|
||||||
|
* @return SSE Emitter
|
||||||
|
*/
|
||||||
|
@PostMapping("chat")
|
||||||
|
public SseEmitter chat(@JsonBody AgentChatRequest request) {
|
||||||
|
return agentRunService.chat(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运行 Agent 草稿态纯文本试用。
|
||||||
|
*
|
||||||
|
* @param request 草稿试用请求
|
||||||
|
* @return SSE Emitter
|
||||||
|
*/
|
||||||
|
@PostMapping("/chat/draft")
|
||||||
|
public SseEmitter chatDraft(@JsonBody AgentDraftChatRequest request) {
|
||||||
|
return agentRunService.chatDraft(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理 Agent 草稿试运行会话。
|
||||||
|
*
|
||||||
|
* @param sessionId 草稿试运行会话 ID
|
||||||
|
* @return 操作结果
|
||||||
|
*/
|
||||||
|
@PostMapping("/chat/draft/clear")
|
||||||
|
public Result<Void> clearDraftSession(@JsonBody(value = "sessionId", required = true) String sessionId) {
|
||||||
|
agentRunService.clearDraftSession(sessionId);
|
||||||
|
return Result.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批准工具执行。
|
||||||
|
*
|
||||||
|
* @param requestId 请求 ID
|
||||||
|
* @param resumeToken 恢复令牌
|
||||||
|
* @return 操作结果
|
||||||
|
*/
|
||||||
|
@PostMapping("/run/approve")
|
||||||
|
public Result<Void> approve(@JsonBody("requestId") String requestId,
|
||||||
|
@JsonBody(value = "resumeToken", required = true) String resumeToken) {
|
||||||
|
agentRunService.approve(requestId, resumeToken);
|
||||||
|
return Result.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拒绝工具执行。
|
||||||
|
*
|
||||||
|
* @param requestId 请求 ID
|
||||||
|
* @param resumeToken 恢复令牌
|
||||||
|
* @param reason 拒绝原因
|
||||||
|
* @return 操作结果
|
||||||
|
*/
|
||||||
|
@PostMapping("/run/reject")
|
||||||
|
public Result<Void> reject(@JsonBody("requestId") String requestId,
|
||||||
|
@JsonBody(value = "resumeToken", required = true) String resumeToken,
|
||||||
|
@JsonBody("reason") String reason) {
|
||||||
|
agentRunService.reject(requestId, resumeToken, reason);
|
||||||
|
return Result.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新 Agent 工具绑定。
|
||||||
|
*
|
||||||
|
* @param agentId Agent ID
|
||||||
|
* @param bindings 工具绑定
|
||||||
|
* @return 保存后的启用绑定
|
||||||
|
*/
|
||||||
|
@PostMapping("/toolBinding/update")
|
||||||
|
@SaCheckPermission("/api/v1/agent/save")
|
||||||
|
public Result<List<AgentToolBinding>> updateToolBinding(@JsonBody(value = "agentId", required = true) BigInteger agentId,
|
||||||
|
@JsonBody("bindings") List<AgentToolBinding> bindings) {
|
||||||
|
return Result.ok(agentToolBindingService.replaceBindings(agentId, bindings));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新 Agent 知识库绑定。
|
||||||
|
*
|
||||||
|
* @param agentId Agent ID
|
||||||
|
* @param bindings 知识库绑定
|
||||||
|
* @return 保存后的启用绑定
|
||||||
|
*/
|
||||||
|
@PostMapping("/knowledgeBinding/update")
|
||||||
|
@SaCheckPermission("/api/v1/agent/save")
|
||||||
|
public Result<List<AgentKnowledgeBinding>> updateKnowledgeBinding(@JsonBody(value = "agentId", required = true) BigInteger agentId,
|
||||||
|
@JsonBody("bindings") List<AgentKnowledgeBinding> bindings) {
|
||||||
|
return Result.ok(agentKnowledgeBindingService.replaceBindings(agentId, bindings));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交发布审批。
|
||||||
|
*
|
||||||
|
* @param id Agent ID
|
||||||
|
* @return 审批实例 ID
|
||||||
|
*/
|
||||||
|
@PostMapping("/submitPublishApproval")
|
||||||
|
@SaCheckPermission("/api/v1/agent/save")
|
||||||
|
public Result<BigInteger> submitPublishApproval(@JsonBody("id") BigInteger id) {
|
||||||
|
return buildApprovalActionResult(agentPublishAppService.submitPublishApproval(id), "已提交发布审批", "已直接发布");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交下线审批。
|
||||||
|
*
|
||||||
|
* @param id Agent ID
|
||||||
|
* @return 审批实例 ID
|
||||||
|
*/
|
||||||
|
@PostMapping("/submitOfflineApproval")
|
||||||
|
@SaCheckPermission("/api/v1/agent/save")
|
||||||
|
public Result<BigInteger> submitOfflineApproval(@JsonBody("id") BigInteger id) {
|
||||||
|
return buildApprovalActionResult(agentPublishAppService.submitOfflineApproval(id), "已提交下线审批", "已直接下线");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交删除审批。
|
||||||
|
*
|
||||||
|
* @param id Agent ID
|
||||||
|
* @return 审批实例 ID
|
||||||
|
*/
|
||||||
|
@PostMapping("/submitDeleteApproval")
|
||||||
|
@SaCheckPermission("/api/v1/agent/remove")
|
||||||
|
public Result<BigInteger> submitDeleteApproval(@JsonBody("id") BigInteger id) {
|
||||||
|
return buildApprovalActionResult(agentPublishAppService.submitDeleteApproval(id), "已提交删除审批", "已直接删除");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Result<?> onRemoveBefore(Collection<Serializable> ids) {
|
||||||
|
for (Serializable id : ids) {
|
||||||
|
Agent agent = service.getById(String.valueOf(id));
|
||||||
|
if (agent != null) {
|
||||||
|
resourceAccessService.assertAccess(CategoryResourceType.AGENT, agent, ResourceAction.MANAGE, "无权限删除该 Agent");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
agentToolBindingService.remove(QueryWrapper.create().in("agent_id", ids));
|
||||||
|
agentKnowledgeBindingService.remove(QueryWrapper.create().in("agent_id", ids));
|
||||||
|
return super.onRemoveBefore(ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询 Agent 分页。
|
||||||
|
*
|
||||||
|
* @param page 分页参数
|
||||||
|
* @param queryWrapper 查询条件
|
||||||
|
* @return Agent 分页
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected Page<Agent> queryPage(Page<Agent> page, QueryWrapper queryWrapper) {
|
||||||
|
if (!applyCategoryPermission(queryWrapper)) {
|
||||||
|
return new Page<>(Collections.emptyList(), page.getPageNumber(), page.getPageSize(), 0L);
|
||||||
|
}
|
||||||
|
applyPublishedOnlyFilter(queryWrapper);
|
||||||
|
Page<Agent> result = super.queryPage(page, queryWrapper);
|
||||||
|
if (isPublishedOnlyRequest()) {
|
||||||
|
result.setRecords(result.getRecords().stream().map(agent -> service.fromSnapshot(agent.getPublishedSnapshotJson())).toList());
|
||||||
|
}
|
||||||
|
agentApprovalStateService.fillAgentApprovalState(result.getRecords());
|
||||||
|
aiResourceCreatorNameSupport.fillAgentCreatorNames(result.getRecords());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean applyCategoryPermission(QueryWrapper queryWrapper) {
|
||||||
|
RoleCategoryAccessSnapshot access = categoryPermissionService.getCurrentAccess(CategoryResourceType.AGENT.getCode());
|
||||||
|
if (!access.isRestricted()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (access.getCategoryIds().isEmpty()) {
|
||||||
|
queryWrapper.eq(Agent::getCreatedBy, access.getAccountId());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
queryWrapper.and(AGENT.CREATED_BY.eq(access.getAccountId()).or(AGENT.CATEGORY_ID.in(access.getCategoryIds())));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyPublishedOnlyFilter(QueryWrapper queryWrapper) {
|
||||||
|
if (isPublishedOnlyRequest()) {
|
||||||
|
queryWrapper.eq("publish_status", PublishStatus.PUBLISHED.getCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isPublishedOnlyRequest() {
|
||||||
|
HttpServletRequest request = currentRequest();
|
||||||
|
if (request == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return "true".equalsIgnoreCase(request.getParameter("publishedOnly"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前 HTTP 请求。
|
||||||
|
*
|
||||||
|
* @return 当前请求,不在 Web 请求上下文中时返回 null
|
||||||
|
*/
|
||||||
|
private HttpServletRequest currentRequest() {
|
||||||
|
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||||
|
if (attributes == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return attributes.getRequest();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Result<BigInteger> buildApprovalActionResult(ApprovalActionResult actionResult,
|
||||||
|
String approvalMessage,
|
||||||
|
String directMessage) {
|
||||||
|
return Result.ok(actionResult.isApprovalRequired() ? approvalMessage : directMessage, actionResult.getInstanceId());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
package tech.easyflow.admin.controller.agent;
|
||||||
|
|
||||||
|
import com.mybatisflex.core.keygen.impl.SnowFlakeIDKeyGenerator;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import tech.easyflow.admin.dto.chatworkspace.ChatWorkspaceConversationView;
|
||||||
|
import tech.easyflow.admin.dto.chatworkspace.ChatWorkspaceSessionDetailView;
|
||||||
|
import tech.easyflow.admin.dto.chatworkspace.ChatWorkspaceSessionPage;
|
||||||
|
import tech.easyflow.admin.service.agent.AgentSessionService;
|
||||||
|
import tech.easyflow.chatlog.domain.dto.ChatHistoryPage;
|
||||||
|
import tech.easyflow.chatlog.domain.query.ChatPageQuery;
|
||||||
|
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 java.math.BigInteger;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Agent 管理端会话控制器。
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/agent/session")
|
||||||
|
public class AgentSessionController {
|
||||||
|
|
||||||
|
private final AgentSessionService agentSessionService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建 Agent 管理端会话控制器。
|
||||||
|
*
|
||||||
|
* @param agentSessionService Agent 会话服务
|
||||||
|
*/
|
||||||
|
public AgentSessionController(AgentSessionService agentSessionService) {
|
||||||
|
this.agentSessionService = agentSessionService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成 Agent 会话 ID。
|
||||||
|
*
|
||||||
|
* @return 会话 ID 字符串
|
||||||
|
*/
|
||||||
|
@GetMapping("/generateId")
|
||||||
|
public Result<String> generateId() {
|
||||||
|
long nextId = new SnowFlakeIDKeyGenerator().nextId();
|
||||||
|
return Result.ok(String.valueOf(nextId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询 Agent 会话分页。
|
||||||
|
*
|
||||||
|
* @param agentId Agent ID,可为空
|
||||||
|
* @param query 分页参数
|
||||||
|
* @return 会话分页
|
||||||
|
*/
|
||||||
|
@GetMapping("/list")
|
||||||
|
public Result<ChatWorkspaceSessionPage> list(BigInteger agentId, ChatPageQuery query) {
|
||||||
|
return Result.ok(agentSessionService.queryCurrentUserSessions(currentAccount(), agentId, query));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询 Agent 会话详情。
|
||||||
|
*
|
||||||
|
* @param sessionId 会话 ID
|
||||||
|
* @return 会话详情
|
||||||
|
*/
|
||||||
|
@GetMapping("/{sessionId}")
|
||||||
|
public Result<ChatWorkspaceSessionDetailView> detail(@PathVariable BigInteger sessionId) {
|
||||||
|
return Result.ok(agentSessionService.getCurrentUserSession(currentAccount(), sessionId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询 Agent 会话消息。
|
||||||
|
*
|
||||||
|
* @param sessionId 会话 ID
|
||||||
|
* @param query 分页参数
|
||||||
|
* @return 消息分页
|
||||||
|
*/
|
||||||
|
@GetMapping("/{sessionId}/messages")
|
||||||
|
public Result<ChatHistoryPage> messages(@PathVariable BigInteger sessionId, ChatPageQuery query) {
|
||||||
|
return Result.ok(agentSessionService.queryCurrentUserMessages(currentAccount(), sessionId, query));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询 Agent 完整会话。
|
||||||
|
*
|
||||||
|
* @param sessionId 会话 ID
|
||||||
|
* @return 完整会话
|
||||||
|
*/
|
||||||
|
@GetMapping("/{sessionId}/conversation")
|
||||||
|
public Result<ChatWorkspaceConversationView> conversation(@PathVariable BigInteger sessionId) {
|
||||||
|
return Result.ok(agentSessionService.getCurrentUserConversation(currentAccount(), sessionId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重命名 Agent 会话。
|
||||||
|
*
|
||||||
|
* @param sessionId 会话 ID
|
||||||
|
* @param title 新标题
|
||||||
|
* @return 操作结果
|
||||||
|
*/
|
||||||
|
@PostMapping("/{sessionId}/rename")
|
||||||
|
public Result<Void> rename(@PathVariable BigInteger sessionId,
|
||||||
|
@JsonBody(value = "title", required = true) String title) {
|
||||||
|
agentSessionService.renameCurrentUserSession(currentAccount(), sessionId, title);
|
||||||
|
return Result.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存 Agent 会话临时知识库。
|
||||||
|
*
|
||||||
|
* @param sessionId 会话 ID
|
||||||
|
* @param knowledgeIds 临时知识库 ID
|
||||||
|
* @return 操作结果
|
||||||
|
*/
|
||||||
|
@PostMapping("/{sessionId}/extraKnowledges")
|
||||||
|
public Result<ChatWorkspaceSessionDetailView> saveExtraKnowledges(@PathVariable BigInteger sessionId,
|
||||||
|
@JsonBody(value = "knowledgeIds") List<BigInteger> knowledgeIds) {
|
||||||
|
return Result.ok(agentSessionService.saveCurrentUserExtraKnowledges(currentAccount(), sessionId, knowledgeIds));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除 Agent 会话。
|
||||||
|
*
|
||||||
|
* @param sessionId 会话 ID
|
||||||
|
* @return 操作结果
|
||||||
|
*/
|
||||||
|
@PostMapping("/{sessionId}/delete")
|
||||||
|
public Result<Void> delete(@PathVariable BigInteger sessionId) {
|
||||||
|
agentSessionService.deleteCurrentUserSession(currentAccount(), sessionId);
|
||||||
|
return Result.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
private LoginAccount currentAccount() {
|
||||||
|
return SaTokenUtil.getLoginAccount();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,20 @@
|
|||||||
package tech.easyflow.admin.controller.ai;
|
package tech.easyflow.admin.controller.ai;
|
||||||
|
|
||||||
|
import com.mybatisflex.core.query.QueryWrapper;
|
||||||
import tech.easyflow.ai.entity.BotCategory;
|
import tech.easyflow.ai.entity.BotCategory;
|
||||||
import tech.easyflow.ai.service.BotCategoryService;
|
import tech.easyflow.ai.service.BotCategoryService;
|
||||||
import tech.easyflow.common.annotation.UsePermission;
|
import tech.easyflow.common.annotation.UsePermission;
|
||||||
|
import tech.easyflow.common.domain.Result;
|
||||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
|
||||||
|
import tech.easyflow.system.service.CategoryPermissionService;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* bot分类 控制层。
|
* bot分类 控制层。
|
||||||
@@ -17,7 +26,24 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
@RequestMapping("/api/v1/botCategory")
|
@RequestMapping("/api/v1/botCategory")
|
||||||
@UsePermission(moduleName = "/api/v1/bot")
|
@UsePermission(moduleName = "/api/v1/bot")
|
||||||
public class BotCategoryController extends BaseCurdController<BotCategoryService, BotCategory> {
|
public class BotCategoryController extends BaseCurdController<BotCategoryService, BotCategory> {
|
||||||
|
@Resource
|
||||||
|
private CategoryPermissionService categoryPermissionService;
|
||||||
|
|
||||||
public BotCategoryController(BotCategoryService service) {
|
public BotCategoryController(BotCategoryService service) {
|
||||||
super(service);
|
super(service);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
@GetMapping("visibleList")
|
||||||
|
public Result<List<BotCategory>> visibleList(BotCategory entity, Boolean asTree, String sortKey, String sortType) {
|
||||||
|
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
|
||||||
|
RoleCategoryAccessSnapshot access = categoryPermissionService.getCurrentAccess("BOT");
|
||||||
|
if (access.isRestricted()) {
|
||||||
|
if (access.getCategoryIds().isEmpty()) {
|
||||||
|
return Result.ok(Collections.emptyList());
|
||||||
|
}
|
||||||
|
queryWrapper.in(BotCategory::getId, access.getCategoryIds());
|
||||||
|
}
|
||||||
|
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||||
|
return Result.ok(service.list(queryWrapper));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,28 +3,46 @@ package tech.easyflow.admin.controller.ai;
|
|||||||
|
|
||||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
import cn.dev33.satoken.annotation.SaIgnore;
|
import cn.dev33.satoken.annotation.SaIgnore;
|
||||||
|
import cn.dev33.satoken.stp.StpUtil;
|
||||||
import com.easyagents.core.model.chat.ChatModel;
|
import com.easyagents.core.model.chat.ChatModel;
|
||||||
import com.easyagents.core.model.chat.ChatOptions;
|
import com.easyagents.core.model.chat.ChatOptions;
|
||||||
import com.alicp.jetcache.Cache;
|
import com.alicp.jetcache.Cache;
|
||||||
|
import com.mybatisflex.core.paginate.Page;
|
||||||
import com.mybatisflex.core.keygen.impl.SnowFlakeIDKeyGenerator;
|
import com.mybatisflex.core.keygen.impl.SnowFlakeIDKeyGenerator;
|
||||||
import com.mybatisflex.core.query.QueryWrapper;
|
import com.mybatisflex.core.query.QueryWrapper;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
import org.springframework.web.context.request.RequestContextHolder;
|
||||||
|
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
|
import tech.easyflow.admin.controller.ai.support.AiResourceCreatorNameSupport;
|
||||||
|
import tech.easyflow.admin.service.ai.ChatWorkspaceService;
|
||||||
|
import tech.easyflow.ai.chattime.availability.ChatTimeToolAvailabilityContext;
|
||||||
import tech.easyflow.ai.easyagents.listener.PromptChoreChatStreamListener;
|
import tech.easyflow.ai.easyagents.listener.PromptChoreChatStreamListener;
|
||||||
import tech.easyflow.ai.entity.*;
|
import tech.easyflow.ai.entity.*;
|
||||||
|
import tech.easyflow.ai.enums.PublishStatus;
|
||||||
|
import tech.easyflow.ai.publish.BotPublishAppService;
|
||||||
|
import tech.easyflow.approval.entity.vo.ApprovalActionResult;
|
||||||
import tech.easyflow.ai.service.*;
|
import tech.easyflow.ai.service.*;
|
||||||
import tech.easyflow.ai.service.impl.BotServiceImpl;
|
import tech.easyflow.ai.service.impl.BotServiceImpl;
|
||||||
import tech.easyflow.common.audio.core.AudioServiceManager;
|
import tech.easyflow.common.audio.core.AudioServiceManager;
|
||||||
import tech.easyflow.common.domain.Result;
|
import tech.easyflow.common.domain.Result;
|
||||||
|
import tech.easyflow.common.entity.LoginAccount;
|
||||||
|
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||||
|
import tech.easyflow.chatlog.service.ChatRoundOperateService;
|
||||||
import tech.easyflow.core.chat.protocol.sse.ChatSseEmitter;
|
import tech.easyflow.core.chat.protocol.sse.ChatSseEmitter;
|
||||||
import tech.easyflow.core.chat.protocol.sse.ChatSseUtil;
|
import tech.easyflow.core.chat.protocol.sse.ChatSseUtil;
|
||||||
|
import tech.easyflow.core.runtime.ChatChannel;
|
||||||
|
import tech.easyflow.core.runtime.ChatRuntimeExtKeys;
|
||||||
|
import tech.easyflow.core.runtime.ChatRuntimeContext;
|
||||||
|
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
|
||||||
|
import tech.easyflow.system.service.CategoryPermissionService;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
@@ -34,6 +52,8 @@ import java.util.HashMap;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static tech.easyflow.ai.entity.table.BotTableDef.BOT;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 控制层。
|
* 控制层。
|
||||||
*
|
*
|
||||||
@@ -55,6 +75,18 @@ public class BotController extends BaseCurdController<BotService, Bot> {
|
|||||||
private Cache<String, Object> cache;
|
private Cache<String, Object> cache;
|
||||||
@Resource
|
@Resource
|
||||||
private AudioServiceManager audioServiceManager;
|
private AudioServiceManager audioServiceManager;
|
||||||
|
@Resource
|
||||||
|
private CategoryPermissionService categoryPermissionService;
|
||||||
|
@Resource
|
||||||
|
private BotPublishAppService botPublishAppService;
|
||||||
|
@Resource
|
||||||
|
private ChatRoundOperateService chatRoundOperateService;
|
||||||
|
@Resource
|
||||||
|
private AiResourceApprovalStateService aiResourceApprovalStateService;
|
||||||
|
@Resource
|
||||||
|
private AiResourceCreatorNameSupport aiResourceCreatorNameSupport;
|
||||||
|
@Resource
|
||||||
|
private ChatWorkspaceService chatWorkspaceService;
|
||||||
|
|
||||||
public BotController(BotService service, ModelService modelService, BotWorkflowService botWorkflowService,
|
public BotController(BotService service, ModelService modelService, BotWorkflowService botWorkflowService,
|
||||||
BotDocumentCollectionService botDocumentCollectionService, BotMessageService botMessageService) {
|
BotDocumentCollectionService botDocumentCollectionService, BotMessageService botMessageService) {
|
||||||
@@ -140,17 +172,42 @@ public class BotController extends BaseCurdController<BotService, Bot> {
|
|||||||
@JsonBody(value = "botId", required = true) BigInteger botId,
|
@JsonBody(value = "botId", required = true) BigInteger botId,
|
||||||
@JsonBody(value = "conversationId", required = true) BigInteger conversationId,
|
@JsonBody(value = "conversationId", required = true) BigInteger conversationId,
|
||||||
@JsonBody(value = "messages") List<Map<String, String>> messages,
|
@JsonBody(value = "messages") List<Map<String, String>> messages,
|
||||||
@JsonBody(value = "attachments") List<String> attachments
|
@JsonBody(value = "attachments") List<String> attachments,
|
||||||
|
@JsonBody(value = "publishedOnly") Boolean publishedOnly,
|
||||||
|
@JsonBody(value = "extraKnowledgeIds") List<BigInteger> extraKnowledgeIds,
|
||||||
|
@JsonBody(value = "regenerateRoundId") BigInteger regenerateRoundId
|
||||||
|
|
||||||
) {
|
) {
|
||||||
|
boolean usePublishedOnly = Boolean.TRUE.equals(publishedOnly);
|
||||||
BotServiceImpl.ChatCheckResult chatCheckResult = new BotServiceImpl.ChatCheckResult();
|
BotServiceImpl.ChatCheckResult chatCheckResult = new BotServiceImpl.ChatCheckResult();
|
||||||
|
if (usePublishedOnly) {
|
||||||
|
chatWorkspaceService.assertSessionContinuable(requireCurrentLoginAccount(), conversationId, botId);
|
||||||
|
}
|
||||||
|
if (regenerateRoundId != null) {
|
||||||
|
chatRoundOperateService.requireRegeneratableRound(conversationId, regenerateRoundId);
|
||||||
|
}
|
||||||
|
|
||||||
// 前置校验:失败则直接返回错误SseEmitter
|
// 前置校验:失败则直接返回错误SseEmitter
|
||||||
SseEmitter errorEmitter = botService.checkChatBeforeStart(botId, prompt, conversationId.toString(), chatCheckResult);
|
SseEmitter errorEmitter = botService.checkChatBeforeStart(
|
||||||
|
botId,
|
||||||
|
prompt,
|
||||||
|
conversationId.toString(),
|
||||||
|
chatCheckResult,
|
||||||
|
usePublishedOnly,
|
||||||
|
regenerateRoundId
|
||||||
|
);
|
||||||
if (errorEmitter != null) {
|
if (errorEmitter != null) {
|
||||||
return errorEmitter;
|
return errorEmitter;
|
||||||
}
|
}
|
||||||
return botService.startChat(botId, prompt, conversationId, messages, chatCheckResult, attachments);
|
return botService.startChat(
|
||||||
|
botId,
|
||||||
|
prompt,
|
||||||
|
conversationId,
|
||||||
|
messages,
|
||||||
|
chatCheckResult,
|
||||||
|
attachments,
|
||||||
|
buildRuntimeContext(chatCheckResult.getAiBot(), conversationId, prompt, attachments, extraKnowledgeIds, regenerateRoundId)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("updateLlmId")
|
@PostMapping("updateLlmId")
|
||||||
@@ -164,15 +221,51 @@ public class BotController extends BaseCurdController<BotService, Bot> {
|
|||||||
@GetMapping("getDetail")
|
@GetMapping("getDetail")
|
||||||
@SaIgnore
|
@SaIgnore
|
||||||
public Result<Bot> getDetail(String id) {
|
public Result<Bot> getDetail(String id) {
|
||||||
return Result.ok(botService.getDetail(id));
|
boolean publishedOnly = isPublishedOnlyRequest();
|
||||||
|
Bot rawBot = StpUtil.isLogin() ? botService.getDetail(id) : botService.getPublishedDetail(id);
|
||||||
|
if (rawBot != null && StpUtil.isLogin()) {
|
||||||
|
categoryPermissionService.assertCategoryResourceVisible("BOT", rawBot.getCreatedBy(), rawBot.getCategoryId(), "无权限访问聊天助手");
|
||||||
|
}
|
||||||
|
if (rawBot == null) {
|
||||||
|
return Result.ok(null);
|
||||||
|
}
|
||||||
|
if (!StpUtil.isLogin() && !PublishStatus.from(rawBot.getPublishStatus()).isExternallyVisible()) {
|
||||||
|
throw new BusinessException("聊天助手尚未发布");
|
||||||
|
}
|
||||||
|
Bot bot = rawBot;
|
||||||
|
if (publishedOnly && StpUtil.isLogin()) {
|
||||||
|
if (PublishStatus.from(rawBot.getPublishStatus()) != PublishStatus.PUBLISHED) {
|
||||||
|
throw new BusinessException("聊天助手尚未发布");
|
||||||
|
}
|
||||||
|
bot = botService.toPublishedView(rawBot);
|
||||||
|
}
|
||||||
|
if (StpUtil.isLogin()) {
|
||||||
|
aiResourceApprovalStateService.fillBotApprovalState(bot);
|
||||||
|
}
|
||||||
|
return Result.ok(bot);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@SaIgnore
|
@SaIgnore
|
||||||
public Result<Bot> detail(String id) {
|
public Result<Bot> detail(String id) {
|
||||||
Bot data = botService.getDetail(id);
|
boolean publishedOnly = isPublishedOnlyRequest();
|
||||||
if (data == null) {
|
Bot rawData = StpUtil.isLogin() ? botService.getDetail(id) : botService.getPublishedDetail(id);
|
||||||
return Result.ok(data);
|
if (rawData == null) {
|
||||||
|
return Result.ok(rawData);
|
||||||
|
}
|
||||||
|
if (StpUtil.isLogin()) {
|
||||||
|
categoryPermissionService.assertCategoryResourceVisible("BOT", rawData.getCreatedBy(), rawData.getCategoryId(), "无权限访问聊天助手");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!StpUtil.isLogin() && !PublishStatus.from(rawData.getPublishStatus()).isExternallyVisible()) {
|
||||||
|
throw new BusinessException("聊天助手尚未发布");
|
||||||
|
}
|
||||||
|
Bot data = rawData;
|
||||||
|
if (publishedOnly && StpUtil.isLogin()) {
|
||||||
|
if (PublishStatus.from(rawData.getPublishStatus()) != PublishStatus.PUBLISHED) {
|
||||||
|
throw new BusinessException("聊天助手尚未发布");
|
||||||
|
}
|
||||||
|
data = botService.toPublishedView(rawData);
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, Object> llmOptions = data.getModelOptions();
|
Map<String, Object> llmOptions = data.getModelOptions();
|
||||||
@@ -181,6 +274,9 @@ public class BotController extends BaseCurdController<BotService, Bot> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (data.getModelId() == null) {
|
if (data.getModelId() == null) {
|
||||||
|
if (StpUtil.isLogin()) {
|
||||||
|
aiResourceApprovalStateService.fillBotApprovalState(data);
|
||||||
|
}
|
||||||
return Result.ok(data);
|
return Result.ok(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,6 +285,9 @@ public class BotController extends BaseCurdController<BotService, Bot> {
|
|||||||
|
|
||||||
if (llm == null) {
|
if (llm == null) {
|
||||||
data.setModelId(null);
|
data.setModelId(null);
|
||||||
|
if (StpUtil.isLogin()) {
|
||||||
|
aiResourceApprovalStateService.fillBotApprovalState(data);
|
||||||
|
}
|
||||||
return Result.ok(data);
|
return Result.ok(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,9 +301,90 @@ public class BotController extends BaseCurdController<BotService, Bot> {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (StpUtil.isLogin()) {
|
||||||
|
aiResourceApprovalStateService.fillBotApprovalState(data);
|
||||||
|
}
|
||||||
return Result.ok(data);
|
return Result.ok(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/submitPublishApproval")
|
||||||
|
@SaCheckPermission("/api/v1/bot/save")
|
||||||
|
public Result<BigInteger> submitPublishApproval(@JsonBody("id") BigInteger id) {
|
||||||
|
return buildApprovalActionResult(
|
||||||
|
botPublishAppService.submitPublishApproval(id),
|
||||||
|
"已提交发布审批",
|
||||||
|
"已直接发布"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/submitOfflineApproval")
|
||||||
|
@SaCheckPermission("/api/v1/bot/save")
|
||||||
|
public Result<BigInteger> submitOfflineApproval(@JsonBody("id") BigInteger id) {
|
||||||
|
return buildApprovalActionResult(
|
||||||
|
botPublishAppService.submitOfflineApproval(id),
|
||||||
|
"已提交下线审批",
|
||||||
|
"已直接下线"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/submitDeleteApproval")
|
||||||
|
@SaCheckPermission("/api/v1/bot/remove")
|
||||||
|
public Result<BigInteger> submitDeleteApproval(@JsonBody("id") BigInteger id) {
|
||||||
|
return buildApprovalActionResult(
|
||||||
|
botPublishAppService.submitDeleteApproval(id),
|
||||||
|
"已提交删除审批",
|
||||||
|
"已直接删除"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Result<List<Bot>> list(Bot entity, Boolean asTree, String sortKey, String sortType) {
|
||||||
|
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
|
||||||
|
applyCategoryPermission(queryWrapper);
|
||||||
|
applyPublishedOnlyFilter(queryWrapper);
|
||||||
|
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||||
|
List<Bot> bots = service.list(queryWrapper);
|
||||||
|
if (isPublishedOnlyRequest()) {
|
||||||
|
bots = bots.stream().map(botService::toPublishedView).toList();
|
||||||
|
}
|
||||||
|
aiResourceApprovalStateService.fillBotApprovalState(bots);
|
||||||
|
return Result.ok(bots);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Page<Bot> queryPage(Page<Bot> page, QueryWrapper queryWrapper) {
|
||||||
|
applyCategoryPermission(queryWrapper);
|
||||||
|
applyPublishedOnlyFilter(queryWrapper);
|
||||||
|
Page<Bot> result = super.queryPage(page, queryWrapper);
|
||||||
|
if (isPublishedOnlyRequest()) {
|
||||||
|
result.setRecords(result.getRecords().stream().map(botService::toPublishedView).toList());
|
||||||
|
}
|
||||||
|
aiResourceApprovalStateService.fillBotApprovalState(result.getRecords());
|
||||||
|
aiResourceCreatorNameSupport.fillBotCreatorNames(result.getRecords());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Result<BigInteger> buildApprovalActionResult(ApprovalActionResult actionResult,
|
||||||
|
String approvalMessage,
|
||||||
|
String directMessage) {
|
||||||
|
return Result.ok(
|
||||||
|
actionResult.isApprovalRequired() ? approvalMessage : directMessage,
|
||||||
|
actionResult.getInstanceId()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyCategoryPermission(QueryWrapper queryWrapper) {
|
||||||
|
RoleCategoryAccessSnapshot access = categoryPermissionService.getCurrentAccess("BOT");
|
||||||
|
if (!access.isRestricted()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (access.getCategoryIds().isEmpty()) {
|
||||||
|
queryWrapper.eq(Bot::getCreatedBy, access.getAccountId());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
queryWrapper.and(BOT.CREATED_BY.eq(access.getAccountId()).or(BOT.CATEGORY_ID.in(access.getCategoryIds())));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Result<?> onSaveOrUpdateBefore(Bot entity, boolean isSave) {
|
protected Result<?> onSaveOrUpdateBefore(Bot entity, boolean isSave) {
|
||||||
|
|
||||||
@@ -278,6 +458,55 @@ public class BotController extends BaseCurdController<BotService, Bot> {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ChatRuntimeContext buildRuntimeContext(Bot bot, BigInteger conversationId, String prompt, List<String> attachments,
|
||||||
|
List<BigInteger> extraKnowledgeIds,
|
||||||
|
BigInteger regenerateRoundId) {
|
||||||
|
LoginAccount account = requireCurrentLoginAccount();
|
||||||
|
ChatRuntimeContext context = new ChatRuntimeContext();
|
||||||
|
context.setChannel(ChatChannel.ADMIN);
|
||||||
|
context.setSessionId(conversationId);
|
||||||
|
context.setTenantId(account.getTenantId());
|
||||||
|
context.setDeptId(account.getDeptId());
|
||||||
|
context.setUserId(account.getId());
|
||||||
|
context.setUserAccount(account.getLoginName());
|
||||||
|
context.setUserName(StringUtils.hasText(account.getNickname()) ? account.getNickname() : account.getLoginName());
|
||||||
|
context.setAssistantId(bot == null ? BigInteger.ZERO : bot.getId());
|
||||||
|
context.setAssistantCode(bot == null ? null : bot.getAlias());
|
||||||
|
context.setAssistantName(bot == null ? null : bot.getTitle());
|
||||||
|
context.setSessionTitle(prompt.length() > 200 ? prompt.substring(0, 200) : prompt);
|
||||||
|
context.setAttachments(attachments);
|
||||||
|
if (extraKnowledgeIds != null) {
|
||||||
|
context.getExt().put(ChatRuntimeExtKeys.EXTRA_KNOWLEDGE_IDS, extraKnowledgeIds);
|
||||||
|
}
|
||||||
|
if (regenerateRoundId != null) {
|
||||||
|
context.getExt().put(ChatRuntimeExtKeys.REGENERATE_ROUND_ID, regenerateRoundId);
|
||||||
|
}
|
||||||
|
ChatTimeToolAvailabilityContext.bindLoggedInSnapshot(context, account, bot);
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyPublishedOnlyFilter(QueryWrapper queryWrapper) {
|
||||||
|
if (isPublishedOnlyRequest()) {
|
||||||
|
queryWrapper.eq("publish_status", PublishStatus.PUBLISHED.getCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isPublishedOnlyRequest() {
|
||||||
|
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||||
|
if (attributes == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return "true".equalsIgnoreCase(attributes.getRequest().getParameter("publishedOnly"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private LoginAccount requireCurrentLoginAccount() {
|
||||||
|
try {
|
||||||
|
return SaTokenUtil.getLoginAccount();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new BusinessException("当前登录状态失效,请重新登录后再试");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Result<?> onRemoveBefore(Collection<Serializable> ids) {
|
protected Result<?> onRemoveBefore(Collection<Serializable> ids) {
|
||||||
QueryWrapper queryWrapperKnowledge = QueryWrapper.create().in(BotDocumentCollection::getBotId, ids);
|
QueryWrapper queryWrapperKnowledge = QueryWrapper.create().in(BotDocumentCollection::getBotId, ids);
|
||||||
@@ -289,6 +518,12 @@ public class BotController extends BaseCurdController<BotService, Bot> {
|
|||||||
return super.onRemoveBefore(ids);
|
return super.onRemoveBefore(ids);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@PostMapping("remove")
|
||||||
|
public Result<?> remove(@JsonBody(value = "id", required = true) Serializable id) {
|
||||||
|
return Result.fail(1, "请使用发布状态操作删除聊天助手");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 系统提示词优化
|
* 系统提示词优化
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
package tech.easyflow.admin.controller.ai;
|
package tech.easyflow.admin.controller.ai;
|
||||||
|
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import tech.easyflow.ai.dto.BotKnowledgeBindingRequest;
|
||||||
import tech.easyflow.ai.entity.BotDocumentCollection;
|
import tech.easyflow.ai.entity.BotDocumentCollection;
|
||||||
|
import tech.easyflow.ai.entity.DocumentCollection;
|
||||||
|
import tech.easyflow.ai.enums.PublishStatus;
|
||||||
|
import tech.easyflow.ai.permission.KnowledgeReadAccessSnapshot;
|
||||||
|
import tech.easyflow.ai.permission.KnowledgeVisibilityQueryHelper;
|
||||||
import tech.easyflow.ai.service.BotDocumentCollectionService;
|
import tech.easyflow.ai.service.BotDocumentCollectionService;
|
||||||
|
import tech.easyflow.ai.service.DocumentCollectionService;
|
||||||
import tech.easyflow.common.annotation.UsePermission;
|
import tech.easyflow.common.annotation.UsePermission;
|
||||||
import tech.easyflow.common.domain.Result;
|
import tech.easyflow.common.domain.Result;
|
||||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||||
@@ -11,8 +17,13 @@ import org.springframework.web.bind.annotation.GetMapping;
|
|||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||||
|
import tech.easyflow.system.enums.CategoryResourceType;
|
||||||
|
import tech.easyflow.system.enums.ResourceAction;
|
||||||
|
import tech.easyflow.system.service.ResourceAccessService;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -25,6 +36,13 @@ import java.util.List;
|
|||||||
@RequestMapping("/api/v1/botKnowledge")
|
@RequestMapping("/api/v1/botKnowledge")
|
||||||
@UsePermission(moduleName = "/api/v1/bot")
|
@UsePermission(moduleName = "/api/v1/bot")
|
||||||
public class BotDocumentCollectionController extends BaseCurdController<BotDocumentCollectionService, BotDocumentCollection> {
|
public class BotDocumentCollectionController extends BaseCurdController<BotDocumentCollectionService, BotDocumentCollection> {
|
||||||
|
@Resource
|
||||||
|
private DocumentCollectionService documentCollectionService;
|
||||||
|
@Resource
|
||||||
|
private KnowledgeVisibilityQueryHelper knowledgeVisibilityQueryHelper;
|
||||||
|
@Resource
|
||||||
|
private ResourceAccessService resourceAccessService;
|
||||||
|
|
||||||
public BotDocumentCollectionController(BotDocumentCollectionService service) {
|
public BotDocumentCollectionController(BotDocumentCollectionService service) {
|
||||||
super(service);
|
super(service);
|
||||||
}
|
}
|
||||||
@@ -35,12 +53,36 @@ public class BotDocumentCollectionController extends BaseCurdController<BotDocum
|
|||||||
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
|
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
|
||||||
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||||
List<BotDocumentCollection> botDocumentCollections = service.getMapper().selectListWithRelationsByQuery(queryWrapper);
|
List<BotDocumentCollection> botDocumentCollections = service.getMapper().selectListWithRelationsByQuery(queryWrapper);
|
||||||
return Result.ok(botDocumentCollections);
|
List<BotDocumentCollection> visibleList = new ArrayList<>();
|
||||||
|
KnowledgeReadAccessSnapshot snapshot = knowledgeVisibilityQueryHelper.getCurrentReadSnapshot();
|
||||||
|
for (BotDocumentCollection relation : botDocumentCollections) {
|
||||||
|
DocumentCollection knowledge = relation.getKnowledge();
|
||||||
|
if (knowledge == null || knowledgeVisibilityQueryHelper.canRead(knowledge, snapshot)) {
|
||||||
|
visibleList.add(relation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Result.ok(visibleList);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("updateBotKnowledgeIds")
|
@PostMapping("updateBotKnowledgeIds")
|
||||||
public Result<?> save(@JsonBody("botId") BigInteger botId, @JsonBody("knowledgeIds") BigInteger [] knowledgeIds) {
|
public Result<?> save(@JsonBody("botId") BigInteger botId,
|
||||||
service.saveBotAndKnowledge(botId, knowledgeIds);
|
@JsonBody("knowledgeBindings") List<BotKnowledgeBindingRequest> knowledgeBindings) {
|
||||||
|
if (knowledgeBindings != null) {
|
||||||
|
for (BotKnowledgeBindingRequest binding : knowledgeBindings) {
|
||||||
|
if (binding == null || binding.getKnowledgeId() == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
DocumentCollection collection = documentCollectionService.getById(binding.getKnowledgeId());
|
||||||
|
if (collection == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
resourceAccessService.assertAccess(CategoryResourceType.KNOWLEDGE, collection, ResourceAction.READ, "无权限绑定知识库");
|
||||||
|
if (PublishStatus.from(collection.getPublishStatus()) != PublishStatus.PUBLISHED) {
|
||||||
|
throw new tech.easyflow.common.web.exceptions.BusinessException("仅已发布知识库可被聊天助手引用");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
service.saveBotAndKnowledge(botId, knowledgeBindings);
|
||||||
return Result.ok();
|
return Result.ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,19 +3,26 @@ package tech.easyflow.admin.controller.ai;
|
|||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import tech.easyflow.ai.entity.Plugin;
|
import tech.easyflow.ai.entity.Plugin;
|
||||||
import tech.easyflow.ai.entity.BotPlugin;
|
import tech.easyflow.ai.entity.BotPlugin;
|
||||||
|
import tech.easyflow.ai.entity.PluginItem;
|
||||||
|
import tech.easyflow.ai.enums.PluginType;
|
||||||
import tech.easyflow.common.annotation.UsePermission;
|
import tech.easyflow.common.annotation.UsePermission;
|
||||||
import tech.easyflow.common.domain.Result;
|
import tech.easyflow.common.domain.Result;
|
||||||
import tech.easyflow.common.tree.Tree;
|
import tech.easyflow.common.tree.Tree;
|
||||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||||
import tech.easyflow.ai.service.BotPluginService;
|
import tech.easyflow.ai.service.BotPluginService;
|
||||||
|
import tech.easyflow.ai.service.PluginService;
|
||||||
|
import tech.easyflow.ai.service.PluginItemService;
|
||||||
|
import tech.easyflow.ai.service.PluginVisibilityService;
|
||||||
import com.mybatisflex.core.query.QueryWrapper;
|
import com.mybatisflex.core.query.QueryWrapper;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||||
|
import tech.easyflow.system.service.CategoryPermissionService;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -35,6 +42,12 @@ public class BotPluginController extends BaseCurdController<BotPluginService, Bo
|
|||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private BotPluginService botPluginService;
|
private BotPluginService botPluginService;
|
||||||
|
@Resource
|
||||||
|
private PluginItemService pluginItemService;
|
||||||
|
@Resource
|
||||||
|
private PluginService pluginService;
|
||||||
|
@Resource
|
||||||
|
private PluginVisibilityService pluginVisibilityService;
|
||||||
|
|
||||||
@GetMapping("list")
|
@GetMapping("list")
|
||||||
public Result<List<BotPlugin>> list(BotPlugin entity, Boolean asTree, String sortKey, String sortType){
|
public Result<List<BotPlugin>> list(BotPlugin entity, Boolean asTree, String sortKey, String sortType){
|
||||||
@@ -43,15 +56,42 @@ public class BotPluginController extends BaseCurdController<BotPluginService, Bo
|
|||||||
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||||
|
|
||||||
List<BotPlugin> botPlugins = service.getMapper().selectListWithRelationsByQuery(queryWrapper);
|
List<BotPlugin> botPlugins = service.getMapper().selectListWithRelationsByQuery(queryWrapper);
|
||||||
|
List<BotPlugin> visibleList = new ArrayList<>();
|
||||||
|
for (BotPlugin relation : botPlugins) {
|
||||||
|
Plugin plugin = relation.getAiPlugin();
|
||||||
|
if (plugin == null) {
|
||||||
|
visibleList.add(relation);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (PluginType.isWorkflow(plugin.getType())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (pluginVisibilityService.canAccessPlugin(plugin.getCreatedBy(), plugin.getId())) {
|
||||||
|
visibleList.add(relation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
List<BotPlugin> list = Tree.tryToTree(botPlugins, asTree);
|
List<BotPlugin> list = Tree.tryToTree(visibleList, asTree);
|
||||||
|
|
||||||
return Result.ok(list);
|
return Result.ok(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/getList")
|
@PostMapping("/getList")
|
||||||
public Result<List<Plugin>> getList(@JsonBody(value = "botId", required = true) String botId){
|
public Result<List<Plugin>> getList(@JsonBody(value = "botId", required = true) String botId){
|
||||||
return Result.ok(botPluginService.getList(botId));
|
List<Plugin> plugins = botPluginService.getList(botId);
|
||||||
|
List<Plugin> visibleList = new ArrayList<>();
|
||||||
|
for (Plugin plugin : plugins) {
|
||||||
|
if (plugin == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (PluginType.isWorkflow(plugin.getType())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (pluginVisibilityService.canAccessPlugin(plugin.getCreatedBy(), plugin.getId())) {
|
||||||
|
visibleList.add(plugin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Result.ok(visibleList);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/getBotPluginToolIds")
|
@PostMapping("/getBotPluginToolIds")
|
||||||
@@ -67,6 +107,26 @@ public class BotPluginController extends BaseCurdController<BotPluginService, Bo
|
|||||||
|
|
||||||
@PostMapping("updateBotPluginToolIds")
|
@PostMapping("updateBotPluginToolIds")
|
||||||
public Result<?> save(@JsonBody("botId") BigInteger botId, @JsonBody("pluginToolIds") BigInteger [] pluginToolIds) {
|
public Result<?> save(@JsonBody("botId") BigInteger botId, @JsonBody("pluginToolIds") BigInteger [] pluginToolIds) {
|
||||||
|
if (pluginToolIds != null) {
|
||||||
|
for (BigInteger pluginToolId : pluginToolIds) {
|
||||||
|
if (pluginToolId == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
PluginItem pluginItem = pluginItemService.getById(pluginToolId);
|
||||||
|
if (pluginItem == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (pluginItem.getPluginId() != null) {
|
||||||
|
Plugin plugin = pluginService.getById(pluginItem.getPluginId());
|
||||||
|
if (plugin != null) {
|
||||||
|
if (PluginType.isWorkflow(plugin.getType())) {
|
||||||
|
throw new tech.easyflow.common.web.exceptions.BusinessException("当前版本暂不支持聊天助手绑定工作流插件");
|
||||||
|
}
|
||||||
|
pluginVisibilityService.assertPluginVisible(plugin.getCreatedBy(), plugin.getId(), "无权限绑定插件");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
service.saveBotAndPluginTool(botId, pluginToolIds);
|
service.saveBotAndPluginTool(botId, pluginToolIds);
|
||||||
return Result.ok();
|
return Result.ok();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,12 @@ package tech.easyflow.admin.controller.ai;
|
|||||||
|
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import tech.easyflow.ai.entity.BotWorkflow;
|
import tech.easyflow.ai.entity.BotWorkflow;
|
||||||
|
import tech.easyflow.ai.entity.Workflow;
|
||||||
|
import tech.easyflow.ai.enums.PublishStatus;
|
||||||
|
import tech.easyflow.ai.permission.WorkflowReadAccessSnapshot;
|
||||||
|
import tech.easyflow.ai.permission.WorkflowVisibilityQueryHelper;
|
||||||
import tech.easyflow.ai.service.BotWorkflowService;
|
import tech.easyflow.ai.service.BotWorkflowService;
|
||||||
|
import tech.easyflow.ai.service.WorkflowService;
|
||||||
import tech.easyflow.common.annotation.UsePermission;
|
import tech.easyflow.common.annotation.UsePermission;
|
||||||
import tech.easyflow.common.domain.Result;
|
import tech.easyflow.common.domain.Result;
|
||||||
import tech.easyflow.common.tree.Tree;
|
import tech.easyflow.common.tree.Tree;
|
||||||
@@ -12,10 +17,16 @@ import org.springframework.web.bind.annotation.GetMapping;
|
|||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||||
|
import tech.easyflow.system.enums.CategoryResourceType;
|
||||||
|
import tech.easyflow.system.enums.ResourceAction;
|
||||||
|
import tech.easyflow.system.service.ResourceAccessService;
|
||||||
|
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 控制层。
|
* 控制层。
|
||||||
*
|
*
|
||||||
@@ -26,6 +37,13 @@ import java.util.List;
|
|||||||
@RequestMapping("/api/v1/botWorkflow")
|
@RequestMapping("/api/v1/botWorkflow")
|
||||||
@UsePermission(moduleName = "/api/v1/bot")
|
@UsePermission(moduleName = "/api/v1/bot")
|
||||||
public class BotWorkflowController extends BaseCurdController<BotWorkflowService, BotWorkflow> {
|
public class BotWorkflowController extends BaseCurdController<BotWorkflowService, BotWorkflow> {
|
||||||
|
@Resource
|
||||||
|
private WorkflowService workflowService;
|
||||||
|
@Resource
|
||||||
|
private WorkflowVisibilityQueryHelper workflowVisibilityQueryHelper;
|
||||||
|
@Resource
|
||||||
|
private ResourceAccessService resourceAccessService;
|
||||||
|
|
||||||
public BotWorkflowController(BotWorkflowService service) {
|
public BotWorkflowController(BotWorkflowService service) {
|
||||||
super(service);
|
super(service);
|
||||||
}
|
}
|
||||||
@@ -36,13 +54,36 @@ public class BotWorkflowController extends BaseCurdController<BotWorkflowService
|
|||||||
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
|
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
|
||||||
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||||
List<BotWorkflow> botWorkflows = service.getMapper().selectListWithRelationsByQuery(queryWrapper);
|
List<BotWorkflow> botWorkflows = service.getMapper().selectListWithRelationsByQuery(queryWrapper);
|
||||||
List<BotWorkflow> list = Tree.tryToTree(botWorkflows, asTree);
|
List<BotWorkflow> visibleList = new ArrayList<>();
|
||||||
|
WorkflowReadAccessSnapshot snapshot = workflowVisibilityQueryHelper.getCurrentReadSnapshot();
|
||||||
|
for (BotWorkflow botWorkflow : botWorkflows) {
|
||||||
|
Workflow workflow = botWorkflow.getWorkflow();
|
||||||
|
if (workflow == null || workflowVisibilityQueryHelper.canRead(workflow, snapshot)) {
|
||||||
|
visibleList.add(botWorkflow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<BotWorkflow> list = Tree.tryToTree(visibleList, asTree);
|
||||||
return Result.ok(list);
|
return Result.ok(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("updateBotWorkflowIds")
|
@PostMapping("updateBotWorkflowIds")
|
||||||
public Result<?> save(@JsonBody("botId") BigInteger botId, @JsonBody("workflowIds") BigInteger [] workflowIds) {
|
public Result<?> save(@JsonBody("botId") BigInteger botId, @JsonBody("workflowIds") BigInteger [] workflowIds) {
|
||||||
|
if (workflowIds != null) {
|
||||||
|
for (BigInteger workflowId : workflowIds) {
|
||||||
|
if (workflowId == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Workflow workflow = workflowService.getById(workflowId);
|
||||||
|
if (workflow == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
resourceAccessService.assertAccess(CategoryResourceType.WORKFLOW, workflow, ResourceAction.READ, "无权限绑定工作流");
|
||||||
|
if (PublishStatus.from(workflow.getPublishStatus()) != PublishStatus.PUBLISHED) {
|
||||||
|
throw new tech.easyflow.common.web.exceptions.BusinessException("仅已发布工作流可被聊天助手引用");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
service.saveBotAndWorkflowTool(botId, workflowIds);
|
service.saveBotAndWorkflowTool(botId, workflowIds);
|
||||||
return Result.ok();
|
return Result.ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package tech.easyflow.admin.controller.ai;
|
||||||
|
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import tech.easyflow.chatlog.domain.dto.ChatHistoryPage;
|
||||||
|
import tech.easyflow.chatlog.domain.dto.ChatMessageRecord;
|
||||||
|
import tech.easyflow.chatlog.domain.dto.ChatSessionPage;
|
||||||
|
import tech.easyflow.chatlog.domain.dto.ChatSessionSummary;
|
||||||
|
import tech.easyflow.chatlog.domain.query.ChatPageQuery;
|
||||||
|
import tech.easyflow.chatlog.domain.query.ChatSessionFilterQuery;
|
||||||
|
import tech.easyflow.chatlog.service.ChatHistoryManageService;
|
||||||
|
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 java.math.BigInteger;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/chatHistory")
|
||||||
|
public class ChatHistoryController {
|
||||||
|
|
||||||
|
private final ChatHistoryManageService chatHistoryManageService;
|
||||||
|
|
||||||
|
public ChatHistoryController(ChatHistoryManageService chatHistoryManageService) {
|
||||||
|
this.chatHistoryManageService = chatHistoryManageService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/sessions")
|
||||||
|
public Result<ChatSessionPage> listSessions(ChatSessionFilterQuery query) {
|
||||||
|
return Result.ok(chatHistoryManageService.queryAdminSessions(query));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/sessions/{sessionId}")
|
||||||
|
public Result<ChatSessionSummary> getSession(@PathVariable BigInteger sessionId) {
|
||||||
|
return Result.ok(chatHistoryManageService.getAdminSession(sessionId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/sessions/{sessionId}/messages")
|
||||||
|
public Result<ChatHistoryPage> queryMessages(@PathVariable BigInteger sessionId, ChatPageQuery query) {
|
||||||
|
return Result.ok(chatHistoryManageService.queryAdminMessages(sessionId, query));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/sessions/{sessionId}/rounds/{roundId}/variants")
|
||||||
|
public Result<List<ChatMessageRecord>> listRoundVariants(@PathVariable BigInteger sessionId,
|
||||||
|
@PathVariable BigInteger roundId) {
|
||||||
|
return Result.ok(chatHistoryManageService.listAdminRoundVariants(sessionId, roundId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/sessions/{sessionId}/rounds/{roundId}/selectVariant")
|
||||||
|
public Result<ChatMessageRecord> selectRoundVariant(@PathVariable BigInteger sessionId,
|
||||||
|
@PathVariable BigInteger roundId,
|
||||||
|
@JsonBody(value = "variantIndex", required = true) Integer variantIndex) {
|
||||||
|
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||||
|
return Result.ok(chatHistoryManageService.selectAdminRoundVariant(sessionId, roundId, variantIndex, account.getId()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package tech.easyflow.admin.controller.ai;
|
||||||
|
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import tech.easyflow.admin.dto.chatworkspace.ChatWorkspaceConversationView;
|
||||||
|
import tech.easyflow.admin.dto.chatworkspace.ChatWorkspaceSessionDetailView;
|
||||||
|
import tech.easyflow.admin.dto.chatworkspace.ChatWorkspaceSessionPage;
|
||||||
|
import tech.easyflow.admin.service.ai.ChatWorkspaceService;
|
||||||
|
import tech.easyflow.chatlog.domain.dto.ChatHistoryPage;
|
||||||
|
import tech.easyflow.chatlog.domain.dto.ChatMessageRecord;
|
||||||
|
import tech.easyflow.chatlog.domain.query.ChatPageQuery;
|
||||||
|
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 java.math.BigInteger;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理端聊天工作台控制器。
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/chatWorkspace")
|
||||||
|
public class ChatWorkspaceController {
|
||||||
|
|
||||||
|
private final ChatWorkspaceService chatWorkspaceService;
|
||||||
|
|
||||||
|
public ChatWorkspaceController(ChatWorkspaceService chatWorkspaceService) {
|
||||||
|
this.chatWorkspaceService = chatWorkspaceService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/sessions")
|
||||||
|
public Result<ChatWorkspaceSessionPage> listSessions(BigInteger assistantId, ChatPageQuery query) {
|
||||||
|
return Result.ok(chatWorkspaceService.queryCurrentUserSessions(currentAccount(), assistantId, query));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/sessions/{sessionId}")
|
||||||
|
public Result<ChatWorkspaceSessionDetailView> getSession(@PathVariable BigInteger sessionId) {
|
||||||
|
return Result.ok(chatWorkspaceService.getCurrentUserSession(currentAccount(), sessionId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/sessions/{sessionId}/messages")
|
||||||
|
public Result<ChatHistoryPage> queryMessages(@PathVariable BigInteger sessionId, ChatPageQuery query) {
|
||||||
|
return Result.ok(chatWorkspaceService.queryCurrentUserMessages(currentAccount(), sessionId, query));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/sessions/{sessionId}/conversation")
|
||||||
|
public Result<ChatWorkspaceConversationView> getConversation(@PathVariable BigInteger sessionId) {
|
||||||
|
return Result.ok(chatWorkspaceService.getCurrentUserConversation(currentAccount(), sessionId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/sessions/{sessionId}/rename")
|
||||||
|
public Result<Void> renameSession(@PathVariable BigInteger sessionId,
|
||||||
|
@JsonBody(value = "title", required = true) String title) {
|
||||||
|
chatWorkspaceService.renameCurrentUserSession(currentAccount(), sessionId, title);
|
||||||
|
return Result.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/sessions/{sessionId}/delete")
|
||||||
|
public Result<Void> deleteSession(@PathVariable BigInteger sessionId) {
|
||||||
|
chatWorkspaceService.deleteCurrentUserSession(currentAccount(), sessionId);
|
||||||
|
return Result.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/sessions/{sessionId}/rounds/{roundId}/variants")
|
||||||
|
public Result<List<ChatMessageRecord>> listRoundVariants(@PathVariable BigInteger sessionId,
|
||||||
|
@PathVariable BigInteger roundId) {
|
||||||
|
return Result.ok(chatWorkspaceService.listCurrentUserRoundVariants(currentAccount(), sessionId, roundId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/sessions/{sessionId}/rounds/{roundId}/selectVariant")
|
||||||
|
public Result<ChatMessageRecord> selectRoundVariant(@PathVariable BigInteger sessionId,
|
||||||
|
@PathVariable BigInteger roundId,
|
||||||
|
@JsonBody(value = "variantIndex", required = true) Integer variantIndex) {
|
||||||
|
return Result.ok(chatWorkspaceService.selectCurrentUserRoundVariant(currentAccount(), sessionId, roundId, variantIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
private LoginAccount currentAccount() {
|
||||||
|
return SaTokenUtil.getLoginAccount();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,12 +2,14 @@ package tech.easyflow.admin.controller.ai;
|
|||||||
|
|
||||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
import com.easyagents.core.model.embedding.EmbeddingModel;
|
import com.easyagents.core.model.embedding.EmbeddingModel;
|
||||||
|
import com.mybatisflex.core.paginate.Page;
|
||||||
import tech.easyflow.ai.entity.DocumentChunk;
|
import tech.easyflow.ai.entity.DocumentChunk;
|
||||||
import tech.easyflow.ai.entity.DocumentCollection;
|
import tech.easyflow.ai.entity.DocumentCollection;
|
||||||
import tech.easyflow.ai.entity.Model;
|
import tech.easyflow.ai.entity.Model;
|
||||||
import tech.easyflow.ai.service.DocumentChunkService;
|
import tech.easyflow.ai.service.DocumentChunkService;
|
||||||
import tech.easyflow.ai.service.DocumentCollectionService;
|
import tech.easyflow.ai.service.DocumentCollectionService;
|
||||||
import tech.easyflow.ai.service.ModelService;
|
import tech.easyflow.ai.service.ModelService;
|
||||||
|
import tech.easyflow.ai.support.DocumentStoreLifecycleSupport;
|
||||||
import tech.easyflow.common.annotation.UsePermission;
|
import tech.easyflow.common.annotation.UsePermission;
|
||||||
import tech.easyflow.common.domain.Result;
|
import tech.easyflow.common.domain.Result;
|
||||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||||
@@ -16,9 +18,15 @@ import com.easyagents.core.document.Document;
|
|||||||
import com.easyagents.core.store.DocumentStore;
|
import com.easyagents.core.store.DocumentStore;
|
||||||
import com.easyagents.core.store.StoreOptions;
|
import com.easyagents.core.store.StoreOptions;
|
||||||
import com.easyagents.core.store.StoreResult;
|
import com.easyagents.core.store.StoreResult;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import tech.easyflow.system.enums.CategoryResourceType;
|
||||||
|
import tech.easyflow.system.enums.ResourceAction;
|
||||||
|
import tech.easyflow.system.enums.ResourceLookup;
|
||||||
|
import tech.easyflow.system.permission.resource.RequireResourceAccess;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
@@ -51,8 +59,29 @@ public class DocumentChunkController extends BaseCurdController<DocumentChunkSer
|
|||||||
super(service);
|
super(service);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("page")
|
||||||
|
@SaCheckPermission("/api/v1/documentCollection/query")
|
||||||
|
@RequireResourceAccess(
|
||||||
|
resource = CategoryResourceType.KNOWLEDGE,
|
||||||
|
action = ResourceAction.READ,
|
||||||
|
lookup = ResourceLookup.DOCUMENT_ID,
|
||||||
|
idExpr = "#request.getParameter('documentId')",
|
||||||
|
denyMessage = "无权限访问知识库"
|
||||||
|
)
|
||||||
|
@Override
|
||||||
|
public Result<Page<DocumentChunk>> page(HttpServletRequest request, String sortKey, String sortType, Long pageNumber, Long pageSize) {
|
||||||
|
return super.page(request, sortKey, sortType, pageNumber, pageSize);
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("update")
|
@PostMapping("update")
|
||||||
@SaCheckPermission("/api/v1/documentCollection/save")
|
@SaCheckPermission("/api/v1/documentCollection/save")
|
||||||
|
@RequireResourceAccess(
|
||||||
|
resource = CategoryResourceType.KNOWLEDGE,
|
||||||
|
action = ResourceAction.MANAGE,
|
||||||
|
lookup = ResourceLookup.DOCUMENT_CHUNK_ID,
|
||||||
|
idExpr = "#documentChunk.id",
|
||||||
|
denyMessage = "无权限管理知识库"
|
||||||
|
)
|
||||||
public Result<?> update(@JsonBody DocumentChunk documentChunk) {
|
public Result<?> update(@JsonBody DocumentChunk documentChunk) {
|
||||||
boolean success = service.updateById(documentChunk);
|
boolean success = service.updateById(documentChunk);
|
||||||
if (success){
|
if (success){
|
||||||
@@ -65,28 +94,39 @@ public class DocumentChunkController extends BaseCurdController<DocumentChunkSer
|
|||||||
if (documentStore == null) {
|
if (documentStore == null) {
|
||||||
return Result.fail(2, "知识库没有配置向量库");
|
return Result.fail(2, "知识库没有配置向量库");
|
||||||
}
|
}
|
||||||
// 设置向量模型
|
try {
|
||||||
Model model = modelService.getModelInstance(knowledge.getVectorEmbedModelId());
|
// 设置向量模型
|
||||||
if (model == null) {
|
Model model = modelService.getModelInstance(knowledge.getVectorEmbedModelId());
|
||||||
return Result.fail(3, "知识库没有配置向量模型");
|
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);
|
||||||
|
} finally {
|
||||||
|
DocumentStoreLifecycleSupport.closeQuietly(documentStore);
|
||||||
}
|
}
|
||||||
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);
|
return Result.ok(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("removeChunk")
|
@PostMapping("removeChunk")
|
||||||
@SaCheckPermission("/api/v1/documentCollection/remove")
|
@SaCheckPermission("/api/v1/documentCollection/remove")
|
||||||
|
@RequireResourceAccess(
|
||||||
|
resource = CategoryResourceType.KNOWLEDGE,
|
||||||
|
action = ResourceAction.MANAGE,
|
||||||
|
lookup = ResourceLookup.DOCUMENT_CHUNK_ID,
|
||||||
|
idExpr = "#chunkId",
|
||||||
|
denyMessage = "无权限管理知识库"
|
||||||
|
)
|
||||||
public Result<?> remove(@JsonBody(value = "id", required = true) BigInteger chunkId) {
|
public Result<?> remove(@JsonBody(value = "id", required = true) BigInteger chunkId) {
|
||||||
DocumentChunk docChunk = documentChunkService.getById(chunkId);
|
DocumentChunk docChunk = documentChunkService.getById(chunkId);
|
||||||
if (docChunk == null) {
|
if (docChunk == null) {
|
||||||
@@ -100,19 +140,23 @@ public class DocumentChunkController extends BaseCurdController<DocumentChunkSer
|
|||||||
if (documentStore == null) {
|
if (documentStore == null) {
|
||||||
return Result.fail(3, "知识库没有配置向量库");
|
return Result.fail(3, "知识库没有配置向量库");
|
||||||
}
|
}
|
||||||
// 设置向量模型
|
try {
|
||||||
Model model = modelService.getModelInstance(knowledge.getVectorEmbedModelId());
|
// 设置向量模型
|
||||||
if (model == null) {
|
Model model = modelService.getModelInstance(knowledge.getVectorEmbedModelId());
|
||||||
return Result.fail(4, "知识库没有配置向量模型");
|
if (model == null) {
|
||||||
}
|
return Result.fail(4, "知识库没有配置向量模型");
|
||||||
EmbeddingModel embeddingModel = model.toEmbeddingModel();
|
}
|
||||||
documentStore.setEmbeddingModel(embeddingModel);
|
EmbeddingModel embeddingModel = model.toEmbeddingModel();
|
||||||
StoreOptions options = StoreOptions.ofCollectionName(knowledge.getVectorStoreCollection());
|
documentStore.setEmbeddingModel(embeddingModel);
|
||||||
List<BigInteger> deleteList = new ArrayList<>();
|
StoreOptions options = StoreOptions.ofCollectionName(knowledge.getVectorStoreCollection());
|
||||||
deleteList.add(chunkId);
|
List<BigInteger> deleteList = new ArrayList<>();
|
||||||
documentStore.delete(deleteList, options);
|
deleteList.add(chunkId);
|
||||||
documentChunkService.removeChunk(knowledge, chunkId);
|
documentStore.delete(deleteList, options);
|
||||||
|
documentChunkService.removeChunk(knowledge, chunkId);
|
||||||
|
|
||||||
return super.remove(chunkId);
|
return super.remove(chunkId);
|
||||||
|
} finally {
|
||||||
|
DocumentStoreLifecycleSupport.closeQuietly(documentStore);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,17 +3,17 @@ package tech.easyflow.admin.controller.ai;
|
|||||||
import com.mybatisflex.core.query.QueryWrapper;
|
import com.mybatisflex.core.query.QueryWrapper;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import tech.easyflow.ai.entity.DocumentCollection;
|
import tech.easyflow.ai.entity.DocumentCollection;
|
||||||
import tech.easyflow.ai.entity.DocumentCollectionCategory;
|
import tech.easyflow.ai.entity.DocumentCollectionCategory;
|
||||||
import tech.easyflow.ai.entity.WorkflowCategory;
|
|
||||||
import tech.easyflow.ai.mapper.DocumentCollectionMapper;
|
import tech.easyflow.ai.mapper.DocumentCollectionMapper;
|
||||||
import tech.easyflow.ai.service.DocumentCollectionCategoryService;
|
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.annotation.UsePermission;
|
||||||
import tech.easyflow.common.domain.Result;
|
import tech.easyflow.common.domain.Result;
|
||||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||||
|
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
|
||||||
|
import tech.easyflow.system.service.CategoryPermissionService;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
@@ -34,6 +34,8 @@ public class DocumentCollectionCategoryController extends BaseCurdController<Doc
|
|||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private DocumentCollectionMapper documentCollectionMapper;
|
private DocumentCollectionMapper documentCollectionMapper;
|
||||||
|
@Resource
|
||||||
|
private CategoryPermissionService categoryPermissionService;
|
||||||
|
|
||||||
public DocumentCollectionCategoryController(DocumentCollectionCategoryService service) {
|
public DocumentCollectionCategoryController(DocumentCollectionCategoryService service) {
|
||||||
super(service);
|
super(service);
|
||||||
@@ -51,4 +53,18 @@ public class DocumentCollectionCategoryController extends BaseCurdController<Doc
|
|||||||
|
|
||||||
return super.onRemoveBefore(ids);
|
return super.onRemoveBefore(ids);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
@GetMapping("visibleList")
|
||||||
|
public Result<List<DocumentCollectionCategory>> visibleList(DocumentCollectionCategory entity, Boolean asTree, String sortKey, String sortType) {
|
||||||
|
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
|
||||||
|
RoleCategoryAccessSnapshot access = categoryPermissionService.getCurrentAccess("KNOWLEDGE");
|
||||||
|
if (access.isRestricted()) {
|
||||||
|
if (access.getCategoryIds().isEmpty()) {
|
||||||
|
return Result.ok(Collections.emptyList());
|
||||||
|
}
|
||||||
|
queryWrapper.in(DocumentCollectionCategory::getId, access.getCategoryIds());
|
||||||
|
}
|
||||||
|
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||||
|
return Result.ok(service.list(queryWrapper));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,14 +2,31 @@ package tech.easyflow.admin.controller.ai;
|
|||||||
|
|
||||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
import com.easyagents.core.document.Document;
|
import com.easyagents.core.document.Document;
|
||||||
|
import com.easyagents.rag.retrieval.RagRetrievalMetadataKeys;
|
||||||
|
import com.mybatisflex.core.paginate.Page;
|
||||||
import com.mybatisflex.core.query.QueryWrapper;
|
import com.mybatisflex.core.query.QueryWrapper;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
import org.springframework.web.context.request.RequestContextHolder;
|
||||||
|
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import tech.easyflow.admin.controller.ai.support.AiResourceCreatorNameSupport;
|
||||||
|
import tech.easyflow.ai.permission.KnowledgeVisibilityQueryHelper;
|
||||||
|
import tech.easyflow.ai.documentimport.DocumentImportDtos;
|
||||||
|
import tech.easyflow.ai.dto.KnowledgeSearchResultItem;
|
||||||
import tech.easyflow.ai.entity.BotDocumentCollection;
|
import tech.easyflow.ai.entity.BotDocumentCollection;
|
||||||
import tech.easyflow.ai.entity.DocumentCollection;
|
import tech.easyflow.ai.entity.DocumentCollection;
|
||||||
|
import tech.easyflow.ai.entity.Model;
|
||||||
|
import tech.easyflow.ai.enums.PublishStatus;
|
||||||
|
import tech.easyflow.ai.publish.KnowledgePublishAppService;
|
||||||
|
import tech.easyflow.ai.service.AiResourceApprovalStateService;
|
||||||
|
import tech.easyflow.ai.vo.OfflineImpactCheckVo;
|
||||||
|
import tech.easyflow.approval.entity.vo.ApprovalActionResult;
|
||||||
|
import tech.easyflow.ai.rag.KnowledgeRetrievalRequest;
|
||||||
|
import tech.easyflow.ai.rag.KnowledgeRetrievalModes;
|
||||||
import tech.easyflow.ai.service.BotDocumentCollectionService;
|
import tech.easyflow.ai.service.BotDocumentCollectionService;
|
||||||
import tech.easyflow.ai.service.DocumentChunkService;
|
import tech.easyflow.ai.service.DocumentChunkService;
|
||||||
import tech.easyflow.ai.service.DocumentCollectionService;
|
import tech.easyflow.ai.service.DocumentCollectionService;
|
||||||
@@ -17,14 +34,25 @@ import tech.easyflow.ai.service.ModelService;
|
|||||||
import tech.easyflow.common.domain.Result;
|
import tech.easyflow.common.domain.Result;
|
||||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||||
|
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||||
|
import tech.easyflow.system.enums.CategoryResourceType;
|
||||||
|
import tech.easyflow.system.enums.ResourceAction;
|
||||||
|
import tech.easyflow.system.enums.ResourceLookup;
|
||||||
|
import tech.easyflow.system.enums.VisibilityScope;
|
||||||
|
import tech.easyflow.system.permission.resource.RequireResourceAccess;
|
||||||
|
import tech.easyflow.system.service.ResourceAccessService;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
|
import java.math.RoundingMode;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 控制层。
|
* 控制层。
|
||||||
@@ -41,6 +69,16 @@ public class DocumentCollectionController extends BaseCurdController<DocumentCol
|
|||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private BotDocumentCollectionService botDocumentCollectionService;
|
private BotDocumentCollectionService botDocumentCollectionService;
|
||||||
|
@Resource
|
||||||
|
private ResourceAccessService resourceAccessService;
|
||||||
|
@Resource
|
||||||
|
private KnowledgeVisibilityQueryHelper knowledgeVisibilityQueryHelper;
|
||||||
|
@Resource
|
||||||
|
private KnowledgePublishAppService knowledgePublishAppService;
|
||||||
|
@Resource
|
||||||
|
private AiResourceApprovalStateService aiResourceApprovalStateService;
|
||||||
|
@Resource
|
||||||
|
private AiResourceCreatorNameSupport aiResourceCreatorNameSupport;
|
||||||
|
|
||||||
public DocumentCollectionController(DocumentCollectionService service, DocumentChunkService chunkService, ModelService llmService) {
|
public DocumentCollectionController(DocumentCollectionService service, DocumentChunkService chunkService, ModelService llmService) {
|
||||||
super(service);
|
super(service);
|
||||||
@@ -50,6 +88,11 @@ public class DocumentCollectionController extends BaseCurdController<DocumentCol
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Result<?> onSaveOrUpdateBefore(DocumentCollection entity, boolean isSave) {
|
protected Result<?> onSaveOrUpdateBefore(DocumentCollection entity, boolean isSave) {
|
||||||
|
normalizeVisibilityScope(entity, isSave);
|
||||||
|
if (!isSave && entity.getId() != null) {
|
||||||
|
DocumentCollection existed = requireKnowledge(String.valueOf(entity.getId()));
|
||||||
|
resourceAccessService.assertAccess(CategoryResourceType.KNOWLEDGE, existed, ResourceAction.MANAGE, "无权限管理知识库");
|
||||||
|
}
|
||||||
|
|
||||||
String alias = entity.getAlias();
|
String alias = entity.getAlias();
|
||||||
String collectionType = entity.getCollectionType();
|
String collectionType = entity.getCollectionType();
|
||||||
@@ -84,25 +127,46 @@ public class DocumentCollectionController extends BaseCurdController<DocumentCol
|
|||||||
Map<String, Object> options = entity.getOptions() == null
|
Map<String, Object> options = entity.getOptions() == null
|
||||||
? new HashMap<>()
|
? new HashMap<>()
|
||||||
: new HashMap<>(entity.getOptions());
|
: new HashMap<>(entity.getOptions());
|
||||||
if (entity.getSearchEngineEnable() == null){
|
|
||||||
entity.setSearchEngineEnable(false);
|
|
||||||
}
|
|
||||||
options.putIfAbsent(DocumentCollection.KEY_CAN_UPDATE_EMBEDDING_MODEL, true);
|
options.putIfAbsent(DocumentCollection.KEY_CAN_UPDATE_EMBEDDING_MODEL, true);
|
||||||
options.putIfAbsent(DocumentCollection.KEY_RERANK_ENABLE, entity.getRerankModelId() != null);
|
options.putIfAbsent(DocumentCollection.KEY_RERANK_ENABLE, entity.getRerankModelId() != null);
|
||||||
entity.setOptions(options);
|
entity.setOptions(options);
|
||||||
}
|
}
|
||||||
|
normalizeInfrastructureFields(entity, isSave);
|
||||||
return super.onSaveOrUpdateBefore(entity, isSave);
|
return super.onSaveOrUpdateBefore(entity, isSave);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("search")
|
@GetMapping("search")
|
||||||
@SaCheckPermission("/api/v1/documentCollection/query")
|
@SaCheckPermission("/api/v1/documentCollection/query")
|
||||||
public Result<List<Document>> search(@RequestParam BigInteger knowledgeId, @RequestParam String keyword) {
|
@RequireResourceAccess(
|
||||||
return Result.ok(service.search(knowledgeId, keyword));
|
resource = CategoryResourceType.KNOWLEDGE,
|
||||||
|
action = ResourceAction.READ,
|
||||||
|
lookup = ResourceLookup.KNOWLEDGE_ID,
|
||||||
|
idExpr = "#knowledgeId",
|
||||||
|
denyMessage = "无权限访问知识库"
|
||||||
|
)
|
||||||
|
public Result<List<KnowledgeSearchResultItem>> search(@RequestParam BigInteger knowledgeId,
|
||||||
|
@RequestParam String keyword,
|
||||||
|
@RequestParam(required = false) String retrievalMode,
|
||||||
|
@RequestParam(required = false) Integer docRecallMaxNum,
|
||||||
|
@RequestParam(required = false) Double simThreshold) {
|
||||||
|
KnowledgeRetrievalRequest request = new KnowledgeRetrievalRequest();
|
||||||
|
request.setKnowledgeId(knowledgeId);
|
||||||
|
request.setQuery(keyword);
|
||||||
|
request.setLimit(docRecallMaxNum);
|
||||||
|
request.setMinSimilarity(simThreshold);
|
||||||
|
request.setRetrievalMode(KnowledgeRetrievalModes.parse(retrievalMode));
|
||||||
|
request.setCallerType("API");
|
||||||
|
request.setCallerId(String.valueOf(knowledgeId));
|
||||||
|
return Result.ok(toKnowledgeSearchResult(service.search(request)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Result<Void> onRemoveBefore(Collection<Serializable> ids) {
|
protected Result<Void> onRemoveBefore(Collection<Serializable> ids) {
|
||||||
|
for (Serializable id : ids) {
|
||||||
|
DocumentCollection collection = requireKnowledge(String.valueOf(id));
|
||||||
|
resourceAccessService.assertAccess(CategoryResourceType.KNOWLEDGE, collection, ResourceAction.MANAGE, "无权限管理知识库");
|
||||||
|
}
|
||||||
|
|
||||||
QueryWrapper queryWrapper = QueryWrapper.create();
|
QueryWrapper queryWrapper = QueryWrapper.create();
|
||||||
queryWrapper.in(BotDocumentCollection::getDocumentCollectionId, ids);
|
queryWrapper.in(BotDocumentCollection::getDocumentCollectionId, ids);
|
||||||
@@ -116,7 +180,274 @@ public class DocumentCollectionController extends BaseCurdController<DocumentCol
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@RequireResourceAccess(
|
||||||
|
resource = CategoryResourceType.KNOWLEDGE,
|
||||||
|
action = ResourceAction.READ,
|
||||||
|
lookup = ResourceLookup.KNOWLEDGE_ID_OR_SLUG,
|
||||||
|
idExpr = "#id",
|
||||||
|
denyMessage = "无权限访问知识库"
|
||||||
|
)
|
||||||
public Result<DocumentCollection> detail(String id) {
|
public Result<DocumentCollection> detail(String id) {
|
||||||
return Result.ok(service.getDetail(id));
|
DocumentCollection detail = service.getDetail(id);
|
||||||
|
aiResourceApprovalStateService.fillKnowledgeApprovalState(detail);
|
||||||
|
return Result.ok(detail);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("modelList")
|
||||||
|
@SaCheckPermission("/api/v1/documentCollection/query")
|
||||||
|
public Result<List<Model>> modelList(Model entity, Boolean asTree, String sortKey, String sortType) {
|
||||||
|
return Result.ok(llmService.listSelectableModels(entity, asTree, sortKey, sortType));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交发布审批。
|
||||||
|
*
|
||||||
|
* @param id 知识库 ID
|
||||||
|
* @return 审批实例 ID
|
||||||
|
*/
|
||||||
|
@PostMapping("/submitPublishApproval")
|
||||||
|
@SaCheckPermission("/api/v1/documentCollection/save")
|
||||||
|
public Result<BigInteger> submitPublishApproval(@JsonBody("id") BigInteger id) {
|
||||||
|
return buildApprovalActionResult(
|
||||||
|
knowledgePublishAppService.submitPublishApproval(id),
|
||||||
|
"已提交发布审批",
|
||||||
|
"已直接发布"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交下线审批。
|
||||||
|
*
|
||||||
|
* @param id 知识库 ID
|
||||||
|
* @return 审批实例 ID
|
||||||
|
*/
|
||||||
|
@PostMapping("/submitOfflineApproval")
|
||||||
|
@SaCheckPermission("/api/v1/documentCollection/save")
|
||||||
|
public Result<BigInteger> submitOfflineApproval(@JsonBody("id") BigInteger id) {
|
||||||
|
return buildApprovalActionResult(
|
||||||
|
knowledgePublishAppService.submitOfflineApproval(id),
|
||||||
|
"已提交下线审批",
|
||||||
|
"已直接下线"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查知识库下线影响。
|
||||||
|
*
|
||||||
|
* @param id 知识库 ID
|
||||||
|
* @return 下线影响结果
|
||||||
|
*/
|
||||||
|
@GetMapping("/offlineImpactCheck")
|
||||||
|
@SaCheckPermission("/api/v1/documentCollection/save")
|
||||||
|
@RequireResourceAccess(
|
||||||
|
resource = CategoryResourceType.KNOWLEDGE,
|
||||||
|
action = ResourceAction.MANAGE,
|
||||||
|
lookup = ResourceLookup.KNOWLEDGE_ID,
|
||||||
|
idExpr = "#id",
|
||||||
|
denyMessage = "无权限管理知识库"
|
||||||
|
)
|
||||||
|
public Result<OfflineImpactCheckVo> offlineImpactCheck(@RequestParam BigInteger id) {
|
||||||
|
return Result.ok(knowledgePublishAppService.checkOfflineImpact(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交删除审批。
|
||||||
|
*
|
||||||
|
* @param id 知识库 ID
|
||||||
|
* @return 审批实例 ID
|
||||||
|
*/
|
||||||
|
@PostMapping("/submitDeleteApproval")
|
||||||
|
@SaCheckPermission("/api/v1/documentCollection/remove")
|
||||||
|
public Result<BigInteger> submitDeleteApproval(@JsonBody("id") BigInteger id) {
|
||||||
|
return buildApprovalActionResult(
|
||||||
|
knowledgePublishAppService.submitDeleteApproval(id),
|
||||||
|
"已提交删除审批",
|
||||||
|
"已直接删除"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("splitterProfile/save")
|
||||||
|
@SaCheckPermission("/api/v1/documentCollection/save")
|
||||||
|
@RequireResourceAccess(
|
||||||
|
resource = CategoryResourceType.KNOWLEDGE,
|
||||||
|
action = ResourceAction.MANAGE,
|
||||||
|
lookup = ResourceLookup.KNOWLEDGE_ID,
|
||||||
|
idExpr = "#request.knowledgeId",
|
||||||
|
denyMessage = "无权限管理知识库"
|
||||||
|
)
|
||||||
|
public Result<Boolean> saveSplitterProfile(@JsonBody DocumentImportDtos.SplitterProfileSaveRequest request) {
|
||||||
|
if (request.getKnowledgeId() == null) {
|
||||||
|
throw new BusinessException("知识库ID不能为空");
|
||||||
|
}
|
||||||
|
DocumentCollection collection = service.getById(request.getKnowledgeId());
|
||||||
|
if (collection == null) {
|
||||||
|
throw new BusinessException("知识库不存在");
|
||||||
|
}
|
||||||
|
if (collection.isFaqCollection()) {
|
||||||
|
throw new BusinessException("FAQ知识库不支持文档导入策略");
|
||||||
|
}
|
||||||
|
Map<String, Object> options = collection.getOptions() == null
|
||||||
|
? new HashMap<>()
|
||||||
|
: new HashMap<>(collection.getOptions());
|
||||||
|
options.put(DocumentCollection.KEY_SPLITTER_DEFAULT_STRATEGY, request.getDefaultStrategyCode());
|
||||||
|
options.put(DocumentCollection.KEY_SPLITTER_AUTO_RECOMMEND_ENABLED, request.getAutoRecommendEnabled());
|
||||||
|
options.put(DocumentCollection.KEY_SPLITTER_FALLBACK_STRATEGY, request.getFallbackStrategyCode());
|
||||||
|
options.put(DocumentCollection.KEY_SPLITTER_STRATEGY_PROFILES, request.getStrategyProfiles());
|
||||||
|
|
||||||
|
DocumentCollection update = new DocumentCollection();
|
||||||
|
update.setId(collection.getId());
|
||||||
|
update.setOptions(options);
|
||||||
|
return Result.ok(service.updateById(update));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Result<List<DocumentCollection>> list(DocumentCollection entity, Boolean asTree, String sortKey, String sortType) {
|
||||||
|
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
|
||||||
|
knowledgeVisibilityQueryHelper.applyReadableAccess(queryWrapper);
|
||||||
|
applyPublishedOnlyFilter(queryWrapper);
|
||||||
|
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||||
|
List<DocumentCollection> collections = service.list(queryWrapper);
|
||||||
|
aiResourceApprovalStateService.fillKnowledgeApprovalState(collections);
|
||||||
|
return Result.ok(collections);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Page<DocumentCollection> queryPage(Page<DocumentCollection> page, QueryWrapper queryWrapper) {
|
||||||
|
knowledgeVisibilityQueryHelper.applyReadableAccess(queryWrapper);
|
||||||
|
applyPublishedOnlyFilter(queryWrapper);
|
||||||
|
Page<DocumentCollection> result = super.queryPage(page, queryWrapper);
|
||||||
|
aiResourceApprovalStateService.fillKnowledgeApprovalState(result.getRecords());
|
||||||
|
aiResourceCreatorNameSupport.fillDocumentCollectionCreatorNames(result.getRecords());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@PostMapping("remove")
|
||||||
|
public Result<?> remove(@JsonBody(value = "id", required = true) Serializable id) {
|
||||||
|
return Result.fail(1, "请使用发布状态操作删除知识库");
|
||||||
|
}
|
||||||
|
|
||||||
|
private Result<BigInteger> buildApprovalActionResult(ApprovalActionResult actionResult,
|
||||||
|
String approvalMessage,
|
||||||
|
String directMessage) {
|
||||||
|
return Result.ok(
|
||||||
|
actionResult.isApprovalRequired() ? approvalMessage : directMessage,
|
||||||
|
actionResult.getInstanceId()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void normalizeVisibilityScope(DocumentCollection entity, boolean isSave) {
|
||||||
|
if (entity == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!StringUtils.hasLength(entity.getVisibilityScope())) {
|
||||||
|
if (isSave) {
|
||||||
|
entity.setVisibilityScope(VisibilityScope.PRIVATE.name());
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
entity.setVisibilityScope(VisibilityScope.from(entity.getVisibilityScope()).name());
|
||||||
|
}
|
||||||
|
|
||||||
|
private DocumentCollection requireKnowledge(String idOrAlias) {
|
||||||
|
DocumentCollection collection = service.getDetail(idOrAlias);
|
||||||
|
if (collection == null) {
|
||||||
|
throw new BusinessException("知识库不存在");
|
||||||
|
}
|
||||||
|
return collection;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void normalizeInfrastructureFields(DocumentCollection entity, boolean isSave) {
|
||||||
|
if (entity == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isSave) {
|
||||||
|
entity.setVectorStoreEnable(true);
|
||||||
|
entity.setVectorStoreType("milvus");
|
||||||
|
entity.setSearchEngineEnable(true);
|
||||||
|
entity.setVectorStoreCollection(generateVectorCollectionName());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (entity.getVectorStoreEnable() != null) {
|
||||||
|
entity.setVectorStoreEnable(true);
|
||||||
|
}
|
||||||
|
if (entity.getVectorStoreType() != null) {
|
||||||
|
entity.setVectorStoreType("milvus");
|
||||||
|
}
|
||||||
|
if (entity.getSearchEngineEnable() != null) {
|
||||||
|
entity.setSearchEngineEnable(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String generateVectorCollectionName() {
|
||||||
|
return "kb_" + UUID.randomUUID().toString().replace("-", "").substring(0, 28);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyPublishedOnlyFilter(QueryWrapper queryWrapper) {
|
||||||
|
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||||
|
if (attributes == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String publishedOnly = attributes.getRequest().getParameter("publishedOnly");
|
||||||
|
if ("true".equalsIgnoreCase(publishedOnly)) {
|
||||||
|
queryWrapper.eq("publish_status", PublishStatus.PUBLISHED.getCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<KnowledgeSearchResultItem> toKnowledgeSearchResult(List<Document> documents) {
|
||||||
|
List<KnowledgeSearchResultItem> results = new ArrayList<>();
|
||||||
|
if (documents == null) {
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
for (int index = 0; index < documents.size(); index++) {
|
||||||
|
Document document = documents.get(index);
|
||||||
|
if (document == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
KnowledgeSearchResultItem item = new KnowledgeSearchResultItem();
|
||||||
|
item.setSorting(index + 1);
|
||||||
|
item.setContent(document.getContent());
|
||||||
|
item.setRenderMarkdown(readMetadataAsString(document, "renderMarkdown"));
|
||||||
|
item.setSourceFileName(readMetadataAsString(document, "sourceFileName"));
|
||||||
|
item.setScore(roundScore(document.getScore()));
|
||||||
|
item.setHitSource(readMetadataAsString(document, RagRetrievalMetadataKeys.HIT_SOURCE));
|
||||||
|
item.setVectorScore(roundScore(readMetadataAsDouble(document, RagRetrievalMetadataKeys.VECTOR_SCORE)));
|
||||||
|
item.setKeywordScore(roundScore(readMetadataAsDouble(document, RagRetrievalMetadataKeys.KEYWORD_SCORE)));
|
||||||
|
results.add(item);
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String readMetadataAsString(Document document, String key) {
|
||||||
|
Object value = document == null ? null : document.getMetadata(key);
|
||||||
|
if (value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String text = String.valueOf(value);
|
||||||
|
return StringUtils.hasText(text) ? text : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Double readMetadataAsDouble(Document document, String key) {
|
||||||
|
Object value = document == null ? null : document.getMetadata(key);
|
||||||
|
if (value instanceof Number) {
|
||||||
|
return ((Number) value).doubleValue();
|
||||||
|
}
|
||||||
|
if (value instanceof String && StringUtils.hasText((String) value)) {
|
||||||
|
try {
|
||||||
|
return Double.valueOf((String) value);
|
||||||
|
} catch (NumberFormatException ignore) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Double roundScore(Double value) {
|
||||||
|
if (value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new BigDecimal(String.valueOf(value))
|
||||||
|
.setScale(4, RoundingMode.HALF_UP)
|
||||||
|
.doubleValue();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
package tech.easyflow.admin.controller.ai;
|
package tech.easyflow.admin.controller.ai;
|
||||||
|
|
||||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
|
import cn.hutool.core.io.IoUtil;
|
||||||
import com.mybatisflex.core.paginate.Page;
|
import com.mybatisflex.core.paginate.Page;
|
||||||
import com.mybatisflex.core.query.QueryWrapper;
|
import com.mybatisflex.core.query.QueryWrapper;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.core.io.ClassPathResource;
|
import org.springframework.core.io.ClassPathResource;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
|
import tech.easyflow.ai.documentimport.DocumentImportDtos;
|
||||||
|
import tech.easyflow.ai.documentimport.task.DocumentImportTaskStatusStreamService;
|
||||||
import tech.easyflow.ai.entity.Document;
|
import tech.easyflow.ai.entity.Document;
|
||||||
import tech.easyflow.ai.entity.DocumentCollection;
|
import tech.easyflow.ai.entity.DocumentCollection;
|
||||||
import tech.easyflow.ai.entity.DocumentCollectionSplitParams;
|
import tech.easyflow.ai.entity.DocumentCollectionSplitParams;
|
||||||
@@ -24,11 +30,21 @@ import tech.easyflow.common.util.StringUtil;
|
|||||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||||
|
import tech.easyflow.common.filestorage.FileStorageService;
|
||||||
|
import tech.easyflow.system.enums.CategoryResourceType;
|
||||||
|
import tech.easyflow.system.enums.ResourceAction;
|
||||||
|
import tech.easyflow.system.enums.ResourceLookup;
|
||||||
|
import tech.easyflow.system.permission.resource.RequireResourceAccess;
|
||||||
|
import tech.easyflow.system.service.ResourceAccessService;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
@@ -58,6 +74,14 @@ public class DocumentController extends BaseCurdController<DocumentService, Docu
|
|||||||
@Autowired
|
@Autowired
|
||||||
private RedisLockExecutor redisLockExecutor;
|
private RedisLockExecutor redisLockExecutor;
|
||||||
|
|
||||||
|
@Resource(name = "default")
|
||||||
|
private FileStorageService storageService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ResourceAccessService resourceAccessService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private DocumentImportTaskStatusStreamService documentImportTaskStatusStreamService;
|
||||||
|
|
||||||
@Value("${easyflow.storage.local.root:}")
|
@Value("${easyflow.storage.local.root:}")
|
||||||
private String fileUploadPath;
|
private String fileUploadPath;
|
||||||
@@ -73,6 +97,8 @@ public class DocumentController extends BaseCurdController<DocumentService, Docu
|
|||||||
@Transactional
|
@Transactional
|
||||||
@SaCheckPermission("/api/v1/documentCollection/remove")
|
@SaCheckPermission("/api/v1/documentCollection/remove")
|
||||||
public Result<?> remove(@JsonBody(value = "id", required = true) String id) {
|
public Result<?> remove(@JsonBody(value = "id", required = true) String id) {
|
||||||
|
Document document = requireDocument(new BigInteger(id));
|
||||||
|
getDocumentCollection(document.getCollectionId().toString(), ResourceAction.MANAGE, "无权限管理知识库");
|
||||||
List<Serializable> ids = Collections.singletonList(id);
|
List<Serializable> ids = Collections.singletonList(id);
|
||||||
Result<?> result = onRemoveBefore(ids);
|
Result<?> result = onRemoveBefore(ids);
|
||||||
if (result != null) return result;
|
if (result != null) return result;
|
||||||
@@ -104,7 +130,7 @@ public class DocumentController extends BaseCurdController<DocumentService, Docu
|
|||||||
throw new BusinessException("知识库id不能为空");
|
throw new BusinessException("知识库id不能为空");
|
||||||
}
|
}
|
||||||
|
|
||||||
DocumentCollection knowledge = getDocumentCollection(kbSlug);
|
DocumentCollection knowledge = getDocumentCollection(kbSlug, ResourceAction.READ, "无权限访问知识库");
|
||||||
|
|
||||||
QueryWrapper queryWrapper = QueryWrapper.create()
|
QueryWrapper queryWrapper = QueryWrapper.create()
|
||||||
.eq(Document::getCollectionId, knowledge.getId());
|
.eq(Document::getCollectionId, knowledge.getId());
|
||||||
@@ -121,11 +147,33 @@ public class DocumentController extends BaseCurdController<DocumentService, Docu
|
|||||||
if (StringUtil.noText(kbSlug)) {
|
if (StringUtil.noText(kbSlug)) {
|
||||||
throw new BusinessException("知识库id不能为空");
|
throw new BusinessException("知识库id不能为空");
|
||||||
}
|
}
|
||||||
DocumentCollection knowledge = getDocumentCollection(kbSlug);
|
DocumentCollection knowledge = getDocumentCollection(kbSlug, ResourceAction.READ, "无权限访问知识库");
|
||||||
Page<Document> documentList = documentService.getDocumentList(knowledge.getId().toString(), pageSize, pageNumber,fileName);
|
Page<Document> documentList = documentService.getDocumentList(knowledge.getId().toString(), pageSize, pageNumber,fileName);
|
||||||
return Result.ok(documentList);
|
return Result.ok(documentList);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("download")
|
||||||
|
@SaCheckPermission("/api/v1/documentCollection/query")
|
||||||
|
@RequireResourceAccess(
|
||||||
|
resource = CategoryResourceType.KNOWLEDGE,
|
||||||
|
action = ResourceAction.READ,
|
||||||
|
lookup = ResourceLookup.DOCUMENT_ID,
|
||||||
|
idExpr = "#documentId",
|
||||||
|
denyMessage = "无权限访问知识库"
|
||||||
|
)
|
||||||
|
public void download(@RequestParam BigInteger documentId, HttpServletResponse response) throws IOException {
|
||||||
|
Document document = requireDocument(documentId);
|
||||||
|
String fileName = resolveDownloadFileName(document);
|
||||||
|
response.setContentType("application/octet-stream");
|
||||||
|
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
|
||||||
|
String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8).replaceAll("\\+", "%20");
|
||||||
|
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + encodedFileName);
|
||||||
|
try (InputStream inputStream = storageService.readStream(document.getDocumentPath())) {
|
||||||
|
IoUtil.copy(inputStream, response.getOutputStream());
|
||||||
|
response.flushBuffer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected String getDefaultOrderBy() {
|
protected String getDefaultOrderBy() {
|
||||||
@@ -138,6 +186,11 @@ public class DocumentController extends BaseCurdController<DocumentService, Docu
|
|||||||
@Transactional
|
@Transactional
|
||||||
@SaCheckPermission("/api/v1/documentCollection/save")
|
@SaCheckPermission("/api/v1/documentCollection/save")
|
||||||
public Result<Boolean> update(@JsonBody Document entity) {
|
public Result<Boolean> update(@JsonBody Document entity) {
|
||||||
|
if (entity.getId() == null) {
|
||||||
|
throw new BusinessException("文档不存在");
|
||||||
|
}
|
||||||
|
Document current = requireDocument(entity.getId());
|
||||||
|
getDocumentCollection(current.getCollectionId().toString(), ResourceAction.MANAGE, "无权限管理知识库");
|
||||||
super.update(entity);
|
super.update(entity);
|
||||||
return Result.ok(updatePosition(entity));
|
return Result.ok(updatePosition(entity));
|
||||||
}
|
}
|
||||||
@@ -152,10 +205,113 @@ public class DocumentController extends BaseCurdController<DocumentService, Docu
|
|||||||
if (documentCollectionSplitParams.getKnowledgeId() == null) {
|
if (documentCollectionSplitParams.getKnowledgeId() == null) {
|
||||||
throw new BusinessException("知识库id不能为空");
|
throw new BusinessException("知识库id不能为空");
|
||||||
}
|
}
|
||||||
getDocumentCollection(documentCollectionSplitParams.getKnowledgeId().toString());
|
getDocumentCollection(documentCollectionSplitParams.getKnowledgeId().toString(), ResourceAction.MANAGE, "无权限管理知识库");
|
||||||
return documentService.textSplit(documentCollectionSplitParams);
|
return documentService.textSplit(documentCollectionSplitParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("import/analyze")
|
||||||
|
@SaCheckPermission("/api/v1/documentCollection/save")
|
||||||
|
public Result<DocumentImportDtos.AnalyzeResponse> analyzeImport(@JsonBody DocumentImportDtos.AnalyzeRequest request) {
|
||||||
|
if (request.getKnowledgeId() == null) {
|
||||||
|
throw new BusinessException("知识库id不能为空");
|
||||||
|
}
|
||||||
|
getDocumentCollection(request.getKnowledgeId().toString(), ResourceAction.MANAGE, "无权限管理知识库");
|
||||||
|
return documentService.analyzeImport(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("import/preview")
|
||||||
|
@SaCheckPermission("/api/v1/documentCollection/save")
|
||||||
|
public Result<DocumentImportDtos.PreviewResponse> previewImport(@JsonBody DocumentImportDtos.PreviewRequest request) {
|
||||||
|
if (request.getKnowledgeId() == null) {
|
||||||
|
throw new BusinessException("知识库id不能为空");
|
||||||
|
}
|
||||||
|
getDocumentCollection(request.getKnowledgeId().toString(), ResourceAction.MANAGE, "无权限管理知识库");
|
||||||
|
return documentService.previewImport(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("import/commit")
|
||||||
|
@SaCheckPermission("/api/v1/documentCollection/save")
|
||||||
|
public Result<DocumentImportDtos.CommitResponse> commitImport(@JsonBody DocumentImportDtos.CommitRequest request) {
|
||||||
|
if (request.getKnowledgeId() == null) {
|
||||||
|
throw new BusinessException("知识库id不能为空");
|
||||||
|
}
|
||||||
|
getDocumentCollection(request.getKnowledgeId().toString(), ResourceAction.MANAGE, "无权限管理知识库");
|
||||||
|
return documentService.commitImport(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("import/task/create")
|
||||||
|
@SaCheckPermission("/api/v1/documentCollection/save")
|
||||||
|
public Result<DocumentImportDtos.TaskCreateResponse> createImportTask(@JsonBody DocumentImportDtos.TaskCreateRequest request) {
|
||||||
|
if (request.getKnowledgeId() == null) {
|
||||||
|
throw new BusinessException("知识库id不能为空");
|
||||||
|
}
|
||||||
|
getDocumentCollection(request.getKnowledgeId().toString(), ResourceAction.MANAGE, "无权限管理知识库");
|
||||||
|
return documentService.createImportTask(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("import/task/detail")
|
||||||
|
@SaCheckPermission("/api/v1/documentCollection/query")
|
||||||
|
public Result<DocumentImportDtos.TaskDetailResponse> getImportTaskDetail(@RequestParam BigInteger taskId) {
|
||||||
|
Result<DocumentImportDtos.TaskDetailResponse> result = documentService.getImportTaskDetail(taskId);
|
||||||
|
if (result.getData() != null && result.getData().getKnowledgeId() != null) {
|
||||||
|
getDocumentCollection(result.getData().getKnowledgeId().toString(), ResourceAction.READ, "无权限访问知识库");
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订阅知识库文档任务状态流。
|
||||||
|
*
|
||||||
|
* @param knowledgeId 知识库 ID
|
||||||
|
* @return SSE 推送连接
|
||||||
|
*/
|
||||||
|
@PostMapping(value = "import/task/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||||
|
@SaCheckPermission("/api/v1/documentCollection/query")
|
||||||
|
public SseEmitter streamImportTask(@JsonBody(value = "knowledgeId", required = true) BigInteger knowledgeId) {
|
||||||
|
getDocumentCollection(knowledgeId.toString(), ResourceAction.READ, "无权限访问知识库");
|
||||||
|
return documentImportTaskStatusStreamService.subscribe(knowledgeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("import/task/preview")
|
||||||
|
@SaCheckPermission("/api/v1/documentCollection/save")
|
||||||
|
public Result<DocumentImportDtos.PreviewResponse> previewImportTask(@JsonBody DocumentImportDtos.PreviewRequest request) {
|
||||||
|
if (request.getKnowledgeId() == null) {
|
||||||
|
throw new BusinessException("知识库id不能为空");
|
||||||
|
}
|
||||||
|
getDocumentCollection(request.getKnowledgeId().toString(), ResourceAction.MANAGE, "无权限管理知识库");
|
||||||
|
return documentService.previewImportTask(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("import/task/startIndex")
|
||||||
|
@SaCheckPermission("/api/v1/documentCollection/save")
|
||||||
|
public Result<DocumentImportDtos.TaskStartIndexResponse> startIndexTask(@JsonBody DocumentImportDtos.TaskStartIndexRequest request) {
|
||||||
|
if (request.getKnowledgeId() == null) {
|
||||||
|
throw new BusinessException("知识库id不能为空");
|
||||||
|
}
|
||||||
|
getDocumentCollection(request.getKnowledgeId().toString(), ResourceAction.MANAGE, "无权限管理知识库");
|
||||||
|
return documentService.startIndexTask(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("import/task/retryParse")
|
||||||
|
@SaCheckPermission("/api/v1/documentCollection/save")
|
||||||
|
public Result<DocumentImportDtos.TaskStartIndexResponse> retryParseTask(@JsonBody DocumentImportDtos.TaskRetryRequest request) {
|
||||||
|
if (request.getKnowledgeId() == null) {
|
||||||
|
throw new BusinessException("知识库id不能为空");
|
||||||
|
}
|
||||||
|
getDocumentCollection(request.getKnowledgeId().toString(), ResourceAction.MANAGE, "无权限管理知识库");
|
||||||
|
return documentService.retryParseTask(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("import/task/retryIndex")
|
||||||
|
@SaCheckPermission("/api/v1/documentCollection/save")
|
||||||
|
public Result<DocumentImportDtos.TaskStartIndexResponse> retryIndexTask(@JsonBody DocumentImportDtos.TaskRetryRequest request) {
|
||||||
|
if (request.getKnowledgeId() == null) {
|
||||||
|
throw new BusinessException("知识库id不能为空");
|
||||||
|
}
|
||||||
|
getDocumentCollection(request.getKnowledgeId().toString(), ResourceAction.MANAGE, "无权限管理知识库");
|
||||||
|
return documentService.retryIndexTask(request);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新 entity
|
* 更新 entity
|
||||||
*
|
*
|
||||||
@@ -219,17 +375,42 @@ public class DocumentController extends BaseCurdController<DocumentService, Docu
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private DocumentCollection getDocumentCollection(String idOrSlug) {
|
private DocumentCollection getDocumentCollection(String idOrSlug, ResourceAction action, String denyMessage) {
|
||||||
DocumentCollection knowledge = StringUtil.isNumeric(idOrSlug)
|
DocumentCollection knowledge = StringUtil.isNumeric(idOrSlug)
|
||||||
? knowledgeService.getById(idOrSlug)
|
? knowledgeService.getById(idOrSlug)
|
||||||
: knowledgeService.getOne(QueryWrapper.create().eq(DocumentCollection::getSlug, idOrSlug));
|
: knowledgeService.getOne(QueryWrapper.create().eq(DocumentCollection::getSlug, idOrSlug));
|
||||||
if (knowledge == null) {
|
if (knowledge == null) {
|
||||||
throw new BusinessException("知识库不存在");
|
throw new BusinessException("知识库不存在");
|
||||||
}
|
}
|
||||||
|
resourceAccessService.assertAccess(CategoryResourceType.KNOWLEDGE, knowledge, action, denyMessage);
|
||||||
if (knowledge.isFaqCollection()) {
|
if (knowledge.isFaqCollection()) {
|
||||||
throw new BusinessException("FAQ知识库不支持文档操作");
|
throw new BusinessException("FAQ知识库不支持文档操作");
|
||||||
}
|
}
|
||||||
return knowledge;
|
return knowledge;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Document requireDocument(BigInteger documentId) {
|
||||||
|
if (documentId == null) {
|
||||||
|
throw new BusinessException("文档不存在");
|
||||||
|
}
|
||||||
|
Document document = service.getById(documentId);
|
||||||
|
if (document == null) {
|
||||||
|
throw new BusinessException("文档不存在");
|
||||||
|
}
|
||||||
|
return document;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveDownloadFileName(Document document) {
|
||||||
|
String fileName = document.getTitle();
|
||||||
|
if (!StringUtil.hasText(fileName)) {
|
||||||
|
String path = document.getDocumentPath();
|
||||||
|
if (!StringUtil.hasText(path)) {
|
||||||
|
return "document";
|
||||||
|
}
|
||||||
|
int slashIndex = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'));
|
||||||
|
return slashIndex >= 0 ? path.substring(slashIndex + 1) : path;
|
||||||
|
}
|
||||||
|
return fileName;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ import tech.easyflow.common.domain.Result;
|
|||||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||||
|
import tech.easyflow.system.enums.CategoryResourceType;
|
||||||
|
import tech.easyflow.system.enums.ResourceAction;
|
||||||
|
import tech.easyflow.system.enums.ResourceLookup;
|
||||||
|
import tech.easyflow.system.permission.resource.RequireResourceAccess;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
@@ -29,6 +33,13 @@ public class FaqCategoryController extends BaseCurdController<FaqCategoryService
|
|||||||
@Override
|
@Override
|
||||||
@GetMapping("list")
|
@GetMapping("list")
|
||||||
@SaCheckPermission("/api/v1/documentCollection/query")
|
@SaCheckPermission("/api/v1/documentCollection/query")
|
||||||
|
@RequireResourceAccess(
|
||||||
|
resource = CategoryResourceType.KNOWLEDGE,
|
||||||
|
action = ResourceAction.READ,
|
||||||
|
lookup = ResourceLookup.KNOWLEDGE_ID,
|
||||||
|
idExpr = "#entity == null ? null : #entity.collectionId",
|
||||||
|
denyMessage = "无权限访问知识库"
|
||||||
|
)
|
||||||
public Result<List<FaqCategory>> list(FaqCategory entity, Boolean asTree, String sortKey, String sortType) {
|
public Result<List<FaqCategory>> list(FaqCategory entity, Boolean asTree, String sortKey, String sortType) {
|
||||||
BigInteger collectionId = entity == null ? null : entity.getCollectionId();
|
BigInteger collectionId = entity == null ? null : entity.getCollectionId();
|
||||||
if (collectionId == null) {
|
if (collectionId == null) {
|
||||||
@@ -40,6 +51,13 @@ public class FaqCategoryController extends BaseCurdController<FaqCategoryService
|
|||||||
@Override
|
@Override
|
||||||
@PostMapping("save")
|
@PostMapping("save")
|
||||||
@SaCheckPermission("/api/v1/documentCollection/save")
|
@SaCheckPermission("/api/v1/documentCollection/save")
|
||||||
|
@RequireResourceAccess(
|
||||||
|
resource = CategoryResourceType.KNOWLEDGE,
|
||||||
|
action = ResourceAction.MANAGE,
|
||||||
|
lookup = ResourceLookup.KNOWLEDGE_ID,
|
||||||
|
idExpr = "#entity.collectionId",
|
||||||
|
denyMessage = "无权限管理知识库"
|
||||||
|
)
|
||||||
public Result<?> save(@JsonBody FaqCategory entity) {
|
public Result<?> save(@JsonBody FaqCategory entity) {
|
||||||
return Result.ok(service.saveCategory(entity));
|
return Result.ok(service.saveCategory(entity));
|
||||||
}
|
}
|
||||||
@@ -47,6 +65,13 @@ public class FaqCategoryController extends BaseCurdController<FaqCategoryService
|
|||||||
@Override
|
@Override
|
||||||
@PostMapping("update")
|
@PostMapping("update")
|
||||||
@SaCheckPermission("/api/v1/documentCollection/save")
|
@SaCheckPermission("/api/v1/documentCollection/save")
|
||||||
|
@RequireResourceAccess(
|
||||||
|
resource = CategoryResourceType.KNOWLEDGE,
|
||||||
|
action = ResourceAction.MANAGE,
|
||||||
|
lookup = ResourceLookup.FAQ_CATEGORY_ID,
|
||||||
|
idExpr = "#entity.id",
|
||||||
|
denyMessage = "无权限管理知识库"
|
||||||
|
)
|
||||||
public Result<?> update(@JsonBody FaqCategory entity) {
|
public Result<?> update(@JsonBody FaqCategory entity) {
|
||||||
return Result.ok(service.updateCategory(entity));
|
return Result.ok(service.updateCategory(entity));
|
||||||
}
|
}
|
||||||
@@ -54,6 +79,13 @@ public class FaqCategoryController extends BaseCurdController<FaqCategoryService
|
|||||||
@Override
|
@Override
|
||||||
@PostMapping("remove")
|
@PostMapping("remove")
|
||||||
@SaCheckPermission("/api/v1/documentCollection/remove")
|
@SaCheckPermission("/api/v1/documentCollection/remove")
|
||||||
|
@RequireResourceAccess(
|
||||||
|
resource = CategoryResourceType.KNOWLEDGE,
|
||||||
|
action = ResourceAction.MANAGE,
|
||||||
|
lookup = ResourceLookup.FAQ_CATEGORY_ID,
|
||||||
|
idExpr = "#id",
|
||||||
|
denyMessage = "无权限管理知识库"
|
||||||
|
)
|
||||||
public Result<?> remove(@JsonBody(value = "id", required = true) Serializable id) {
|
public Result<?> remove(@JsonBody(value = "id", required = true) Serializable id) {
|
||||||
return Result.ok(service.removeCategory(new BigInteger(String.valueOf(id))));
|
return Result.ok(service.removeCategory(new BigInteger(String.valueOf(id))));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ import tech.easyflow.common.vo.UploadResVo;
|
|||||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||||
|
import tech.easyflow.system.enums.CategoryResourceType;
|
||||||
|
import tech.easyflow.system.enums.ResourceAction;
|
||||||
|
import tech.easyflow.system.enums.ResourceLookup;
|
||||||
|
import tech.easyflow.system.permission.resource.RequireResourceAccess;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
@@ -67,13 +71,31 @@ public class FaqItemController extends BaseCurdController<FaqItemService, FaqIte
|
|||||||
@Override
|
@Override
|
||||||
@GetMapping("list")
|
@GetMapping("list")
|
||||||
@SaCheckPermission("/api/v1/documentCollection/query")
|
@SaCheckPermission("/api/v1/documentCollection/query")
|
||||||
|
@RequireResourceAccess(
|
||||||
|
resource = CategoryResourceType.KNOWLEDGE,
|
||||||
|
action = ResourceAction.READ,
|
||||||
|
lookup = ResourceLookup.KNOWLEDGE_ID,
|
||||||
|
idExpr = "#entity == null ? null : #entity.collectionId",
|
||||||
|
denyMessage = "无权限访问知识库"
|
||||||
|
)
|
||||||
public Result<java.util.List<FaqItem>> list(FaqItem entity, Boolean asTree, String sortKey, String sortType) {
|
public Result<java.util.List<FaqItem>> list(FaqItem entity, Boolean asTree, String sortKey, String sortType) {
|
||||||
|
BigInteger collectionId = entity == null ? null : entity.getCollectionId();
|
||||||
|
if (collectionId == null) {
|
||||||
|
throw new BusinessException("知识库ID不能为空");
|
||||||
|
}
|
||||||
return super.list(entity, asTree, sortKey, sortType);
|
return super.list(entity, asTree, sortKey, sortType);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@GetMapping("page")
|
@GetMapping("page")
|
||||||
@SaCheckPermission("/api/v1/documentCollection/query")
|
@SaCheckPermission("/api/v1/documentCollection/query")
|
||||||
|
@RequireResourceAccess(
|
||||||
|
resource = CategoryResourceType.KNOWLEDGE,
|
||||||
|
action = ResourceAction.READ,
|
||||||
|
lookup = ResourceLookup.KNOWLEDGE_ID,
|
||||||
|
idExpr = "#request.getParameter('collectionId')",
|
||||||
|
denyMessage = "无权限访问知识库"
|
||||||
|
)
|
||||||
public Result<Page<FaqItem>> page(HttpServletRequest request, String sortKey, String sortType, Long pageNumber, Long pageSize) {
|
public Result<Page<FaqItem>> page(HttpServletRequest request, String sortKey, String sortType, Long pageNumber, Long pageSize) {
|
||||||
if (pageNumber == null || pageNumber < 1) {
|
if (pageNumber == null || pageNumber < 1) {
|
||||||
pageNumber = 1L;
|
pageNumber = 1L;
|
||||||
@@ -123,6 +145,13 @@ public class FaqItemController extends BaseCurdController<FaqItemService, FaqIte
|
|||||||
@Override
|
@Override
|
||||||
@GetMapping("detail")
|
@GetMapping("detail")
|
||||||
@SaCheckPermission("/api/v1/documentCollection/query")
|
@SaCheckPermission("/api/v1/documentCollection/query")
|
||||||
|
@RequireResourceAccess(
|
||||||
|
resource = CategoryResourceType.KNOWLEDGE,
|
||||||
|
action = ResourceAction.READ,
|
||||||
|
lookup = ResourceLookup.FAQ_ITEM_ID,
|
||||||
|
idExpr = "#id",
|
||||||
|
denyMessage = "无权限访问知识库"
|
||||||
|
)
|
||||||
public Result<FaqItem> detail(String id) {
|
public Result<FaqItem> detail(String id) {
|
||||||
return super.detail(id);
|
return super.detail(id);
|
||||||
}
|
}
|
||||||
@@ -130,6 +159,13 @@ public class FaqItemController extends BaseCurdController<FaqItemService, FaqIte
|
|||||||
@Override
|
@Override
|
||||||
@PostMapping("save")
|
@PostMapping("save")
|
||||||
@SaCheckPermission("/api/v1/documentCollection/save")
|
@SaCheckPermission("/api/v1/documentCollection/save")
|
||||||
|
@RequireResourceAccess(
|
||||||
|
resource = CategoryResourceType.KNOWLEDGE,
|
||||||
|
action = ResourceAction.MANAGE,
|
||||||
|
lookup = ResourceLookup.KNOWLEDGE_ID,
|
||||||
|
idExpr = "#entity.collectionId",
|
||||||
|
denyMessage = "无权限管理知识库"
|
||||||
|
)
|
||||||
public Result<?> save(@JsonBody FaqItem entity) {
|
public Result<?> save(@JsonBody FaqItem entity) {
|
||||||
return Result.ok(service.saveFaqItem(entity));
|
return Result.ok(service.saveFaqItem(entity));
|
||||||
}
|
}
|
||||||
@@ -137,6 +173,13 @@ public class FaqItemController extends BaseCurdController<FaqItemService, FaqIte
|
|||||||
@Override
|
@Override
|
||||||
@PostMapping("update")
|
@PostMapping("update")
|
||||||
@SaCheckPermission("/api/v1/documentCollection/save")
|
@SaCheckPermission("/api/v1/documentCollection/save")
|
||||||
|
@RequireResourceAccess(
|
||||||
|
resource = CategoryResourceType.KNOWLEDGE,
|
||||||
|
action = ResourceAction.MANAGE,
|
||||||
|
lookup = ResourceLookup.FAQ_ITEM_ID,
|
||||||
|
idExpr = "#entity.id",
|
||||||
|
denyMessage = "无权限管理知识库"
|
||||||
|
)
|
||||||
public Result<?> update(@JsonBody FaqItem entity) {
|
public Result<?> update(@JsonBody FaqItem entity) {
|
||||||
return Result.ok(service.updateFaqItem(entity));
|
return Result.ok(service.updateFaqItem(entity));
|
||||||
}
|
}
|
||||||
@@ -144,12 +187,26 @@ public class FaqItemController extends BaseCurdController<FaqItemService, FaqIte
|
|||||||
@Override
|
@Override
|
||||||
@PostMapping("remove")
|
@PostMapping("remove")
|
||||||
@SaCheckPermission("/api/v1/documentCollection/remove")
|
@SaCheckPermission("/api/v1/documentCollection/remove")
|
||||||
|
@RequireResourceAccess(
|
||||||
|
resource = CategoryResourceType.KNOWLEDGE,
|
||||||
|
action = ResourceAction.MANAGE,
|
||||||
|
lookup = ResourceLookup.FAQ_ITEM_ID,
|
||||||
|
idExpr = "#id",
|
||||||
|
denyMessage = "无权限管理知识库"
|
||||||
|
)
|
||||||
public Result<?> remove(@JsonBody(value = "id", required = true) Serializable id) {
|
public Result<?> remove(@JsonBody(value = "id", required = true) Serializable id) {
|
||||||
return Result.ok(service.removeFaqItem(new java.math.BigInteger(String.valueOf(id))));
|
return Result.ok(service.removeFaqItem(new java.math.BigInteger(String.valueOf(id))));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(value = "uploadImage", produces = MediaType.APPLICATION_JSON_VALUE)
|
@PostMapping(value = "uploadImage", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||||
@SaCheckPermission("/api/v1/documentCollection/save")
|
@SaCheckPermission("/api/v1/documentCollection/save")
|
||||||
|
@RequireResourceAccess(
|
||||||
|
resource = CategoryResourceType.KNOWLEDGE,
|
||||||
|
action = ResourceAction.MANAGE,
|
||||||
|
lookup = ResourceLookup.KNOWLEDGE_ID,
|
||||||
|
idExpr = "#collectionId",
|
||||||
|
denyMessage = "无权限管理知识库"
|
||||||
|
)
|
||||||
public Result<UploadResVo> uploadImage(MultipartFile file, BigInteger collectionId) {
|
public Result<UploadResVo> uploadImage(MultipartFile file, BigInteger collectionId) {
|
||||||
if (collectionId == null) {
|
if (collectionId == null) {
|
||||||
throw new BusinessException("知识库ID不能为空");
|
throw new BusinessException("知识库ID不能为空");
|
||||||
@@ -180,12 +237,26 @@ public class FaqItemController extends BaseCurdController<FaqItemService, FaqIte
|
|||||||
|
|
||||||
@PostMapping(value = "importExcel", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
@PostMapping(value = "importExcel", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||||
@SaCheckPermission("/api/v1/documentCollection/save")
|
@SaCheckPermission("/api/v1/documentCollection/save")
|
||||||
|
@RequireResourceAccess(
|
||||||
|
resource = CategoryResourceType.KNOWLEDGE,
|
||||||
|
action = ResourceAction.MANAGE,
|
||||||
|
lookup = ResourceLookup.KNOWLEDGE_ID,
|
||||||
|
idExpr = "#collectionId",
|
||||||
|
denyMessage = "无权限管理知识库"
|
||||||
|
)
|
||||||
public Result<FaqImportResultVo> importExcel(MultipartFile file, BigInteger collectionId) {
|
public Result<FaqImportResultVo> importExcel(MultipartFile file, BigInteger collectionId) {
|
||||||
return Result.ok(service.importFromExcel(collectionId, file));
|
return Result.ok(service.importFromExcel(collectionId, file));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("downloadImportTemplate")
|
@GetMapping("downloadImportTemplate")
|
||||||
@SaCheckPermission("/api/v1/documentCollection/query")
|
@SaCheckPermission("/api/v1/documentCollection/query")
|
||||||
|
@RequireResourceAccess(
|
||||||
|
resource = CategoryResourceType.KNOWLEDGE,
|
||||||
|
action = ResourceAction.READ,
|
||||||
|
lookup = ResourceLookup.KNOWLEDGE_ID,
|
||||||
|
idExpr = "#collectionId",
|
||||||
|
denyMessage = "无权限访问知识库"
|
||||||
|
)
|
||||||
public void downloadImportTemplate(BigInteger collectionId, HttpServletResponse response) throws Exception {
|
public void downloadImportTemplate(BigInteger collectionId, HttpServletResponse response) throws Exception {
|
||||||
if (collectionId == null) {
|
if (collectionId == null) {
|
||||||
throw new BusinessException("知识库ID不能为空");
|
throw new BusinessException("知识库ID不能为空");
|
||||||
@@ -206,6 +277,13 @@ public class FaqItemController extends BaseCurdController<FaqItemService, FaqIte
|
|||||||
|
|
||||||
@GetMapping("exportExcel")
|
@GetMapping("exportExcel")
|
||||||
@SaCheckPermission("/api/v1/documentCollection/query")
|
@SaCheckPermission("/api/v1/documentCollection/query")
|
||||||
|
@RequireResourceAccess(
|
||||||
|
resource = CategoryResourceType.KNOWLEDGE,
|
||||||
|
action = ResourceAction.READ,
|
||||||
|
lookup = ResourceLookup.KNOWLEDGE_ID,
|
||||||
|
idExpr = "#collectionId",
|
||||||
|
denyMessage = "无权限访问知识库"
|
||||||
|
)
|
||||||
public void exportExcel(BigInteger collectionId, HttpServletResponse response) throws Exception {
|
public void exportExcel(BigInteger collectionId, HttpServletResponse response) throws Exception {
|
||||||
if (collectionId == null) {
|
if (collectionId == null) {
|
||||||
throw new BusinessException("知识库ID不能为空");
|
throw new BusinessException("知识库ID不能为空");
|
||||||
|
|||||||
@@ -0,0 +1,240 @@
|
|||||||
|
package tech.easyflow.admin.controller.ai;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
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.dto.KnowledgeShareApiGrantRequest;
|
||||||
|
import tech.easyflow.ai.entity.DocumentCollection;
|
||||||
|
import tech.easyflow.ai.enums.KnowledgeShareActionScope;
|
||||||
|
import tech.easyflow.ai.service.DocumentCollectionService;
|
||||||
|
import tech.easyflow.ai.service.KnowledgeShareAuditService;
|
||||||
|
import tech.easyflow.ai.service.KnowledgeSharePermissionService;
|
||||||
|
import tech.easyflow.ai.service.KnowledgeShareService;
|
||||||
|
import tech.easyflow.ai.vo.KnowledgeShareUrlCreateResult;
|
||||||
|
import tech.easyflow.common.domain.Result;
|
||||||
|
import tech.easyflow.common.entity.LoginAccount;
|
||||||
|
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||||
|
import tech.easyflow.common.util.RequestUtil;
|
||||||
|
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||||
|
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||||
|
import tech.easyflow.system.enums.CategoryResourceType;
|
||||||
|
import tech.easyflow.system.enums.ResourceAction;
|
||||||
|
import tech.easyflow.system.service.ResourceAccessService;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 知识库分享管理接口。
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/knowledgeShare")
|
||||||
|
public class KnowledgeShareController {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private KnowledgeShareService knowledgeShareService;
|
||||||
|
@Resource
|
||||||
|
private KnowledgeSharePermissionService knowledgeSharePermissionService;
|
||||||
|
@Resource
|
||||||
|
private KnowledgeShareAuditService knowledgeShareAuditService;
|
||||||
|
@Resource
|
||||||
|
private ResourceAccessService resourceAccessService;
|
||||||
|
@Resource
|
||||||
|
private DocumentCollectionService documentCollectionService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建 URL 分享。
|
||||||
|
*
|
||||||
|
* @param request HTTP 请求
|
||||||
|
* @param knowledgeId 知识库 ID
|
||||||
|
* @return 创建结果
|
||||||
|
*/
|
||||||
|
@PostMapping("/url/create")
|
||||||
|
@SaCheckPermission("/api/v1/documentCollection/save")
|
||||||
|
public Result<KnowledgeShareUrlCreateResult> createUrlShare(HttpServletRequest request, @JsonBody("knowledgeId") BigInteger knowledgeId) {
|
||||||
|
assertManagePermission(knowledgeId);
|
||||||
|
LoginAccount loginAccount = SaTokenUtil.getLoginAccount();
|
||||||
|
KnowledgeShareUrlCreateResult result = knowledgeShareService.createUrlShare(
|
||||||
|
knowledgeId,
|
||||||
|
loginAccount.getTenantId(),
|
||||||
|
loginAccount.getDeptId(),
|
||||||
|
loginAccount.getId(),
|
||||||
|
buildShareBaseUrl(request),
|
||||||
|
KnowledgeShareActionScope.defaultUrlScopes()
|
||||||
|
);
|
||||||
|
knowledgeShareAuditService.log(
|
||||||
|
loginAccount.getId(),
|
||||||
|
"创建知识库 URL 分享",
|
||||||
|
"KNOWLEDGE_SHARE_CREATE",
|
||||||
|
request.getRequestURI(),
|
||||||
|
Map.of("knowledgeId", knowledgeId, "shareId", result.getId())
|
||||||
|
);
|
||||||
|
return Result.ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为系统访问令牌配置知识库 API 分享授权。
|
||||||
|
*
|
||||||
|
* @param request 授权请求
|
||||||
|
* @return 结果
|
||||||
|
*/
|
||||||
|
@PostMapping("/api/grant")
|
||||||
|
@SaCheckPermission("/api/v1/documentCollection/save")
|
||||||
|
public Result<Void> grantApiShare(@JsonBody KnowledgeShareApiGrantRequest request) {
|
||||||
|
assertManagePermission(request.getKnowledgeId());
|
||||||
|
knowledgeSharePermissionService.grantApiShare(
|
||||||
|
request.getApiKeyId(),
|
||||||
|
request.getKnowledgeId(),
|
||||||
|
request.getActionScopes()
|
||||||
|
);
|
||||||
|
LoginAccount loginAccount = SaTokenUtil.getLoginAccount();
|
||||||
|
knowledgeShareAuditService.log(
|
||||||
|
loginAccount.getId(),
|
||||||
|
"配置知识库 API 分享授权",
|
||||||
|
"KNOWLEDGE_API_SHARE_GRANT",
|
||||||
|
"/api/v1/knowledgeShare/api/grant",
|
||||||
|
Map.of(
|
||||||
|
"knowledgeId", request.getKnowledgeId(),
|
||||||
|
"apiKeyId", request.getApiKeyId(),
|
||||||
|
"actionScopes", request.getActionScopes()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return Result.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertManagePermission(BigInteger knowledgeId) {
|
||||||
|
DocumentCollection knowledge = documentCollectionService.getById(knowledgeId);
|
||||||
|
if (knowledge == null) {
|
||||||
|
throw new BusinessException("知识库不存在");
|
||||||
|
}
|
||||||
|
resourceAccessService.assertAccess(
|
||||||
|
CategoryResourceType.KNOWLEDGE,
|
||||||
|
knowledge,
|
||||||
|
ResourceAction.MANAGE,
|
||||||
|
"无权限管理知识库"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildShareBaseUrl(HttpServletRequest request) {
|
||||||
|
String referer = RequestUtil.getReferer(request);
|
||||||
|
String refererBaseUrl = extractFrontendBaseUrl(referer);
|
||||||
|
if (refererBaseUrl != null) {
|
||||||
|
return refererBaseUrl + "/share/knowledge";
|
||||||
|
}
|
||||||
|
|
||||||
|
String forwardedOrigin = buildForwardedOrigin(request);
|
||||||
|
if (forwardedOrigin != null) {
|
||||||
|
return forwardedOrigin + normalizeBasePath(firstHeaderValue(request.getHeader("X-Forwarded-Prefix"))) + "/share/knowledge";
|
||||||
|
}
|
||||||
|
|
||||||
|
String origin = normalizeOrigin(request.getHeader("Origin"));
|
||||||
|
if (origin != null) {
|
||||||
|
return origin + normalizeBasePath(request.getContextPath()) + "/share/knowledge";
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder builder = new StringBuilder();
|
||||||
|
builder.append(request.getScheme()).append("://").append(request.getServerName());
|
||||||
|
if (request.getServerPort() != 80 && request.getServerPort() != 443) {
|
||||||
|
builder.append(':').append(request.getServerPort());
|
||||||
|
}
|
||||||
|
builder.append(normalizeBasePath(request.getContextPath())).append("/share/knowledge");
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractFrontendBaseUrl(String url) {
|
||||||
|
if (url == null || url.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
URI uri = new URI(url.trim());
|
||||||
|
if (uri.getScheme() == null || uri.getHost() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String origin = extractOrigin(url);
|
||||||
|
if (origin == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return origin + inferFrontendBasePath(uri.getPath());
|
||||||
|
} catch (URISyntaxException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String inferFrontendBasePath(String path) {
|
||||||
|
if (path == null || path.isBlank() || "/".equals(path)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
for (String marker : new String[]{"/ai/", "/auth/", "/share/"}) {
|
||||||
|
int markerIndex = path.indexOf(marker);
|
||||||
|
if (markerIndex > 0) {
|
||||||
|
return normalizeBasePath(path.substring(0, markerIndex));
|
||||||
|
}
|
||||||
|
if (markerIndex == 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeBasePath(String basePath) {
|
||||||
|
if (basePath == null || basePath.isBlank() || "/".equals(basePath.trim())) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
String normalized = basePath.trim();
|
||||||
|
if (!normalized.startsWith("/")) {
|
||||||
|
normalized = "/" + normalized;
|
||||||
|
}
|
||||||
|
while (normalized.endsWith("/") && normalized.length() > 1) {
|
||||||
|
normalized = normalized.substring(0, normalized.length() - 1);
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildForwardedOrigin(HttpServletRequest request) {
|
||||||
|
String proto = firstHeaderValue(request.getHeader("X-Forwarded-Proto"));
|
||||||
|
String host = firstHeaderValue(request.getHeader("X-Forwarded-Host"));
|
||||||
|
if (proto == null || host == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return normalizeOrigin(proto + "://" + host);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractOrigin(String url) {
|
||||||
|
if (url == null || url.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
URI uri = new URI(url.trim());
|
||||||
|
if (uri.getScheme() == null || uri.getHost() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
StringBuilder builder = new StringBuilder();
|
||||||
|
builder.append(uri.getScheme()).append("://").append(uri.getHost());
|
||||||
|
if (uri.getPort() != -1 && uri.getPort() != 80 && uri.getPort() != 443) {
|
||||||
|
builder.append(':').append(uri.getPort());
|
||||||
|
}
|
||||||
|
return builder.toString();
|
||||||
|
} catch (URISyntaxException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeOrigin(String origin) {
|
||||||
|
return extractOrigin(origin);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String firstHeaderValue(String value) {
|
||||||
|
if (value == null || value.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
int commaIndex = value.indexOf(',');
|
||||||
|
String normalized = commaIndex >= 0 ? value.substring(0, commaIndex) : value;
|
||||||
|
normalized = normalized.trim();
|
||||||
|
return normalized.isEmpty() ? null : normalized;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package tech.easyflow.admin.controller.ai;
|
package tech.easyflow.admin.controller.ai;
|
||||||
|
|
||||||
|
import com.easyagents.mcp.client.McpEnvironmentCheckResult;
|
||||||
import com.mybatisflex.core.paginate.Page;
|
import com.mybatisflex.core.paginate.Page;
|
||||||
import com.mybatisflex.core.query.QueryWrapper;
|
import com.mybatisflex.core.query.QueryWrapper;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
@@ -64,6 +65,11 @@ public class McpController extends BaseCurdController<McpService, Mcp> {
|
|||||||
return Result.ok(service.getMcpTools(id));
|
return Result.ok(service.getMcpTools(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/check")
|
||||||
|
public Result<McpEnvironmentCheckResult> check(@JsonBody("configJson") String configJson) {
|
||||||
|
return Result.ok(service.checkMcp(configJson));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@GetMapping("pageTools")
|
@GetMapping("pageTools")
|
||||||
public Result<Page<Mcp>> pageTools(HttpServletRequest request, String sortKey, String sortType, Long pageNumber, Long pageSize) {
|
public Result<Page<Mcp>> pageTools(HttpServletRequest request, String sortKey, String sortType, Long pageNumber, Long pageSize) {
|
||||||
@@ -80,4 +86,4 @@ public class McpController extends BaseCurdController<McpService, Mcp> {
|
|||||||
|
|
||||||
return Result.ok(service.pageTools(mcpPage));
|
return Result.ok(service.pageTools(mcpPage));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import org.springframework.beans.factory.annotation.Autowired;
|
|||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import tech.easyflow.ai.dto.ModelInvokeConfigDtos;
|
||||||
import tech.easyflow.ai.entity.Model;
|
import tech.easyflow.ai.entity.Model;
|
||||||
import tech.easyflow.ai.entity.ModelProvider;
|
import tech.easyflow.ai.entity.ModelProvider;
|
||||||
import tech.easyflow.ai.entity.table.ModelTableDef;
|
import tech.easyflow.ai.entity.table.ModelTableDef;
|
||||||
@@ -69,6 +70,12 @@ public class ModelController extends BaseCurdController<ModelService, Model> {
|
|||||||
return Result.ok(modelService.getList(entity));
|
return Result.ok(modelService.getList(entity));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("invokeList")
|
||||||
|
@SaCheckPermission("/api/v1/model/query")
|
||||||
|
public Result<List<Model>> invokeList() {
|
||||||
|
return Result.ok(modelService.listInvokeModels());
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("/addAiLlm")
|
@PostMapping("/addAiLlm")
|
||||||
@SaCheckPermission("/api/v1/model/save")
|
@SaCheckPermission("/api/v1/model/save")
|
||||||
public Result<Boolean> addAiLlm(Model entity) {
|
public Result<Boolean> addAiLlm(Model entity) {
|
||||||
@@ -92,11 +99,29 @@ public class ModelController extends BaseCurdController<ModelService, Model> {
|
|||||||
return Result.ok();
|
return Result.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/updateByEntity")
|
@PostMapping("/updateInvokeConfig")
|
||||||
@SaCheckPermission("/api/v1/model/save")
|
@SaCheckPermission("/api/v1/model/save")
|
||||||
public Result<?> updateByEntity(@RequestBody Model entity) {
|
public Result<Model> updateInvokeConfig(@RequestBody ModelInvokeConfigDtos.UpdateRequest request) {
|
||||||
modelService.updateByEntity(entity);
|
return Result.ok(modelService.updateInvokeConfig(
|
||||||
return Result.ok();
|
request.getId(),
|
||||||
|
request.getInvokeCode(),
|
||||||
|
request.getPublishEnabled()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/batchUpdateInvokePublishStatus")
|
||||||
|
@SaCheckPermission("/api/v1/model/save")
|
||||||
|
public Result<List<Model>> batchUpdateInvokePublishStatus(@RequestBody ModelInvokeConfigDtos.BatchPublishRequest request) {
|
||||||
|
return Result.ok(modelService.batchUpdateInvokePublishStatus(
|
||||||
|
request.getIds(),
|
||||||
|
request.getPublishEnabled()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Result<?> onSaveOrUpdateBefore(Model entity, boolean isSave) {
|
||||||
|
modelService.validateForSaveOrUpdate(entity, isSave);
|
||||||
|
return super.onSaveOrUpdateBefore(entity, isSave);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/selectLlmByProviderCategory")
|
@GetMapping("/selectLlmByProviderCategory")
|
||||||
@@ -129,19 +154,6 @@ public class ModelController extends BaseCurdController<ModelService, Model> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加所有模型
|
|
||||||
*
|
|
||||||
* @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")
|
@GetMapping("/selectLlmList")
|
||||||
@SaCheckPermission("/api/v1/model/query")
|
@SaCheckPermission("/api/v1/model/query")
|
||||||
public Result<List<Model>> selectLlmList(Model entity, Boolean asTree, String sortKey, String sortType) {
|
public Result<List<Model>> selectLlmList(Model entity, Boolean asTree, String sortKey, String sortType) {
|
||||||
@@ -154,15 +166,6 @@ public class ModelController extends BaseCurdController<ModelService, Model> {
|
|||||||
return Result.ok(totalList);
|
return Result.ok(totalList);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Result<?> onSaveOrUpdateBefore(Model entity, boolean isSave) {
|
|
||||||
if (isSave) {
|
|
||||||
entity.setWithUsed(true);
|
|
||||||
}
|
|
||||||
return super.onSaveOrUpdateBefore(entity, isSave);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@PostMapping("removeLlmByIds")
|
@PostMapping("removeLlmByIds")
|
||||||
@Transactional
|
@Transactional
|
||||||
public Result<?> removeLlm(@JsonBody(value = "id", required = true) Serializable id) {
|
public Result<?> removeLlm(@JsonBody(value = "id", required = true) Serializable id) {
|
||||||
@@ -171,4 +174,4 @@ public class ModelController extends BaseCurdController<ModelService, Model> {
|
|||||||
service.remove(queryWrapper);
|
service.remove(queryWrapper);
|
||||||
return Result.ok();
|
return Result.ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package tech.easyflow.admin.controller.ai;
|
package tech.easyflow.admin.controller.ai;
|
||||||
|
|
||||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
|
import com.mybatisflex.core.query.QueryWrapper;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
@@ -10,9 +11,13 @@ import tech.easyflow.ai.service.PluginCategoryService;
|
|||||||
import tech.easyflow.common.annotation.UsePermission;
|
import tech.easyflow.common.annotation.UsePermission;
|
||||||
import tech.easyflow.common.domain.Result;
|
import tech.easyflow.common.domain.Result;
|
||||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||||
|
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
|
||||||
|
import tech.easyflow.system.service.CategoryPermissionService;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 控制层。
|
* 控制层。
|
||||||
@@ -30,6 +35,8 @@ public class PluginCategoryController extends BaseCurdController<PluginCategoryS
|
|||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private PluginCategoryService pluginCategoryService;
|
private PluginCategoryService pluginCategoryService;
|
||||||
|
@Resource
|
||||||
|
private CategoryPermissionService categoryPermissionService;
|
||||||
|
|
||||||
@GetMapping("/doRemoveCategory")
|
@GetMapping("/doRemoveCategory")
|
||||||
@SaCheckPermission("/api/v1/plugin/remove")
|
@SaCheckPermission("/api/v1/plugin/remove")
|
||||||
@@ -37,4 +44,18 @@ public class PluginCategoryController extends BaseCurdController<PluginCategoryS
|
|||||||
|
|
||||||
return Result.ok(pluginCategoryService.doRemoveCategory(id));
|
return Result.ok(pluginCategoryService.doRemoveCategory(id));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
@GetMapping("/visibleList")
|
||||||
|
public Result<List<PluginCategory>> visibleList(PluginCategory entity, Boolean asTree, String sortKey, String sortType) {
|
||||||
|
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
|
||||||
|
RoleCategoryAccessSnapshot access = categoryPermissionService.getCurrentAccess("PLUGIN");
|
||||||
|
if (access.isRestricted()) {
|
||||||
|
if (access.getCategoryIds().isEmpty()) {
|
||||||
|
return Result.ok(Collections.emptyList());
|
||||||
|
}
|
||||||
|
queryWrapper.in(PluginCategory::getId, access.getCategoryIds());
|
||||||
|
}
|
||||||
|
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||||
|
return Result.ok(service.list(queryWrapper));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
package tech.easyflow.admin.controller.ai;
|
package tech.easyflow.admin.controller.ai;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import tech.easyflow.ai.entity.PluginCategory;
|
import tech.easyflow.ai.entity.PluginCategory;
|
||||||
import tech.easyflow.ai.entity.PluginCategoryMapping;
|
import tech.easyflow.ai.entity.PluginCategoryMapping;
|
||||||
import tech.easyflow.ai.service.PluginCategoryMappingService;
|
import tech.easyflow.ai.service.PluginCategoryMappingService;
|
||||||
|
import tech.easyflow.common.annotation.UsePermission;
|
||||||
import tech.easyflow.common.domain.Result;
|
import tech.easyflow.common.domain.Result;
|
||||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||||
@@ -21,6 +23,7 @@ import java.util.List;
|
|||||||
*/
|
*/
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/pluginCategoryMapping")
|
@RequestMapping("/api/v1/pluginCategoryMapping")
|
||||||
|
@UsePermission(moduleName = "/api/v1/plugin")
|
||||||
public class PluginCategoryMappingController extends BaseCurdController<PluginCategoryMappingService, PluginCategoryMapping> {
|
public class PluginCategoryMappingController extends BaseCurdController<PluginCategoryMappingService, PluginCategoryMapping> {
|
||||||
public PluginCategoryMappingController(PluginCategoryMappingService service) {
|
public PluginCategoryMappingController(PluginCategoryMappingService service) {
|
||||||
super(service);
|
super(service);
|
||||||
@@ -30,6 +33,7 @@ public class PluginCategoryMappingController extends BaseCurdController<PluginCa
|
|||||||
private PluginCategoryMappingService relationService;
|
private PluginCategoryMappingService relationService;
|
||||||
|
|
||||||
@PostMapping("/updateRelation")
|
@PostMapping("/updateRelation")
|
||||||
|
@SaCheckPermission("/api/v1/plugin/save")
|
||||||
public Result<Boolean> updateRelation(
|
public Result<Boolean> updateRelation(
|
||||||
@JsonBody(value="pluginId") BigInteger pluginId,
|
@JsonBody(value="pluginId") BigInteger pluginId,
|
||||||
@JsonBody(value="categoryIds") ArrayList<BigInteger> categoryIds
|
@JsonBody(value="categoryIds") ArrayList<BigInteger> categoryIds
|
||||||
@@ -42,4 +46,4 @@ public class PluginCategoryMappingController extends BaseCurdController<PluginCa
|
|||||||
){
|
){
|
||||||
return Result.ok(relationService.getPluginCategories(pluginId));
|
return Result.ok(relationService.getPluginCategories(pluginId));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,16 +6,37 @@ import com.mybatisflex.core.query.QueryWrapper;
|
|||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import tech.easyflow.admin.controller.ai.support.AiResourceCreatorNameSupport;
|
||||||
|
import tech.easyflow.ai.entity.Model;
|
||||||
import tech.easyflow.ai.entity.Plugin;
|
import tech.easyflow.ai.entity.Plugin;
|
||||||
|
import tech.easyflow.ai.entity.Workflow;
|
||||||
|
import tech.easyflow.ai.plugin.workflow.snapshot.WorkflowPluginSnapshotResolver;
|
||||||
|
import tech.easyflow.ai.service.ModelService;
|
||||||
|
import tech.easyflow.ai.service.PluginVisibilityService;
|
||||||
|
import tech.easyflow.ai.permission.WorkflowVisibilityQueryHelper;
|
||||||
|
import tech.easyflow.ai.service.WorkflowService;
|
||||||
import tech.easyflow.common.domain.Result;
|
import tech.easyflow.common.domain.Result;
|
||||||
|
import tech.easyflow.common.entity.LoginAccount;
|
||||||
|
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||||
import tech.easyflow.ai.service.PluginService;
|
import tech.easyflow.ai.service.PluginService;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||||
|
import tech.easyflow.system.enums.CategoryResourceType;
|
||||||
|
import tech.easyflow.system.enums.ResourceAction;
|
||||||
|
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
|
||||||
|
import tech.easyflow.system.service.CategoryPermissionService;
|
||||||
|
import tech.easyflow.system.service.ResourceAccessService;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static tech.easyflow.ai.entity.table.PluginTableDef.PLUGIN;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 控制层。
|
* 控制层。
|
||||||
@@ -32,6 +53,22 @@ public class PluginController extends BaseCurdController<PluginService, Plugin>
|
|||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
PluginService pluginService;
|
PluginService pluginService;
|
||||||
|
@Resource
|
||||||
|
private CategoryPermissionService categoryPermissionService;
|
||||||
|
@Resource
|
||||||
|
private PluginVisibilityService pluginVisibilityService;
|
||||||
|
@Resource
|
||||||
|
private ModelService modelService;
|
||||||
|
@Resource
|
||||||
|
private WorkflowVisibilityQueryHelper workflowVisibilityQueryHelper;
|
||||||
|
@Resource
|
||||||
|
private WorkflowService workflowService;
|
||||||
|
@Resource
|
||||||
|
private ResourceAccessService resourceAccessService;
|
||||||
|
@Resource
|
||||||
|
private WorkflowPluginSnapshotResolver workflowPluginSnapshotResolver;
|
||||||
|
@Resource
|
||||||
|
private AiResourceCreatorNameSupport aiResourceCreatorNameSupport;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Result<?> onSaveOrUpdateBefore(Plugin entity, boolean isSave) {
|
protected Result<?> onSaveOrUpdateBefore(Plugin entity, boolean isSave) {
|
||||||
@@ -40,7 +77,7 @@ public class PluginController extends BaseCurdController<PluginService, Plugin>
|
|||||||
|
|
||||||
@PostMapping("/plugin/save")
|
@PostMapping("/plugin/save")
|
||||||
@SaCheckPermission("/api/v1/plugin/save")
|
@SaCheckPermission("/api/v1/plugin/save")
|
||||||
public Result<Boolean> savePlugin(@JsonBody Plugin plugin){
|
public Result<Plugin> savePlugin(@JsonBody Plugin plugin){
|
||||||
|
|
||||||
return Result.ok(pluginService.savePlugin(plugin));
|
return Result.ok(pluginService.savePlugin(plugin));
|
||||||
}
|
}
|
||||||
@@ -62,7 +99,10 @@ public class PluginController extends BaseCurdController<PluginService, Plugin>
|
|||||||
@PostMapping("/getList")
|
@PostMapping("/getList")
|
||||||
@SaCheckPermission("/api/v1/plugin/query")
|
@SaCheckPermission("/api/v1/plugin/query")
|
||||||
public Result<List<Plugin>> getList(){
|
public Result<List<Plugin>> getList(){
|
||||||
return Result.ok(pluginService.getList());
|
QueryWrapper queryWrapper = QueryWrapper.create().select();
|
||||||
|
applyCategoryPermission(queryWrapper);
|
||||||
|
List<Plugin> plugins = service.getMapper().selectListWithRelationsByQuery(queryWrapper);
|
||||||
|
return Result.ok(pluginService.preparePluginsForCurrentUser(plugins, true, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/pageByCategory")
|
@GetMapping("/pageByCategory")
|
||||||
@@ -76,15 +116,107 @@ public class PluginController extends BaseCurdController<PluginService, Plugin>
|
|||||||
}
|
}
|
||||||
if (category == 0){
|
if (category == 0){
|
||||||
QueryWrapper queryWrapper = buildQueryWrapper(request);
|
QueryWrapper queryWrapper = buildQueryWrapper(request);
|
||||||
|
applyCategoryPermission(queryWrapper);
|
||||||
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||||
return Result.ok(queryPage(new Page<>(pageNumber, pageSize), queryWrapper));
|
return Result.ok(queryPage(new Page<>(pageNumber, pageSize), queryWrapper));
|
||||||
} else {
|
} else {
|
||||||
return pluginService.pageByCategory(pageNumber, pageSize, category);
|
Result<Page<Plugin>> result = pluginService.pageByCategory(pageNumber, pageSize, category);
|
||||||
|
if (result != null && result.getData() != null) {
|
||||||
|
aiResourceCreatorNameSupport.fillPluginCreatorNames(result.getData().getRecords());
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/workflowCandidates")
|
||||||
|
@SaCheckPermission("/api/v1/plugin/query")
|
||||||
|
public Result<List<Workflow>> workflowCandidates(String keyword) {
|
||||||
|
QueryWrapper queryWrapper = QueryWrapper.create();
|
||||||
|
workflowVisibilityQueryHelper.applyReadableAccess(queryWrapper);
|
||||||
|
queryWrapper.eq("publish_status", tech.easyflow.ai.enums.PublishStatus.PUBLISHED.getCode());
|
||||||
|
if (keyword != null && !keyword.isBlank()) {
|
||||||
|
queryWrapper.like("title", keyword.trim());
|
||||||
|
}
|
||||||
|
queryWrapper.orderBy("modified desc");
|
||||||
|
LoginAccount loginAccount = SaTokenUtil.getLoginAccount();
|
||||||
|
List<Workflow> workflows = workflowService.list(queryWrapper).stream()
|
||||||
|
.filter(workflow -> canBindWorkflowCandidate(workflow, loginAccount))
|
||||||
|
.collect(java.util.stream.Collectors.toCollection(ArrayList::new));
|
||||||
|
return Result.ok(workflows);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/modelList")
|
||||||
|
@SaCheckPermission("/api/v1/plugin/query")
|
||||||
|
public Result<List<Model>> modelList(Model entity, Boolean asTree, String sortKey, String sortType) {
|
||||||
|
return Result.ok(modelService.listSelectableModels(entity, asTree, sortKey, sortType));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Page<Plugin> queryPage(Page<Plugin> page, QueryWrapper queryWrapper) {
|
protected Page<Plugin> queryPage(Page<Plugin> page, QueryWrapper queryWrapper) {
|
||||||
return service.getMapper().paginateWithRelations(page, queryWrapper);
|
applyCategoryPermission(queryWrapper);
|
||||||
|
List<Plugin> totalList = service.getMapper().selectListWithRelationsByQuery(queryWrapper);
|
||||||
|
boolean availableOnly = isAvailableOnly();
|
||||||
|
List<Plugin> prepared = pluginService.preparePluginsForCurrentUser(totalList, !availableOnly, availableOnly);
|
||||||
|
aiResourceCreatorNameSupport.fillPluginCreatorNames(prepared);
|
||||||
|
long total = prepared.size();
|
||||||
|
int fromIndex = Math.max(0, Math.toIntExact((page.getPageNumber() - 1) * page.getPageSize()));
|
||||||
|
if (fromIndex >= prepared.size()) {
|
||||||
|
return new Page<>(Collections.emptyList(), page.getPageNumber(), page.getPageSize(), total);
|
||||||
|
}
|
||||||
|
int toIndex = Math.min(prepared.size(), Math.toIntExact(fromIndex + page.getPageSize()));
|
||||||
|
return new Page<>(prepared.subList(fromIndex, toIndex), page.getPageNumber(), page.getPageSize(), total);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Result<Plugin> detail(String id) {
|
||||||
|
Plugin plugin = service.getMapper().selectOneWithRelationsById(id);
|
||||||
|
if (plugin != null) {
|
||||||
|
pluginVisibilityService.assertPluginVisible(plugin.getCreatedBy(), plugin.getId(), "无权限访问插件");
|
||||||
|
pluginService.preparePluginForCurrentUser(plugin);
|
||||||
|
}
|
||||||
|
return Result.ok(plugin);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyCategoryPermission(QueryWrapper queryWrapper) {
|
||||||
|
RoleCategoryAccessSnapshot access = categoryPermissionService.getCurrentAccess("PLUGIN");
|
||||||
|
if (!access.isRestricted()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (access.getCategoryIds().isEmpty()) {
|
||||||
|
queryWrapper.eq(Plugin::getCreatedBy, access.getAccountIdAsLong());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Set<BigInteger> pluginIds = pluginVisibilityService.getCurrentVisiblePluginIds();
|
||||||
|
if (pluginIds.isEmpty()) {
|
||||||
|
queryWrapper.eq(Plugin::getCreatedBy, access.getAccountIdAsLong());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
queryWrapper.and(PLUGIN.CREATED_BY.eq(access.getAccountIdAsLong()).or(PLUGIN.ID.in(pluginIds)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isAvailableOnly() {
|
||||||
|
HttpServletRequest request = currentHttpRequest();
|
||||||
|
return request != null && "true".equalsIgnoreCase(request.getParameter("availableOnly"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private HttpServletRequest currentHttpRequest() {
|
||||||
|
org.springframework.web.context.request.ServletRequestAttributes attributes =
|
||||||
|
(org.springframework.web.context.request.ServletRequestAttributes)
|
||||||
|
org.springframework.web.context.request.RequestContextHolder.getRequestAttributes();
|
||||||
|
return attributes == null ? null : attributes.getRequest();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean canBindWorkflowCandidate(Workflow workflow, LoginAccount loginAccount) {
|
||||||
|
if (workflow == null || loginAccount == null || loginAccount.getId() == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (workflow.getPublishedSnapshotJson() == null || workflow.getPublishedSnapshotJson().isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Workflow publishedWorkflow = workflowService.toPublishedView(workflow);
|
||||||
|
if (!workflowPluginSnapshotResolver.isSupportedForWorkflowPlugin(publishedWorkflow)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return resourceAccessService.canAccess(loginAccount, CategoryResourceType.WORKFLOW, workflow, ResourceAction.USE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +1,49 @@
|
|||||||
package tech.easyflow.admin.controller.ai;
|
package tech.easyflow.admin.controller.ai;
|
||||||
|
|
||||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
|
import cn.dev33.satoken.stp.StpUtil;
|
||||||
import cn.hutool.core.util.IdUtil;
|
import cn.hutool.core.util.IdUtil;
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import com.alibaba.fastjson2.JSON;
|
import com.alibaba.fastjson2.JSON;
|
||||||
import com.alibaba.fastjson2.JSONArray;
|
import com.alibaba.fastjson2.JSONArray;
|
||||||
import com.alibaba.fastjson2.JSONObject;
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
import com.easyagents.flow.core.chain.runtime.ChainExecutor;
|
||||||
import com.mybatisflex.core.query.QueryWrapper;
|
import com.mybatisflex.core.query.QueryWrapper;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import tech.easyflow.ai.easyagentsflow.entity.ChainInfo;
|
||||||
|
import tech.easyflow.ai.easyagentsflow.entity.NodeInfo;
|
||||||
|
import tech.easyflow.ai.easyagentsflow.entity.WorkflowCheckStage;
|
||||||
|
import tech.easyflow.ai.easyagentsflow.service.TinyFlowService;
|
||||||
|
import tech.easyflow.ai.easyagentsflow.service.WorkflowCheckService;
|
||||||
|
import tech.easyflow.ai.easyagentsflow.support.PublishedWorkflowDefinitionIds;
|
||||||
import tech.easyflow.ai.entity.BotPlugin;
|
import tech.easyflow.ai.entity.BotPlugin;
|
||||||
|
import tech.easyflow.ai.entity.Plugin;
|
||||||
import tech.easyflow.ai.entity.PluginItem;
|
import tech.easyflow.ai.entity.PluginItem;
|
||||||
|
import tech.easyflow.ai.entity.Workflow;
|
||||||
|
import tech.easyflow.ai.enums.PluginType;
|
||||||
|
import tech.easyflow.ai.plugin.workflow.snapshot.WorkflowPluginSnapshotResolver;
|
||||||
import tech.easyflow.ai.service.BotPluginService;
|
import tech.easyflow.ai.service.BotPluginService;
|
||||||
|
import tech.easyflow.ai.service.PluginService;
|
||||||
import tech.easyflow.ai.service.PluginItemService;
|
import tech.easyflow.ai.service.PluginItemService;
|
||||||
|
import tech.easyflow.ai.service.WorkflowService;
|
||||||
|
import tech.easyflow.common.constant.Constants;
|
||||||
import tech.easyflow.common.annotation.UsePermission;
|
import tech.easyflow.common.annotation.UsePermission;
|
||||||
import tech.easyflow.common.domain.Result;
|
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.controller.BaseCurdController;
|
||||||
|
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 控制层。
|
* 控制层。
|
||||||
@@ -45,6 +64,18 @@ public class PluginItemController extends BaseCurdController<PluginItemService,
|
|||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private BotPluginService botPluginService;
|
private BotPluginService botPluginService;
|
||||||
|
@Resource
|
||||||
|
private PluginService pluginService;
|
||||||
|
@Resource
|
||||||
|
private WorkflowPluginSnapshotResolver workflowPluginSnapshotResolver;
|
||||||
|
@Resource
|
||||||
|
private WorkflowService workflowService;
|
||||||
|
@Resource
|
||||||
|
private ChainExecutor chainExecutor;
|
||||||
|
@Resource
|
||||||
|
private TinyFlowService tinyFlowService;
|
||||||
|
@Resource
|
||||||
|
private WorkflowCheckService workflowCheckService;
|
||||||
|
|
||||||
@PostMapping("/tool/save")
|
@PostMapping("/tool/save")
|
||||||
@SaCheckPermission("/api/v1/plugin/save")
|
@SaCheckPermission("/api/v1/plugin/save")
|
||||||
@@ -87,8 +118,18 @@ public class PluginItemController extends BaseCurdController<PluginItemService,
|
|||||||
if (record == null) {
|
if (record == null) {
|
||||||
return Result.ok(nodeData);
|
return Result.ok(nodeData);
|
||||||
}
|
}
|
||||||
|
Plugin plugin = pluginService.getById(record.getPluginId());
|
||||||
nodeData.put("pluginId", record.getId().toString());
|
nodeData.put("pluginId", record.getId().toString());
|
||||||
nodeData.put("pluginName", record.getName());
|
nodeData.put("pluginName", record.getName());
|
||||||
|
if (plugin != null) {
|
||||||
|
plugin = pluginService.preparePluginForCurrentUser(plugin);
|
||||||
|
nodeData.put("pluginType", plugin.getType());
|
||||||
|
nodeData.put("workflowId", plugin.getWorkflowId());
|
||||||
|
nodeData.put("workflowTitle", plugin.getWorkflowTitle());
|
||||||
|
nodeData.put("available", plugin.getAvailable());
|
||||||
|
nodeData.put("reasonCode", plugin.getReasonCode());
|
||||||
|
nodeData.put("reasonMessage", plugin.getReasonMessage());
|
||||||
|
}
|
||||||
|
|
||||||
JSONArray parameters = new JSONArray();
|
JSONArray parameters = new JSONArray();
|
||||||
JSONArray outputDefs = new JSONArray();
|
JSONArray outputDefs = new JSONArray();
|
||||||
@@ -104,6 +145,7 @@ public class PluginItemController extends BaseCurdController<PluginItemService,
|
|||||||
handleArray(array);
|
handleArray(array);
|
||||||
outputDefs = array;
|
outputDefs = array;
|
||||||
}
|
}
|
||||||
|
nodeData.put("schemaHash", resolveSchemaHash(record, plugin));
|
||||||
nodeData.put("parameters", parameters);
|
nodeData.put("parameters", parameters);
|
||||||
nodeData.put("outputDefs", outputDefs);
|
nodeData.put("outputDefs", outputDefs);
|
||||||
return Result.ok(nodeData);
|
return Result.ok(nodeData);
|
||||||
@@ -119,6 +161,71 @@ public class PluginItemController extends BaseCurdController<PluginItemService,
|
|||||||
return pluginItemService.pluginToolTest(inputData, pluginToolId);
|
return pluginItemService.pluginToolTest(inputData, pluginToolId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作流插件异步试运行。
|
||||||
|
*
|
||||||
|
* @param inputData 输入参数 JSON
|
||||||
|
* @param pluginToolId 插件工具 ID
|
||||||
|
* @return 执行 ID
|
||||||
|
*/
|
||||||
|
@PostMapping("/testAsync")
|
||||||
|
@SaCheckPermission("/api/v1/plugin/query")
|
||||||
|
public Result<String> pluginToolTestAsync(@JsonBody(value = "inputData", required = true) String inputData,
|
||||||
|
@JsonBody(value = "pluginToolId", required = true) BigInteger pluginToolId) {
|
||||||
|
PluginItem pluginItem = pluginItemService.getById(pluginToolId);
|
||||||
|
Plugin plugin = requireWorkflowPlugin(pluginItem);
|
||||||
|
Plugin preparedPlugin = pluginService.preparePluginForCurrentUser(plugin);
|
||||||
|
if (Boolean.FALSE.equals(preparedPlugin.getAvailable())) {
|
||||||
|
throw new BusinessException(preparedPlugin.getReasonMessage());
|
||||||
|
}
|
||||||
|
Workflow workflow = workflowService.getPublishedById(preparedPlugin.getWorkflowId());
|
||||||
|
if (workflow == null) {
|
||||||
|
throw new BusinessException("未找到已发布工作流");
|
||||||
|
}
|
||||||
|
workflowCheckService.checkOrThrow(workflow.getContent(), WorkflowCheckStage.PRE_EXECUTE, workflow.getId());
|
||||||
|
Map<String, Object> variables = JSON.parseObject(inputData, Map.class);
|
||||||
|
if (variables == null) {
|
||||||
|
variables = new HashMap<>();
|
||||||
|
}
|
||||||
|
if (StpUtil.isLogin()) {
|
||||||
|
variables.put(Constants.LOGIN_USER_KEY, SaTokenUtil.getLoginAccount());
|
||||||
|
}
|
||||||
|
String executeId = chainExecutor.executeAsync(
|
||||||
|
PublishedWorkflowDefinitionIds.published(String.valueOf(workflow.getId())),
|
||||||
|
variables
|
||||||
|
);
|
||||||
|
return Result.ok(executeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取工作流插件试运行状态。
|
||||||
|
*
|
||||||
|
* @param executeId 执行 ID
|
||||||
|
* @param nodes 节点列表
|
||||||
|
* @return 链路状态
|
||||||
|
*/
|
||||||
|
@PostMapping("/testChainStatus")
|
||||||
|
@SaCheckPermission("/api/v1/plugin/query")
|
||||||
|
public Result<ChainInfo> pluginToolTestChainStatus(@JsonBody(value = "executeId", required = true) String executeId,
|
||||||
|
@JsonBody("nodes") List<NodeInfo> nodes) {
|
||||||
|
return Result.ok(tinyFlowService.getChainStatus(executeId, nodes));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 恢复工作流插件试运行。
|
||||||
|
*
|
||||||
|
* @param executeId 执行 ID
|
||||||
|
* @param confirmParams 恢复参数
|
||||||
|
* @return 空结果
|
||||||
|
*/
|
||||||
|
@PostMapping("/testResume")
|
||||||
|
@SaCheckPermission("/api/v1/plugin/query")
|
||||||
|
public Result<Void> pluginToolTestResume(@JsonBody(value = "executeId", required = true) String executeId,
|
||||||
|
@JsonBody("confirmParams") Map<String, Object> confirmParams) {
|
||||||
|
chainExecutor.resumeAsync(executeId, confirmParams);
|
||||||
|
return Result.ok();
|
||||||
|
}
|
||||||
|
|
||||||
private void handleArray(JSONArray array) {
|
private void handleArray(JSONArray array) {
|
||||||
for (Object o : array) {
|
for (Object o : array) {
|
||||||
JSONObject obj = (JSONObject) o;
|
JSONObject obj = (JSONObject) o;
|
||||||
@@ -134,6 +241,40 @@ public class PluginItemController extends BaseCurdController<PluginItemService,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String resolveSchemaHash(PluginItem record, Plugin plugin) {
|
||||||
|
if (record == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (StrUtil.isNotBlank(record.getSchemaHash())) {
|
||||||
|
return record.getSchemaHash();
|
||||||
|
}
|
||||||
|
if (plugin == null || !PluginType.isWorkflow(plugin.getType()) || plugin.getWorkflowId() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
tech.easyflow.ai.entity.Workflow workflow = workflowService.getPublishedById(plugin.getWorkflowId());
|
||||||
|
if (workflow == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return workflowPluginSnapshotResolver.resolveSchemaHash(workflow);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Plugin requireWorkflowPlugin(PluginItem pluginItem) {
|
||||||
|
if (pluginItem == null) {
|
||||||
|
throw new BusinessException("插件工具不存在");
|
||||||
|
}
|
||||||
|
Plugin plugin = pluginService.getById(pluginItem.getPluginId());
|
||||||
|
if (plugin == null) {
|
||||||
|
throw new BusinessException("插件不存在");
|
||||||
|
}
|
||||||
|
if (!PluginType.isWorkflow(plugin.getType())) {
|
||||||
|
throw new BusinessException("当前工具不是工作流插件");
|
||||||
|
}
|
||||||
|
if (plugin.getWorkflowId() == null) {
|
||||||
|
throw new BusinessException("插件未绑定工作流");
|
||||||
|
}
|
||||||
|
return plugin;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Result<?> onRemoveBefore(Collection<Serializable> ids) {
|
protected Result<?> onRemoveBefore(Collection<Serializable> ids) {
|
||||||
|
|
||||||
@@ -144,6 +285,15 @@ public class PluginItemController extends BaseCurdController<PluginItemService,
|
|||||||
if (exists){
|
if (exists){
|
||||||
return Result.fail(1, "此工具还关联着bot,请先取消关联!");
|
return Result.fail(1, "此工具还关联着bot,请先取消关联!");
|
||||||
}
|
}
|
||||||
|
if (ids.size() == 1) {
|
||||||
|
PluginItem pluginItem = pluginItemService.getById(ids.iterator().next());
|
||||||
|
if (pluginItem != null) {
|
||||||
|
Plugin plugin = pluginService.getById(pluginItem.getPluginId());
|
||||||
|
if (plugin != null && PluginType.isWorkflow(plugin.getType())) {
|
||||||
|
return Result.fail(1, "工作流插件工具由系统自动维护,不支持删除");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package tech.easyflow.admin.controller.ai;
|
||||||
|
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import tech.easyflow.chatlog.domain.dto.ChatMessageRecord;
|
||||||
|
import tech.easyflow.chatlog.domain.dto.PublicChatSessionRestoreResult;
|
||||||
|
import tech.easyflow.chatlog.service.PublicChatSessionRestoreService;
|
||||||
|
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 java.math.BigInteger;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/public-chat")
|
||||||
|
public class PublicChatSessionController {
|
||||||
|
|
||||||
|
private final PublicChatSessionRestoreService publicChatSessionRestoreService;
|
||||||
|
|
||||||
|
public PublicChatSessionController(PublicChatSessionRestoreService publicChatSessionRestoreService) {
|
||||||
|
this.publicChatSessionRestoreService = publicChatSessionRestoreService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/session/restore")
|
||||||
|
public Result<PublicChatSessionRestoreResult> restoreSession(BigInteger botId,
|
||||||
|
BigInteger conversationId,
|
||||||
|
Integer limit) {
|
||||||
|
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||||
|
BigInteger userId = account == null ? null : account.getId();
|
||||||
|
PublicChatSessionRestoreResult result = publicChatSessionRestoreService.restoreSession(
|
||||||
|
userId,
|
||||||
|
botId,
|
||||||
|
conversationId,
|
||||||
|
limit
|
||||||
|
);
|
||||||
|
return Result.ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/session/{sessionId}/rounds/{roundId}/variants")
|
||||||
|
public Result<List<ChatMessageRecord>> listRoundVariants(@PathVariable BigInteger sessionId,
|
||||||
|
@PathVariable BigInteger roundId,
|
||||||
|
BigInteger botId) {
|
||||||
|
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||||
|
BigInteger userId = account == null ? null : account.getId();
|
||||||
|
return Result.ok(publicChatSessionRestoreService.listVariants(userId, botId, sessionId, roundId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/session/{sessionId}/rounds/{roundId}/selectVariant")
|
||||||
|
public Result<ChatMessageRecord> selectRoundVariant(@PathVariable BigInteger sessionId,
|
||||||
|
@PathVariable BigInteger roundId,
|
||||||
|
BigInteger botId,
|
||||||
|
@JsonBody(value = "variantIndex", required = true) Integer variantIndex) {
|
||||||
|
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||||
|
BigInteger userId = account == null ? null : account.getId();
|
||||||
|
BigInteger operatorId = account == null ? BigInteger.ZERO : account.getId();
|
||||||
|
return Result.ok(publicChatSessionRestoreService.selectVariant(userId, botId, sessionId, roundId, variantIndex, operatorId));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,20 @@
|
|||||||
package tech.easyflow.admin.controller.ai;
|
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.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import tech.easyflow.ai.entity.ResourceCategory;
|
import tech.easyflow.ai.entity.ResourceCategory;
|
||||||
import tech.easyflow.ai.service.ResourceCategoryService;
|
import tech.easyflow.ai.service.ResourceCategoryService;
|
||||||
import tech.easyflow.common.annotation.UsePermission;
|
import tech.easyflow.common.annotation.UsePermission;
|
||||||
|
import tech.easyflow.common.domain.Result;
|
||||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||||
|
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
|
||||||
|
import tech.easyflow.system.service.CategoryPermissionService;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 素材分类
|
* 素材分类
|
||||||
@@ -14,9 +23,24 @@ import tech.easyflow.common.web.controller.BaseCurdController;
|
|||||||
@RequestMapping("/api/v1/resourceCategory")
|
@RequestMapping("/api/v1/resourceCategory")
|
||||||
@UsePermission(moduleName = "/api/v1/resource")
|
@UsePermission(moduleName = "/api/v1/resource")
|
||||||
public class ResourceCategoryController extends BaseCurdController<ResourceCategoryService, ResourceCategory> {
|
public class ResourceCategoryController extends BaseCurdController<ResourceCategoryService, ResourceCategory> {
|
||||||
|
@Resource
|
||||||
|
private CategoryPermissionService categoryPermissionService;
|
||||||
|
|
||||||
public ResourceCategoryController(ResourceCategoryService service) {
|
public ResourceCategoryController(ResourceCategoryService service) {
|
||||||
super(service);
|
super(service);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
@GetMapping("visibleList")
|
||||||
|
public Result<List<ResourceCategory>> visibleList(ResourceCategory entity, Boolean asTree, String sortKey, String sortType) {
|
||||||
|
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
|
||||||
|
RoleCategoryAccessSnapshot access = categoryPermissionService.getCurrentAccess("RESOURCE");
|
||||||
|
if (access.isRestricted()) {
|
||||||
|
if (access.getCategoryIds().isEmpty()) {
|
||||||
|
return Result.ok(Collections.emptyList());
|
||||||
|
}
|
||||||
|
queryWrapper.in(ResourceCategory::getId, access.getCategoryIds());
|
||||||
|
}
|
||||||
|
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||||
|
return Result.ok(service.list(queryWrapper));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,18 +4,28 @@ import cn.hutool.core.io.FileTypeUtil;
|
|||||||
import cn.hutool.http.HttpUtil;
|
import cn.hutool.http.HttpUtil;
|
||||||
import com.mybatisflex.core.paginate.Page;
|
import com.mybatisflex.core.paginate.Page;
|
||||||
import com.mybatisflex.core.query.QueryWrapper;
|
import com.mybatisflex.core.query.QueryWrapper;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import tech.easyflow.admin.model.ai.ResourcePreviewVo;
|
||||||
import tech.easyflow.ai.entity.Resource;
|
import tech.easyflow.ai.entity.Resource;
|
||||||
import tech.easyflow.ai.service.ResourceService;
|
import tech.easyflow.ai.service.ResourceService;
|
||||||
|
import tech.easyflow.ai.utils.DocUtil;
|
||||||
import tech.easyflow.common.domain.Result;
|
import tech.easyflow.common.domain.Result;
|
||||||
import tech.easyflow.common.entity.LoginAccount;
|
import tech.easyflow.common.entity.LoginAccount;
|
||||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||||
|
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||||
|
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
|
||||||
|
import tech.easyflow.system.service.CategoryPermissionService;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static tech.easyflow.ai.entity.table.ResourceTableDef.RESOURCE;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 素材库
|
* 素材库
|
||||||
@@ -26,6 +36,11 @@ import java.util.Date;
|
|||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/resource")
|
@RequestMapping("/api/v1/resource")
|
||||||
public class ResourceController extends BaseCurdController<ResourceService, Resource> {
|
public class ResourceController extends BaseCurdController<ResourceService, Resource> {
|
||||||
|
private static final int RESOURCE_PREVIEW_CONTENT_LIMIT = 20_000;
|
||||||
|
|
||||||
|
@javax.annotation.Resource
|
||||||
|
private CategoryPermissionService categoryPermissionService;
|
||||||
|
|
||||||
public ResourceController(ResourceService service) {
|
public ResourceController(ResourceService service) {
|
||||||
super(service);
|
super(service);
|
||||||
}
|
}
|
||||||
@@ -50,7 +65,89 @@ public class ResourceController extends BaseCurdController<ResourceService, Reso
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Page<Resource> queryPage(Page<Resource> page, QueryWrapper queryWrapper) {
|
protected Page<Resource> queryPage(Page<Resource> page, QueryWrapper queryWrapper) {
|
||||||
queryWrapper.eq(Resource::getCreatedBy, SaTokenUtil.getLoginAccount().getId().toString());
|
applyCategoryPermission(queryWrapper);
|
||||||
return super.queryPage(page, queryWrapper);
|
return super.queryPage(page, queryWrapper);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
@Override
|
||||||
|
public Result<List<Resource>> list(Resource entity, Boolean asTree, String sortKey, String sortType) {
|
||||||
|
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
|
||||||
|
applyCategoryPermission(queryWrapper);
|
||||||
|
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||||
|
return Result.ok(service.list(queryWrapper));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Result<Resource> detail(String id) {
|
||||||
|
Resource resource = service.getById(id);
|
||||||
|
if (resource != null) {
|
||||||
|
categoryPermissionService.assertCategoryResourceVisible("RESOURCE", resource.getCreatedBy(), resource.getCategoryId(), "无权限访问素材");
|
||||||
|
}
|
||||||
|
return Result.ok(resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取素材预览内容。
|
||||||
|
*
|
||||||
|
* @param id 素材ID
|
||||||
|
* @return 预览文本
|
||||||
|
*/
|
||||||
|
@GetMapping("/previewContent")
|
||||||
|
public Result<ResourcePreviewVo> previewContent(String id) {
|
||||||
|
Resource resource = service.getById(id);
|
||||||
|
if (resource == null) {
|
||||||
|
throw new BusinessException("素材不存在");
|
||||||
|
}
|
||||||
|
categoryPermissionService.assertCategoryResourceVisible("RESOURCE", resource.getCreatedBy(), resource.getCategoryId(), "无权限访问素材");
|
||||||
|
|
||||||
|
byte[] bytes = DocUtil.downloadFile(resource.getResourceUrl());
|
||||||
|
String suffix = resolvePreviewSuffix(resource, bytes);
|
||||||
|
String content = DocUtil.readPreviewContent(suffix, new ByteArrayInputStream(bytes));
|
||||||
|
return Result.ok(buildPreviewVo(content));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyCategoryPermission(QueryWrapper queryWrapper) {
|
||||||
|
RoleCategoryAccessSnapshot access = categoryPermissionService.getCurrentAccess("RESOURCE");
|
||||||
|
if (!access.isRestricted()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (access.getCategoryIds().isEmpty()) {
|
||||||
|
queryWrapper.eq(Resource::getCreatedBy, access.getAccountId());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
queryWrapper.and(RESOURCE.CREATED_BY.eq(access.getAccountId()).or(RESOURCE.CATEGORY_ID.in(access.getCategoryIds())));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析用于预览的文件后缀。
|
||||||
|
*
|
||||||
|
* @param resource 素材实体
|
||||||
|
* @param bytes 文件字节
|
||||||
|
* @return 标准化后的文件后缀
|
||||||
|
*/
|
||||||
|
private String resolvePreviewSuffix(Resource resource, byte[] bytes) {
|
||||||
|
if (StringUtils.hasText(resource.getSuffix())) {
|
||||||
|
return DocUtil.normalizeSuffix(resource.getSuffix());
|
||||||
|
}
|
||||||
|
String detectedSuffix = FileTypeUtil.getType(new ByteArrayInputStream(bytes), resource.getResourceUrl());
|
||||||
|
if (!StringUtils.hasText(detectedSuffix)) {
|
||||||
|
throw new BusinessException("无法识别当前素材文件类型");
|
||||||
|
}
|
||||||
|
return DocUtil.normalizeSuffix(detectedSuffix);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建预览返回对象,并在服务端截断超长内容。
|
||||||
|
*
|
||||||
|
* @param content 原始内容
|
||||||
|
* @return 预览内容对象
|
||||||
|
*/
|
||||||
|
private ResourcePreviewVo buildPreviewVo(String content) {
|
||||||
|
String safeContent = content == null ? "" : content;
|
||||||
|
boolean truncated = safeContent.length() > RESOURCE_PREVIEW_CONTENT_LIMIT;
|
||||||
|
ResourcePreviewVo vo = new ResourcePreviewVo();
|
||||||
|
vo.setContent(truncated ? safeContent.substring(0, RESOURCE_PREVIEW_CONTENT_LIMIT) : safeContent);
|
||||||
|
vo.setTruncated(truncated);
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,974 @@
|
|||||||
|
package tech.easyflow.admin.controller.ai;
|
||||||
|
|
||||||
|
import cn.hutool.core.io.IoUtil;
|
||||||
|
import com.easyagents.core.model.embedding.EmbeddingModel;
|
||||||
|
import com.easyagents.core.store.DocumentStore;
|
||||||
|
import com.easyagents.core.store.StoreOptions;
|
||||||
|
import com.easyagents.core.store.StoreResult;
|
||||||
|
import com.mybatisflex.core.paginate.Page;
|
||||||
|
import com.mybatisflex.core.query.QueryWrapper;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import org.springframework.core.io.ClassPathResource;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
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.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
import tech.easyflow.ai.documentimport.DocumentImportDtos;
|
||||||
|
import tech.easyflow.ai.documentimport.task.DocumentImportTaskStatusStreamService;
|
||||||
|
import tech.easyflow.ai.dto.KnowledgeShareLimitedConfigRequest;
|
||||||
|
import tech.easyflow.ai.dto.KnowledgeSearchResultItem;
|
||||||
|
import tech.easyflow.ai.entity.Document;
|
||||||
|
import tech.easyflow.ai.entity.DocumentChunk;
|
||||||
|
import tech.easyflow.ai.entity.DocumentCollection;
|
||||||
|
import tech.easyflow.ai.entity.DocumentCollectionSplitParams;
|
||||||
|
import tech.easyflow.ai.entity.FaqCategory;
|
||||||
|
import tech.easyflow.ai.entity.FaqItem;
|
||||||
|
import tech.easyflow.ai.entity.Model;
|
||||||
|
import tech.easyflow.ai.enums.KnowledgeShareActionScope;
|
||||||
|
import tech.easyflow.ai.rag.KnowledgeRetrievalModes;
|
||||||
|
import tech.easyflow.ai.rag.KnowledgeRetrievalRequest;
|
||||||
|
import tech.easyflow.ai.service.DocumentChunkService;
|
||||||
|
import tech.easyflow.ai.service.DocumentCollectionService;
|
||||||
|
import tech.easyflow.ai.service.DocumentService;
|
||||||
|
import tech.easyflow.ai.service.FaqCategoryService;
|
||||||
|
import tech.easyflow.ai.service.FaqItemService;
|
||||||
|
import tech.easyflow.ai.service.KnowledgeEmbeddingService;
|
||||||
|
import tech.easyflow.ai.service.KnowledgeShareAuditService;
|
||||||
|
import tech.easyflow.ai.service.KnowledgeShareService;
|
||||||
|
import tech.easyflow.ai.service.ModelService;
|
||||||
|
import tech.easyflow.ai.support.DocumentStoreLifecycleSupport;
|
||||||
|
import tech.easyflow.ai.vo.FaqImportResultVo;
|
||||||
|
import tech.easyflow.ai.vo.KnowledgeShareAuthContext;
|
||||||
|
import tech.easyflow.ai.vo.KnowledgeShareViewDetail;
|
||||||
|
import tech.easyflow.common.domain.Result;
|
||||||
|
import tech.easyflow.common.filestorage.FileStorageService;
|
||||||
|
import tech.easyflow.common.vo.UploadResVo;
|
||||||
|
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||||
|
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 内部员工登录态下的知识库分享访问接口。
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/share/knowledge")
|
||||||
|
public class ShareKnowledgeController {
|
||||||
|
|
||||||
|
private static final long MAX_IMAGE_SIZE_BYTES = 5L * 1024L * 1024L;
|
||||||
|
private static final Set<String> ALLOWED_IMAGE_TYPES = new HashSet<>(Arrays.asList(
|
||||||
|
"image/jpeg",
|
||||||
|
"image/png",
|
||||||
|
"image/webp",
|
||||||
|
"image/gif"
|
||||||
|
));
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private KnowledgeShareService knowledgeShareService;
|
||||||
|
@Resource
|
||||||
|
private KnowledgeShareAuditService knowledgeShareAuditService;
|
||||||
|
@Resource
|
||||||
|
private DocumentCollectionService documentCollectionService;
|
||||||
|
@Resource
|
||||||
|
private DocumentService documentService;
|
||||||
|
@Resource
|
||||||
|
private DocumentChunkService documentChunkService;
|
||||||
|
@Resource
|
||||||
|
private ModelService modelService;
|
||||||
|
@Resource
|
||||||
|
private FaqItemService faqItemService;
|
||||||
|
@Resource
|
||||||
|
private FaqCategoryService faqCategoryService;
|
||||||
|
@Resource
|
||||||
|
private KnowledgeEmbeddingService knowledgeEmbeddingService;
|
||||||
|
@Resource(name = "default")
|
||||||
|
private FileStorageService fileStorageService;
|
||||||
|
@Resource
|
||||||
|
private DocumentImportTaskStatusStreamService documentImportTaskStatusStreamService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取知识库详情。
|
||||||
|
*
|
||||||
|
* @param shareKey 分享访问密钥
|
||||||
|
* @return 知识库详情
|
||||||
|
*/
|
||||||
|
@GetMapping("/documentCollection/detail")
|
||||||
|
public Result<KnowledgeShareViewDetail> detail(@RequestParam String shareKey) {
|
||||||
|
KnowledgeShareAuthContext context = knowledgeShareService.assertUrlShareAccess(
|
||||||
|
shareKey,
|
||||||
|
null,
|
||||||
|
KnowledgeShareActionScope.VIEW.name()
|
||||||
|
);
|
||||||
|
audit(context, "访问知识库分享页", "KNOWLEDGE_SHARE_URL_ACCESS", false, auditDetail("knowledgeId", context.getKnowledge().getId()));
|
||||||
|
KnowledgeShareViewDetail detail = new KnowledgeShareViewDetail();
|
||||||
|
detail.setKnowledge(context.getKnowledge());
|
||||||
|
detail.setPermissionScopes(new java.util.ArrayList<String>(context.getShare().getPermissionScopeSet()));
|
||||||
|
return Result.ok(detail);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取模型列表。
|
||||||
|
*
|
||||||
|
* @param shareKey 分享访问密钥
|
||||||
|
* @param modelType 模型类型
|
||||||
|
* @return 模型列表
|
||||||
|
*/
|
||||||
|
@GetMapping("/documentCollection/modelList")
|
||||||
|
public Result<List<Model>> modelList(
|
||||||
|
@RequestParam String shareKey,
|
||||||
|
@RequestParam String modelType
|
||||||
|
) {
|
||||||
|
knowledgeShareService.assertUrlShareAccess(shareKey, null, KnowledgeShareActionScope.CONFIG_UPDATE.name());
|
||||||
|
Model entity = new Model();
|
||||||
|
entity.setModelType(modelType);
|
||||||
|
return Result.ok(modelService.listSelectableModels(entity, false, null, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新受限配置。
|
||||||
|
*
|
||||||
|
* @param request 更新请求
|
||||||
|
* @param shareKey 分享访问密钥
|
||||||
|
* @return 结果
|
||||||
|
*/
|
||||||
|
@PostMapping("/documentCollection/shareConfigUpdate")
|
||||||
|
public Result<Void> updateConfig(
|
||||||
|
@JsonBody KnowledgeShareLimitedConfigRequest request,
|
||||||
|
@RequestParam String shareKey
|
||||||
|
) {
|
||||||
|
KnowledgeShareAuthContext context = knowledgeShareService.assertUrlShareAccess(
|
||||||
|
shareKey,
|
||||||
|
request == null ? null : request.getKnowledgeId(),
|
||||||
|
KnowledgeShareActionScope.CONFIG_UPDATE.name()
|
||||||
|
);
|
||||||
|
BigInteger knowledgeId = resolveKnowledgeId(context, request == null ? null : request.getKnowledgeId());
|
||||||
|
DocumentCollection current = context.getKnowledge();
|
||||||
|
DocumentCollection update = new DocumentCollection();
|
||||||
|
update.setId(current.getId());
|
||||||
|
Map<String, Object> options = current.getOptions() == null
|
||||||
|
? new HashMap<>()
|
||||||
|
: new HashMap<>(current.getOptions());
|
||||||
|
if (request.getVectorEmbedModelId() != null) {
|
||||||
|
update.setVectorEmbedModelId(request.getVectorEmbedModelId());
|
||||||
|
options.put(DocumentCollection.KEY_CAN_UPDATE_EMBEDDING_MODEL, true);
|
||||||
|
Model model = modelService.getModelInstance(request.getVectorEmbedModelId());
|
||||||
|
if (model != null) {
|
||||||
|
update.setDimensionOfVectorModel(Model.getEmbeddingDimension(model.toEmbeddingModel()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (request.getRerankModelId() != null || current.getRerankModelId() != null) {
|
||||||
|
update.setRerankModelId(request.getRerankModelId());
|
||||||
|
}
|
||||||
|
if (request.getRerankEnable() != null) {
|
||||||
|
options.put(DocumentCollection.KEY_RERANK_ENABLE, request.getRerankEnable());
|
||||||
|
}
|
||||||
|
if (request.getDocRecallMaxNum() != null) {
|
||||||
|
options.put(DocumentCollection.KEY_DOC_RECALL_MAX_NUM, request.getDocRecallMaxNum());
|
||||||
|
}
|
||||||
|
if (request.getSimThreshold() != null) {
|
||||||
|
options.put(DocumentCollection.KEY_SIMILARITY_THRESHOLD, BigDecimal.valueOf(request.getSimThreshold()));
|
||||||
|
}
|
||||||
|
update.setOptions(options);
|
||||||
|
documentCollectionService.updateById(update);
|
||||||
|
if (Boolean.TRUE.equals(request.getRebuildVectors())) {
|
||||||
|
knowledgeEmbeddingService.rebuildKnowledgeVectors(knowledgeId);
|
||||||
|
}
|
||||||
|
audit(context, "更新知识库分享受限配置", "KNOWLEDGE_SHARE_URL_WRITE", true, auditDetail("knowledgeId", knowledgeId));
|
||||||
|
return Result.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检索知识库。
|
||||||
|
*
|
||||||
|
* @param shareKey 分享访问密钥
|
||||||
|
* @param keyword 检索关键词
|
||||||
|
* @param retrievalMode 检索模式
|
||||||
|
* @return 检索结果
|
||||||
|
*/
|
||||||
|
@GetMapping("/documentCollection/search")
|
||||||
|
public Result<List<KnowledgeSearchResultItem>> search(
|
||||||
|
@RequestParam String shareKey,
|
||||||
|
@RequestParam String keyword,
|
||||||
|
@RequestParam(required = false) String retrievalMode
|
||||||
|
) {
|
||||||
|
KnowledgeShareAuthContext context = knowledgeShareService.assertUrlShareAccess(
|
||||||
|
shareKey,
|
||||||
|
null,
|
||||||
|
KnowledgeShareActionScope.SEARCH.name()
|
||||||
|
);
|
||||||
|
KnowledgeRetrievalRequest request = new KnowledgeRetrievalRequest();
|
||||||
|
request.setKnowledgeId(context.getKnowledge().getId());
|
||||||
|
request.setQuery(keyword);
|
||||||
|
request.setRetrievalMode(KnowledgeRetrievalModes.parse(retrievalMode));
|
||||||
|
request.setCallerType("SHARE_URL");
|
||||||
|
request.setCallerId(String.valueOf(context.getShare().getId()));
|
||||||
|
return Result.ok(toKnowledgeSearchResult(documentCollectionService.search(request)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文档分页。
|
||||||
|
*/
|
||||||
|
@GetMapping("/document/documentList")
|
||||||
|
public Result<Page<Document>> documentPage(
|
||||||
|
@RequestParam String shareKey,
|
||||||
|
@RequestParam(required = false) String title,
|
||||||
|
@RequestParam(defaultValue = "10") int pageSize,
|
||||||
|
@RequestParam(defaultValue = "1") int pageNumber
|
||||||
|
) {
|
||||||
|
KnowledgeShareAuthContext context = knowledgeShareService.assertUrlShareAccess(
|
||||||
|
shareKey,
|
||||||
|
null,
|
||||||
|
KnowledgeShareActionScope.VIEW.name()
|
||||||
|
);
|
||||||
|
return Result.ok(documentService.getDocumentList(context.getKnowledge().getId().toString(), pageSize, pageNumber, title));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订阅分享知识库的文档任务状态流。
|
||||||
|
*
|
||||||
|
* @param shareKey 分享访问密钥
|
||||||
|
* @param knowledgeId 知识库 ID
|
||||||
|
* @return SSE 推送连接
|
||||||
|
*/
|
||||||
|
@PostMapping(value = "/document/import/task/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||||
|
public SseEmitter streamDocumentTask(
|
||||||
|
@RequestParam String shareKey,
|
||||||
|
@JsonBody(value = "knowledgeId", required = true) BigInteger knowledgeId
|
||||||
|
) {
|
||||||
|
KnowledgeShareAuthContext context = knowledgeShareService.assertUrlShareAccess(
|
||||||
|
shareKey,
|
||||||
|
knowledgeId,
|
||||||
|
KnowledgeShareActionScope.VIEW.name()
|
||||||
|
);
|
||||||
|
return documentImportTaskStatusStreamService.subscribe(context.getKnowledge().getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载文档。
|
||||||
|
*/
|
||||||
|
@GetMapping("/document/download")
|
||||||
|
public void documentDownload(
|
||||||
|
@RequestParam String shareKey,
|
||||||
|
@RequestParam BigInteger documentId,
|
||||||
|
HttpServletResponse response
|
||||||
|
) throws Exception {
|
||||||
|
KnowledgeShareAuthContext context = knowledgeShareService.assertUrlShareAccess(
|
||||||
|
shareKey,
|
||||||
|
null,
|
||||||
|
KnowledgeShareActionScope.VIEW.name()
|
||||||
|
);
|
||||||
|
Document document = documentService.getById(documentId);
|
||||||
|
if (document == null || document.getCollectionId() == null
|
||||||
|
|| document.getCollectionId().compareTo(context.getKnowledge().getId()) != 0) {
|
||||||
|
throw new BusinessException("文档不存在");
|
||||||
|
}
|
||||||
|
response.setContentType("application/octet-stream");
|
||||||
|
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
|
||||||
|
String fileName = URLEncoder.encode(document.getTitle(), StandardCharsets.UTF_8).replaceAll("\\+", "%20");
|
||||||
|
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName);
|
||||||
|
try (InputStream inputStream = fileStorageService.readStream(document.getDocumentPath())) {
|
||||||
|
IoUtil.copy(inputStream, response.getOutputStream());
|
||||||
|
response.flushBuffer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除文档。
|
||||||
|
*/
|
||||||
|
@PostMapping("/document/removeDoc")
|
||||||
|
public Result<?> removeDocument(
|
||||||
|
@RequestParam String shareKey,
|
||||||
|
@JsonBody("id") String id
|
||||||
|
) {
|
||||||
|
KnowledgeShareAuthContext context = knowledgeShareService.assertUrlShareAccess(
|
||||||
|
shareKey,
|
||||||
|
null,
|
||||||
|
KnowledgeShareActionScope.CONTENT_DELETE.name()
|
||||||
|
);
|
||||||
|
Document document = documentService.getById(new BigInteger(id));
|
||||||
|
if (document == null || document.getCollectionId() == null
|
||||||
|
|| document.getCollectionId().compareTo(context.getKnowledge().getId()) != 0) {
|
||||||
|
throw new BusinessException("文档不存在");
|
||||||
|
}
|
||||||
|
boolean success = documentService.removeDoc(id);
|
||||||
|
audit(context, "删除分享文档", "KNOWLEDGE_SHARE_URL_WRITE", true,
|
||||||
|
auditDetail("knowledgeId", context.getKnowledge().getId(), "documentId", id));
|
||||||
|
return Result.ok(success);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文档导入分析。
|
||||||
|
*/
|
||||||
|
@PostMapping("/document/import/analyze")
|
||||||
|
public Result<DocumentImportDtos.AnalyzeResponse> analyzeImport(
|
||||||
|
@RequestParam String shareKey,
|
||||||
|
@JsonBody DocumentImportDtos.AnalyzeRequest request
|
||||||
|
) {
|
||||||
|
KnowledgeShareAuthContext context = knowledgeShareService.assertUrlShareAccess(
|
||||||
|
shareKey,
|
||||||
|
request == null ? null : request.getKnowledgeId(),
|
||||||
|
KnowledgeShareActionScope.CONTENT_CREATE.name()
|
||||||
|
);
|
||||||
|
BigInteger knowledgeId = resolveKnowledgeId(context, request == null ? null : request.getKnowledgeId());
|
||||||
|
request.setKnowledgeId(knowledgeId);
|
||||||
|
audit(context, "分析分享文档导入", "KNOWLEDGE_SHARE_URL_WRITE", true, auditDetail("knowledgeId", knowledgeId));
|
||||||
|
return documentService.analyzeImport(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文档导入预览。
|
||||||
|
*/
|
||||||
|
@PostMapping("/document/import/preview")
|
||||||
|
public Result<DocumentImportDtos.PreviewResponse> previewImport(
|
||||||
|
@RequestParam String shareKey,
|
||||||
|
@JsonBody DocumentImportDtos.PreviewRequest request
|
||||||
|
) {
|
||||||
|
KnowledgeShareAuthContext context = knowledgeShareService.assertUrlShareAccess(
|
||||||
|
shareKey,
|
||||||
|
request == null ? null : request.getKnowledgeId(),
|
||||||
|
KnowledgeShareActionScope.CONTENT_CREATE.name()
|
||||||
|
);
|
||||||
|
BigInteger knowledgeId = resolveKnowledgeId(context, request == null ? null : request.getKnowledgeId());
|
||||||
|
request.setKnowledgeId(knowledgeId);
|
||||||
|
audit(context, "预览分享文档导入", "KNOWLEDGE_SHARE_URL_WRITE", true, auditDetail("knowledgeId", knowledgeId));
|
||||||
|
return documentService.previewImport(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交文档导入。
|
||||||
|
*/
|
||||||
|
@PostMapping("/document/import/commit")
|
||||||
|
public Result<DocumentImportDtos.CommitResponse> commitImport(
|
||||||
|
@RequestParam String shareKey,
|
||||||
|
@JsonBody DocumentImportDtos.CommitRequest request
|
||||||
|
) {
|
||||||
|
KnowledgeShareAuthContext context = knowledgeShareService.assertUrlShareAccess(
|
||||||
|
shareKey,
|
||||||
|
request == null ? null : request.getKnowledgeId(),
|
||||||
|
KnowledgeShareActionScope.CONTENT_CREATE.name()
|
||||||
|
);
|
||||||
|
BigInteger knowledgeId = resolveKnowledgeId(context, request == null ? null : request.getKnowledgeId());
|
||||||
|
request.setKnowledgeId(knowledgeId);
|
||||||
|
audit(context, "提交分享文档导入", "KNOWLEDGE_SHARE_URL_WRITE", true, auditDetail("knowledgeId", knowledgeId));
|
||||||
|
return documentService.commitImport(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/document/import/task/create")
|
||||||
|
public Result<DocumentImportDtos.TaskCreateResponse> createImportTask(
|
||||||
|
@RequestParam String shareKey,
|
||||||
|
@JsonBody DocumentImportDtos.TaskCreateRequest request
|
||||||
|
) {
|
||||||
|
KnowledgeShareAuthContext context = knowledgeShareService.assertUrlShareAccess(
|
||||||
|
shareKey,
|
||||||
|
request == null ? null : request.getKnowledgeId(),
|
||||||
|
KnowledgeShareActionScope.CONTENT_CREATE.name()
|
||||||
|
);
|
||||||
|
BigInteger knowledgeId = resolveKnowledgeId(context, request == null ? null : request.getKnowledgeId());
|
||||||
|
request.setKnowledgeId(knowledgeId);
|
||||||
|
audit(context, "创建分享文档导入任务", "KNOWLEDGE_SHARE_URL_WRITE", true, auditDetail("knowledgeId", knowledgeId));
|
||||||
|
return documentService.createImportTask(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/document/import/task/detail")
|
||||||
|
public Result<DocumentImportDtos.TaskDetailResponse> getImportTaskDetail(
|
||||||
|
@RequestParam String shareKey,
|
||||||
|
@RequestParam BigInteger taskId
|
||||||
|
) {
|
||||||
|
KnowledgeShareAuthContext context = knowledgeShareService.assertUrlShareAccess(
|
||||||
|
shareKey,
|
||||||
|
null,
|
||||||
|
KnowledgeShareActionScope.VIEW.name()
|
||||||
|
);
|
||||||
|
Result<DocumentImportDtos.TaskDetailResponse> result = documentService.getImportTaskDetail(taskId);
|
||||||
|
BigInteger knowledgeId = result.getData() == null ? null : result.getData().getKnowledgeId();
|
||||||
|
if (knowledgeId == null || knowledgeId.compareTo(context.getKnowledge().getId()) != 0) {
|
||||||
|
throw new BusinessException("任务不存在");
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/document/import/task/preview")
|
||||||
|
public Result<DocumentImportDtos.PreviewResponse> previewImportTask(
|
||||||
|
@RequestParam String shareKey,
|
||||||
|
@JsonBody DocumentImportDtos.PreviewRequest request
|
||||||
|
) {
|
||||||
|
KnowledgeShareAuthContext context = knowledgeShareService.assertUrlShareAccess(
|
||||||
|
shareKey,
|
||||||
|
request == null ? null : request.getKnowledgeId(),
|
||||||
|
KnowledgeShareActionScope.CONTENT_CREATE.name()
|
||||||
|
);
|
||||||
|
BigInteger knowledgeId = resolveKnowledgeId(context, request == null ? null : request.getKnowledgeId());
|
||||||
|
request.setKnowledgeId(knowledgeId);
|
||||||
|
audit(context, "预览分享文档分块", "KNOWLEDGE_SHARE_URL_WRITE", true, auditDetail("knowledgeId", knowledgeId));
|
||||||
|
return documentService.previewImportTask(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/document/import/task/startIndex")
|
||||||
|
public Result<DocumentImportDtos.TaskStartIndexResponse> startIndexTask(
|
||||||
|
@RequestParam String shareKey,
|
||||||
|
@JsonBody DocumentImportDtos.TaskStartIndexRequest request
|
||||||
|
) {
|
||||||
|
KnowledgeShareAuthContext context = knowledgeShareService.assertUrlShareAccess(
|
||||||
|
shareKey,
|
||||||
|
request == null ? null : request.getKnowledgeId(),
|
||||||
|
KnowledgeShareActionScope.CONTENT_CREATE.name()
|
||||||
|
);
|
||||||
|
BigInteger knowledgeId = resolveKnowledgeId(context, request == null ? null : request.getKnowledgeId());
|
||||||
|
request.setKnowledgeId(knowledgeId);
|
||||||
|
audit(context, "启动分享文档向量化", "KNOWLEDGE_SHARE_URL_WRITE", true, auditDetail("knowledgeId", knowledgeId));
|
||||||
|
return documentService.startIndexTask(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/document/import/task/retryParse")
|
||||||
|
public Result<DocumentImportDtos.TaskStartIndexResponse> retryParseTask(
|
||||||
|
@RequestParam String shareKey,
|
||||||
|
@JsonBody DocumentImportDtos.TaskRetryRequest request
|
||||||
|
) {
|
||||||
|
KnowledgeShareAuthContext context = knowledgeShareService.assertUrlShareAccess(
|
||||||
|
shareKey,
|
||||||
|
request == null ? null : request.getKnowledgeId(),
|
||||||
|
KnowledgeShareActionScope.CONTENT_CREATE.name()
|
||||||
|
);
|
||||||
|
BigInteger knowledgeId = resolveKnowledgeId(context, request == null ? null : request.getKnowledgeId());
|
||||||
|
request.setKnowledgeId(knowledgeId);
|
||||||
|
audit(context, "重试分享文档解析", "KNOWLEDGE_SHARE_URL_WRITE", true, auditDetail("knowledgeId", knowledgeId));
|
||||||
|
return documentService.retryParseTask(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/document/import/task/retryIndex")
|
||||||
|
public Result<DocumentImportDtos.TaskStartIndexResponse> retryIndexTask(
|
||||||
|
@RequestParam String shareKey,
|
||||||
|
@JsonBody DocumentImportDtos.TaskRetryRequest request
|
||||||
|
) {
|
||||||
|
KnowledgeShareAuthContext context = knowledgeShareService.assertUrlShareAccess(
|
||||||
|
shareKey,
|
||||||
|
request == null ? null : request.getKnowledgeId(),
|
||||||
|
KnowledgeShareActionScope.CONTENT_CREATE.name()
|
||||||
|
);
|
||||||
|
BigInteger knowledgeId = resolveKnowledgeId(context, request == null ? null : request.getKnowledgeId());
|
||||||
|
request.setKnowledgeId(knowledgeId);
|
||||||
|
audit(context, "重试分享文档向量化", "KNOWLEDGE_SHARE_URL_WRITE", true, auditDetail("knowledgeId", knowledgeId));
|
||||||
|
return documentService.retryIndexTask(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chunk 分页。
|
||||||
|
*/
|
||||||
|
@GetMapping("/documentChunk/page")
|
||||||
|
public Result<Page<DocumentChunk>> documentChunkPage(
|
||||||
|
@RequestParam String shareKey,
|
||||||
|
@RequestParam BigInteger documentId,
|
||||||
|
@RequestParam(defaultValue = "1") long pageNumber,
|
||||||
|
@RequestParam(defaultValue = "10") long pageSize
|
||||||
|
) {
|
||||||
|
KnowledgeShareAuthContext context = knowledgeShareService.assertUrlShareAccess(
|
||||||
|
shareKey,
|
||||||
|
null,
|
||||||
|
KnowledgeShareActionScope.VIEW.name()
|
||||||
|
);
|
||||||
|
Document document = documentService.getById(documentId);
|
||||||
|
if (document == null || document.getCollectionId() == null
|
||||||
|
|| document.getCollectionId().compareTo(context.getKnowledge().getId()) != 0) {
|
||||||
|
throw new BusinessException("文档不存在");
|
||||||
|
}
|
||||||
|
QueryWrapper wrapper = QueryWrapper.create()
|
||||||
|
.eq(DocumentChunk::getDocumentId, documentId)
|
||||||
|
.orderBy("sorting asc");
|
||||||
|
return Result.ok(documentChunkService.page(new Page<>(pageNumber, pageSize), wrapper));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新 Chunk。
|
||||||
|
*/
|
||||||
|
@PostMapping("/documentChunk/update")
|
||||||
|
public Result<?> updateDocumentChunk(
|
||||||
|
@RequestParam String shareKey,
|
||||||
|
@JsonBody DocumentChunk documentChunk
|
||||||
|
) {
|
||||||
|
KnowledgeShareAuthContext context = knowledgeShareService.assertUrlShareAccess(
|
||||||
|
shareKey,
|
||||||
|
null,
|
||||||
|
KnowledgeShareActionScope.CONTENT_UPDATE.name()
|
||||||
|
);
|
||||||
|
DocumentChunk current = documentChunkService.getById(documentChunk.getId());
|
||||||
|
if (current == null || current.getDocumentCollectionId() == null
|
||||||
|
|| current.getDocumentCollectionId().compareTo(context.getKnowledge().getId()) != 0) {
|
||||||
|
throw new BusinessException("记录不存在");
|
||||||
|
}
|
||||||
|
boolean success = documentChunkService.updateById(documentChunk);
|
||||||
|
if (success) {
|
||||||
|
DocumentStore documentStore = context.getKnowledge().toDocumentStore();
|
||||||
|
if (documentStore == null) {
|
||||||
|
return Result.fail(2, "知识库没有配置向量库");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Model model = modelService.getModelInstance(context.getKnowledge().getVectorEmbedModelId());
|
||||||
|
if (model == null) {
|
||||||
|
return Result.fail(3, "知识库没有配置向量模型");
|
||||||
|
}
|
||||||
|
EmbeddingModel embeddingModel = model.toEmbeddingModel();
|
||||||
|
documentStore.setEmbeddingModel(embeddingModel);
|
||||||
|
StoreOptions options = StoreOptions.ofCollectionName(context.getKnowledge().getVectorStoreCollection());
|
||||||
|
com.easyagents.core.document.Document doc = com.easyagents.core.document.Document.of(documentChunk.getContent());
|
||||||
|
doc.setId(documentChunk.getId());
|
||||||
|
StoreResult result = documentStore.update(doc, options);
|
||||||
|
audit(context, "更新分享文档 Chunk", "KNOWLEDGE_SHARE_URL_WRITE", true,
|
||||||
|
auditDetail("knowledgeId", context.getKnowledge().getId(), "chunkId", documentChunk.getId()));
|
||||||
|
return Result.ok(result);
|
||||||
|
} finally {
|
||||||
|
DocumentStoreLifecycleSupport.closeQuietly(documentStore);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Result.ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除 Chunk。
|
||||||
|
*/
|
||||||
|
@PostMapping("/documentChunk/removeChunk")
|
||||||
|
public Result<?> removeDocumentChunk(
|
||||||
|
@RequestParam String shareKey,
|
||||||
|
@JsonBody("id") BigInteger chunkId
|
||||||
|
) {
|
||||||
|
KnowledgeShareAuthContext context = knowledgeShareService.assertUrlShareAccess(
|
||||||
|
shareKey,
|
||||||
|
null,
|
||||||
|
KnowledgeShareActionScope.CONTENT_DELETE.name()
|
||||||
|
);
|
||||||
|
DocumentChunk current = documentChunkService.getById(chunkId);
|
||||||
|
if (current == null || current.getDocumentCollectionId() == null
|
||||||
|
|| current.getDocumentCollectionId().compareTo(context.getKnowledge().getId()) != 0) {
|
||||||
|
return Result.fail(1, "记录不存在");
|
||||||
|
}
|
||||||
|
DocumentStore documentStore = context.getKnowledge().toDocumentStore();
|
||||||
|
if (documentStore == null) {
|
||||||
|
return Result.fail(2, "知识库没有配置向量库");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Model model = modelService.getModelInstance(context.getKnowledge().getVectorEmbedModelId());
|
||||||
|
if (model == null) {
|
||||||
|
return Result.fail(3, "知识库没有配置向量模型");
|
||||||
|
}
|
||||||
|
documentStore.setEmbeddingModel(model.toEmbeddingModel());
|
||||||
|
StoreOptions options = StoreOptions.ofCollectionName(context.getKnowledge().getVectorStoreCollection());
|
||||||
|
documentStore.delete(Collections.singletonList(chunkId), options);
|
||||||
|
documentChunkService.removeById(chunkId);
|
||||||
|
audit(context, "删除分享文档 Chunk", "KNOWLEDGE_SHARE_URL_WRITE", true,
|
||||||
|
auditDetail("knowledgeId", context.getKnowledge().getId(), "chunkId", chunkId));
|
||||||
|
return Result.ok(true);
|
||||||
|
} finally {
|
||||||
|
DocumentStoreLifecycleSupport.closeQuietly(documentStore);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FAQ 分类列表。
|
||||||
|
*/
|
||||||
|
@GetMapping("/faqCategory/list")
|
||||||
|
public Result<List<FaqCategory>> faqCategoryList(
|
||||||
|
@RequestParam String shareKey,
|
||||||
|
@RequestParam(required = false) Boolean asTree
|
||||||
|
) {
|
||||||
|
KnowledgeShareAuthContext context = knowledgeShareService.assertUrlShareAccess(
|
||||||
|
shareKey,
|
||||||
|
null,
|
||||||
|
KnowledgeShareActionScope.VIEW.name()
|
||||||
|
);
|
||||||
|
return Result.ok(faqCategoryService.listByCollection(context.getKnowledge().getId(), asTree));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存 FAQ 分类。
|
||||||
|
*/
|
||||||
|
@PostMapping("/faqCategory/save")
|
||||||
|
public Result<?> saveFaqCategory(
|
||||||
|
@RequestParam String shareKey,
|
||||||
|
@JsonBody FaqCategory entity
|
||||||
|
) {
|
||||||
|
KnowledgeShareAuthContext context = knowledgeShareService.assertUrlShareAccess(
|
||||||
|
shareKey,
|
||||||
|
entity.getCollectionId(),
|
||||||
|
KnowledgeShareActionScope.CONTENT_CREATE.name()
|
||||||
|
);
|
||||||
|
audit(context, "新增 FAQ 分类", "KNOWLEDGE_SHARE_URL_WRITE", true, auditDetail("knowledgeId", entity.getCollectionId()));
|
||||||
|
return Result.ok(faqCategoryService.saveCategory(entity));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新 FAQ 分类。
|
||||||
|
*/
|
||||||
|
@PostMapping("/faqCategory/update")
|
||||||
|
public Result<?> updateFaqCategory(
|
||||||
|
@RequestParam String shareKey,
|
||||||
|
@JsonBody FaqCategory entity
|
||||||
|
) {
|
||||||
|
KnowledgeShareAuthContext context = knowledgeShareService.assertUrlShareAccess(
|
||||||
|
shareKey,
|
||||||
|
null,
|
||||||
|
KnowledgeShareActionScope.CONTENT_UPDATE.name()
|
||||||
|
);
|
||||||
|
audit(context, "更新 FAQ 分类", "KNOWLEDGE_SHARE_URL_WRITE", true,
|
||||||
|
auditDetail("knowledgeId", context.getKnowledge().getId(), "categoryId", entity.getId()));
|
||||||
|
return Result.ok(faqCategoryService.updateCategory(entity));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除 FAQ 分类。
|
||||||
|
*/
|
||||||
|
@PostMapping("/faqCategory/remove")
|
||||||
|
public Result<?> removeFaqCategory(
|
||||||
|
@RequestParam String shareKey,
|
||||||
|
@JsonBody("id") BigInteger id
|
||||||
|
) {
|
||||||
|
KnowledgeShareAuthContext context = knowledgeShareService.assertUrlShareAccess(
|
||||||
|
shareKey,
|
||||||
|
null,
|
||||||
|
KnowledgeShareActionScope.CONTENT_DELETE.name()
|
||||||
|
);
|
||||||
|
audit(context, "删除 FAQ 分类", "KNOWLEDGE_SHARE_URL_WRITE", true,
|
||||||
|
auditDetail("knowledgeId", context.getKnowledge().getId(), "categoryId", id));
|
||||||
|
return Result.ok(faqCategoryService.removeCategory(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FAQ 分页。
|
||||||
|
*/
|
||||||
|
@GetMapping("/faqItem/page")
|
||||||
|
public Result<Page<FaqItem>> faqPage(
|
||||||
|
@RequestParam String shareKey,
|
||||||
|
HttpServletRequest request,
|
||||||
|
@RequestParam(defaultValue = "1") long pageNumber,
|
||||||
|
@RequestParam(defaultValue = "10") long pageSize
|
||||||
|
) {
|
||||||
|
KnowledgeShareAuthContext context = knowledgeShareService.assertUrlShareAccess(
|
||||||
|
shareKey,
|
||||||
|
null,
|
||||||
|
KnowledgeShareActionScope.VIEW.name()
|
||||||
|
);
|
||||||
|
faqCategoryService.ensureDefaultCategory(context.getKnowledge().getId());
|
||||||
|
QueryWrapper queryWrapper = QueryWrapper.create()
|
||||||
|
.eq(FaqItem::getCollectionId, context.getKnowledge().getId());
|
||||||
|
String question = request.getParameter("question");
|
||||||
|
if (StringUtils.hasText(question)) {
|
||||||
|
queryWrapper.like(FaqItem::getQuestion, question.trim());
|
||||||
|
}
|
||||||
|
String categoryId = request.getParameter("categoryId");
|
||||||
|
if (StringUtils.hasText(categoryId)) {
|
||||||
|
List<BigInteger> descendantIds =
|
||||||
|
faqCategoryService.findDescendantIds(context.getKnowledge().getId(), new BigInteger(categoryId));
|
||||||
|
if (descendantIds.isEmpty()) {
|
||||||
|
queryWrapper.eq(FaqItem::getId, BigInteger.ZERO);
|
||||||
|
} else {
|
||||||
|
queryWrapper.in(FaqItem::getCategoryId, descendantIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
queryWrapper.orderBy("order_no asc");
|
||||||
|
Page<FaqItem> page = faqItemService.page(new Page<>(pageNumber, pageSize), queryWrapper);
|
||||||
|
Map<BigInteger, String> pathMap = faqCategoryService.buildPathMap(context.getKnowledge().getId());
|
||||||
|
if (page.getRecords() != null) {
|
||||||
|
for (FaqItem record : page.getRecords()) {
|
||||||
|
record.setCategoryPath(pathMap.get(record.getCategoryId()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Result.ok(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FAQ 详情。
|
||||||
|
*/
|
||||||
|
@GetMapping("/faqItem/detail")
|
||||||
|
public Result<FaqItem> faqDetail(
|
||||||
|
@RequestParam String shareKey,
|
||||||
|
@RequestParam String id
|
||||||
|
) {
|
||||||
|
KnowledgeShareAuthContext context = knowledgeShareService.assertUrlShareAccess(
|
||||||
|
shareKey,
|
||||||
|
null,
|
||||||
|
KnowledgeShareActionScope.VIEW.name()
|
||||||
|
);
|
||||||
|
FaqItem item = faqItemService.getById(id);
|
||||||
|
if (item == null || item.getCollectionId() == null
|
||||||
|
|| item.getCollectionId().compareTo(context.getKnowledge().getId()) != 0) {
|
||||||
|
throw new BusinessException("FAQ不存在");
|
||||||
|
}
|
||||||
|
return Result.ok(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存 FAQ。
|
||||||
|
*/
|
||||||
|
@PostMapping("/faqItem/save")
|
||||||
|
public Result<?> saveFaq(
|
||||||
|
@RequestParam String shareKey,
|
||||||
|
@JsonBody FaqItem entity
|
||||||
|
) {
|
||||||
|
KnowledgeShareAuthContext context = knowledgeShareService.assertUrlShareAccess(
|
||||||
|
shareKey,
|
||||||
|
entity.getCollectionId(),
|
||||||
|
KnowledgeShareActionScope.CONTENT_CREATE.name()
|
||||||
|
);
|
||||||
|
audit(context, "新增 FAQ", "KNOWLEDGE_SHARE_URL_WRITE", true, auditDetail("knowledgeId", entity.getCollectionId()));
|
||||||
|
return Result.ok(faqItemService.saveFaqItem(entity));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新 FAQ。
|
||||||
|
*/
|
||||||
|
@PostMapping("/faqItem/update")
|
||||||
|
public Result<?> updateFaq(
|
||||||
|
@RequestParam String shareKey,
|
||||||
|
@JsonBody FaqItem entity
|
||||||
|
) {
|
||||||
|
KnowledgeShareAuthContext context = knowledgeShareService.assertUrlShareAccess(
|
||||||
|
shareKey,
|
||||||
|
null,
|
||||||
|
KnowledgeShareActionScope.CONTENT_UPDATE.name()
|
||||||
|
);
|
||||||
|
audit(context, "更新 FAQ", "KNOWLEDGE_SHARE_URL_WRITE", true,
|
||||||
|
auditDetail("knowledgeId", context.getKnowledge().getId(), "faqId", entity.getId()));
|
||||||
|
return Result.ok(faqItemService.updateFaqItem(entity));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除 FAQ。
|
||||||
|
*/
|
||||||
|
@PostMapping("/faqItem/remove")
|
||||||
|
public Result<?> removeFaq(
|
||||||
|
@RequestParam String shareKey,
|
||||||
|
@JsonBody("id") BigInteger id
|
||||||
|
) {
|
||||||
|
KnowledgeShareAuthContext context = knowledgeShareService.assertUrlShareAccess(
|
||||||
|
shareKey,
|
||||||
|
null,
|
||||||
|
KnowledgeShareActionScope.CONTENT_DELETE.name()
|
||||||
|
);
|
||||||
|
audit(context, "删除 FAQ", "KNOWLEDGE_SHARE_URL_WRITE", true,
|
||||||
|
auditDetail("knowledgeId", context.getKnowledge().getId(), "faqId", id));
|
||||||
|
return Result.ok(faqItemService.removeFaqItem(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传 FAQ 图片。
|
||||||
|
*/
|
||||||
|
@PostMapping(value = "/faqItem/uploadImage", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||||
|
public Result<UploadResVo> uploadFaqImage(
|
||||||
|
@RequestParam String shareKey,
|
||||||
|
MultipartFile file,
|
||||||
|
BigInteger collectionId
|
||||||
|
) {
|
||||||
|
KnowledgeShareAuthContext context = knowledgeShareService.assertUrlShareAccess(
|
||||||
|
shareKey,
|
||||||
|
collectionId,
|
||||||
|
KnowledgeShareActionScope.CONTENT_UPDATE.name()
|
||||||
|
);
|
||||||
|
if (file == null || file.isEmpty()) {
|
||||||
|
throw new BusinessException("图片不能为空");
|
||||||
|
}
|
||||||
|
if (file.getSize() > MAX_IMAGE_SIZE_BYTES) {
|
||||||
|
throw new BusinessException("图片大小不能超过5MB");
|
||||||
|
}
|
||||||
|
if (!isAllowedImageType(file)) {
|
||||||
|
throw new BusinessException("仅支持 JPG/PNG/WEBP/GIF 图片");
|
||||||
|
}
|
||||||
|
String path = fileStorageService.save(file, "faq/" + collectionId);
|
||||||
|
UploadResVo result = new UploadResVo();
|
||||||
|
result.setPath(path);
|
||||||
|
audit(context, "上传 FAQ 图片", "KNOWLEDGE_SHARE_URL_WRITE", true, auditDetail("knowledgeId", collectionId));
|
||||||
|
return Result.ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导入 FAQ Excel。
|
||||||
|
*/
|
||||||
|
@PostMapping(value = "/faqItem/importExcel", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||||
|
public Result<FaqImportResultVo> importFaqExcel(
|
||||||
|
@RequestParam String shareKey,
|
||||||
|
MultipartFile file,
|
||||||
|
BigInteger collectionId
|
||||||
|
) {
|
||||||
|
KnowledgeShareAuthContext context = knowledgeShareService.assertUrlShareAccess(
|
||||||
|
shareKey,
|
||||||
|
collectionId,
|
||||||
|
KnowledgeShareActionScope.IMPORT_EXPORT.name()
|
||||||
|
);
|
||||||
|
audit(context, "导入 FAQ Excel", "KNOWLEDGE_SHARE_URL_WRITE", true, auditDetail("knowledgeId", collectionId));
|
||||||
|
return Result.ok(faqItemService.importFromExcel(collectionId, file));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载 FAQ 导入模板。
|
||||||
|
*/
|
||||||
|
@GetMapping("/faqItem/downloadImportTemplate")
|
||||||
|
public void downloadFaqImportTemplate(
|
||||||
|
@RequestParam String shareKey,
|
||||||
|
@RequestParam BigInteger collectionId,
|
||||||
|
HttpServletResponse response
|
||||||
|
) throws Exception {
|
||||||
|
KnowledgeShareAuthContext context = knowledgeShareService.assertUrlShareAccess(
|
||||||
|
shareKey,
|
||||||
|
collectionId,
|
||||||
|
KnowledgeShareActionScope.IMPORT_EXPORT.name()
|
||||||
|
);
|
||||||
|
response.setContentType("application/octet-stream");
|
||||||
|
response.setHeader(
|
||||||
|
"Content-disposition",
|
||||||
|
"attachment;filename*=utf-8''" + URLEncoder.encode("faq_import_template.xlsx", StandardCharsets.UTF_8)
|
||||||
|
);
|
||||||
|
faqItemService.writeImportTemplate(response.getOutputStream());
|
||||||
|
response.flushBuffer();
|
||||||
|
audit(context, "下载 FAQ 导入模板", "KNOWLEDGE_SHARE_URL_ACCESS", false, auditDetail("knowledgeId", collectionId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出 FAQ Excel。
|
||||||
|
*/
|
||||||
|
@GetMapping("/faqItem/exportExcel")
|
||||||
|
public void exportFaqExcel(
|
||||||
|
@RequestParam String shareKey,
|
||||||
|
@RequestParam BigInteger collectionId,
|
||||||
|
HttpServletResponse response
|
||||||
|
) throws Exception {
|
||||||
|
KnowledgeShareAuthContext context = knowledgeShareService.assertUrlShareAccess(
|
||||||
|
shareKey,
|
||||||
|
collectionId,
|
||||||
|
KnowledgeShareActionScope.IMPORT_EXPORT.name()
|
||||||
|
);
|
||||||
|
String fileName = "faq_export_" + new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()) + ".xlsx";
|
||||||
|
response.setContentType("application/octet-stream");
|
||||||
|
response.setHeader(
|
||||||
|
"Content-disposition",
|
||||||
|
"attachment;filename*=utf-8''" + URLEncoder.encode(fileName, StandardCharsets.UTF_8)
|
||||||
|
);
|
||||||
|
faqItemService.exportToExcel(collectionId, response.getOutputStream());
|
||||||
|
response.flushBuffer();
|
||||||
|
audit(context, "导出 FAQ Excel", "KNOWLEDGE_SHARE_URL_ACCESS", false, auditDetail("knowledgeId", collectionId));
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isAllowedImageType(MultipartFile file) {
|
||||||
|
String contentType = file.getContentType();
|
||||||
|
return contentType != null && ALLOWED_IMAGE_TYPES.contains(contentType.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验分享请求中的知识库标识。
|
||||||
|
*
|
||||||
|
* @param knowledgeId 知识库 ID
|
||||||
|
* @return 已校验的知识库 ID
|
||||||
|
*/
|
||||||
|
private BigInteger resolveKnowledgeId(KnowledgeShareAuthContext context, BigInteger requestedKnowledgeId) {
|
||||||
|
if (requestedKnowledgeId != null) {
|
||||||
|
return requestedKnowledgeId;
|
||||||
|
}
|
||||||
|
if (context != null && context.getKnowledge() != null && context.getKnowledge().getId() != null) {
|
||||||
|
return context.getKnowledge().getId();
|
||||||
|
}
|
||||||
|
if (context == null || context.getKnowledge() == null) {
|
||||||
|
throw new BusinessException("知识库不能为空");
|
||||||
|
}
|
||||||
|
throw new BusinessException("知识库不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造允许空值的审计详情。
|
||||||
|
*
|
||||||
|
* @param keyValues 键值对
|
||||||
|
* @return 审计详情
|
||||||
|
*/
|
||||||
|
private Map<String, Object> auditDetail(Object... keyValues) {
|
||||||
|
if (keyValues == null || keyValues.length == 0) {
|
||||||
|
return new HashMap<>();
|
||||||
|
}
|
||||||
|
if ((keyValues.length & 1) != 0) {
|
||||||
|
throw new IllegalArgumentException("审计详情参数必须成对出现");
|
||||||
|
}
|
||||||
|
Map<String, Object> detail = new HashMap<>(keyValues.length / 2);
|
||||||
|
for (int index = 0; index < keyValues.length; index += 2) {
|
||||||
|
Object key = keyValues[index];
|
||||||
|
if (!(key instanceof String detailKey) || !StringUtils.hasText(detailKey)) {
|
||||||
|
throw new IllegalArgumentException("审计详情 key 必须为非空字符串");
|
||||||
|
}
|
||||||
|
detail.put(detailKey, keyValues[index + 1]);
|
||||||
|
}
|
||||||
|
return detail;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录分享访问审计。
|
||||||
|
*
|
||||||
|
* @param context 分享鉴权上下文
|
||||||
|
* @param actionName 动作名称
|
||||||
|
* @param actionType 动作类型
|
||||||
|
* @param writeOperation 是否写操作
|
||||||
|
* @param detail 审计详情
|
||||||
|
*/
|
||||||
|
private void audit(KnowledgeShareAuthContext context, String actionName, String actionType, boolean writeOperation, Map<String, Object> detail) {
|
||||||
|
Map<String, Object> payload = new HashMap<>(detail);
|
||||||
|
payload.put("shareId", context.getShare().getId());
|
||||||
|
payload.put("writeOperation", writeOperation);
|
||||||
|
knowledgeShareAuditService.log(null, actionName, actionType, "/api/v1/share/knowledge", payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<KnowledgeSearchResultItem> toKnowledgeSearchResult(List<com.easyagents.core.document.Document> documents) {
|
||||||
|
List<KnowledgeSearchResultItem> result = new java.util.ArrayList<>();
|
||||||
|
if (documents == null) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
for (int index = 0; index < documents.size(); index++) {
|
||||||
|
com.easyagents.core.document.Document document = documents.get(index);
|
||||||
|
if (document == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
KnowledgeSearchResultItem item = new KnowledgeSearchResultItem();
|
||||||
|
item.setSorting(index + 1);
|
||||||
|
item.setContent(document.getContent());
|
||||||
|
Object renderMarkdown = document.getMetadata("renderMarkdown");
|
||||||
|
item.setRenderMarkdown(renderMarkdown == null ? null : String.valueOf(renderMarkdown));
|
||||||
|
Object sourceFileName = document.getMetadata("sourceFileName");
|
||||||
|
item.setSourceFileName(sourceFileName == null ? null : String.valueOf(sourceFileName));
|
||||||
|
item.setScore(document.getScore() == null ? null : document.getScore().doubleValue());
|
||||||
|
Object hitSource = document.getMetadata("hitSource");
|
||||||
|
item.setHitSource(hitSource == null ? null : String.valueOf(hitSource));
|
||||||
|
item.setVectorScore(asDouble(document.getMetadata("vectorScore")));
|
||||||
|
item.setKeywordScore(asDouble(document.getMetadata("keywordScore")));
|
||||||
|
result.add(item);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Double asDouble(Object value) {
|
||||||
|
if (value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (value instanceof Number number) {
|
||||||
|
return number.doubleValue();
|
||||||
|
}
|
||||||
|
if (value instanceof String text && StringUtils.hasText(text)) {
|
||||||
|
try {
|
||||||
|
return Double.parseDouble(text);
|
||||||
|
} catch (NumberFormatException ignore) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ import com.easyagents.flow.core.parser.ChainParser;
|
|||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import tech.easyflow.ai.easyagentsflow.service.WorkflowDatacenterContentService;
|
||||||
import tech.easyflow.ai.entity.Workflow;
|
import tech.easyflow.ai.entity.Workflow;
|
||||||
import tech.easyflow.ai.service.WorkflowService;
|
import tech.easyflow.ai.service.WorkflowService;
|
||||||
import tech.easyflow.common.domain.Result;
|
import tech.easyflow.common.domain.Result;
|
||||||
@@ -29,6 +30,8 @@ public class WorkFlowNodeController {
|
|||||||
private WorkflowService workflowService;
|
private WorkflowService workflowService;
|
||||||
@Resource
|
@Resource
|
||||||
private ChainParser chainParser;
|
private ChainParser chainParser;
|
||||||
|
@Resource
|
||||||
|
private WorkflowDatacenterContentService workflowDatacenterContentService;
|
||||||
|
|
||||||
@GetMapping("/getChainParams")
|
@GetMapping("/getChainParams")
|
||||||
public Result<?> getChainParams(String currentId, String workflowId) {
|
public Result<?> getChainParams(String currentId, String workflowId) {
|
||||||
@@ -43,7 +46,7 @@ public class WorkFlowNodeController {
|
|||||||
nodeData.put("workflowId", workflow.getId());
|
nodeData.put("workflowId", workflow.getId());
|
||||||
nodeData.put("workflowName", workflow.getTitle());
|
nodeData.put("workflowName", workflow.getTitle());
|
||||||
|
|
||||||
ChainDefinition definition = chainParser.parse(workflow.getContent());
|
ChainDefinition definition = chainParser.parse(workflowDatacenterContentService.prepareContent(workflow.getContent()));
|
||||||
List<Node> nodes = definition.getNodes();
|
List<Node> nodes = definition.getNodes();
|
||||||
JSONArray inputs = new JSONArray();
|
JSONArray inputs = new JSONArray();
|
||||||
JSONArray outputs = new JSONArray();
|
JSONArray outputs = new JSONArray();
|
||||||
|
|||||||
@@ -1,12 +1,21 @@
|
|||||||
package tech.easyflow.admin.controller.ai;
|
package tech.easyflow.admin.controller.ai;
|
||||||
|
|
||||||
|
import com.mybatisflex.core.query.QueryWrapper;
|
||||||
import tech.easyflow.ai.entity.WorkflowCategory;
|
import tech.easyflow.ai.entity.WorkflowCategory;
|
||||||
import tech.easyflow.ai.service.WorkflowCategoryService;
|
import tech.easyflow.ai.service.WorkflowCategoryService;
|
||||||
import tech.easyflow.common.annotation.UsePermission;
|
import tech.easyflow.common.annotation.UsePermission;
|
||||||
|
import tech.easyflow.common.domain.Result;
|
||||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||||
|
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
|
||||||
|
import tech.easyflow.system.service.CategoryPermissionService;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 控制层。
|
* 控制层。
|
||||||
*
|
*
|
||||||
@@ -17,9 +26,24 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
@RequestMapping("/api/v1/workflowCategory")
|
@RequestMapping("/api/v1/workflowCategory")
|
||||||
@UsePermission(moduleName = "/api/v1/workflow")
|
@UsePermission(moduleName = "/api/v1/workflow")
|
||||||
public class WorkflowCategoryController extends BaseCurdController<WorkflowCategoryService, WorkflowCategory> {
|
public class WorkflowCategoryController extends BaseCurdController<WorkflowCategoryService, WorkflowCategory> {
|
||||||
|
@Resource
|
||||||
|
private CategoryPermissionService categoryPermissionService;
|
||||||
|
|
||||||
public WorkflowCategoryController(WorkflowCategoryService service) {
|
public WorkflowCategoryController(WorkflowCategoryService service) {
|
||||||
super(service);
|
super(service);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
@GetMapping("visibleList")
|
||||||
|
public Result<List<WorkflowCategory>> visibleList(WorkflowCategory entity, Boolean asTree, String sortKey, String sortType) {
|
||||||
|
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
|
||||||
|
RoleCategoryAccessSnapshot access = categoryPermissionService.getCurrentAccess("WORKFLOW");
|
||||||
|
if (access.isRestricted()) {
|
||||||
|
if (access.getCategoryIds().isEmpty()) {
|
||||||
|
return Result.ok(Collections.emptyList());
|
||||||
|
}
|
||||||
|
queryWrapper.in(WorkflowCategory::getId, access.getCategoryIds());
|
||||||
|
}
|
||||||
|
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||||
|
return Result.ok(service.list(queryWrapper));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,14 +4,16 @@ import cn.dev33.satoken.annotation.SaCheckPermission;
|
|||||||
import cn.dev33.satoken.stp.StpUtil;
|
import cn.dev33.satoken.stp.StpUtil;
|
||||||
import cn.hutool.core.io.IoUtil;
|
import cn.hutool.core.io.IoUtil;
|
||||||
import cn.hutool.core.util.IdUtil;
|
import cn.hutool.core.util.IdUtil;
|
||||||
import com.easyagents.flow.core.chain.ChainDefinition;
|
import com.mybatisflex.core.paginate.Page;
|
||||||
import com.easyagents.flow.core.chain.Parameter;
|
|
||||||
import com.easyagents.flow.core.chain.runtime.ChainExecutor;
|
import com.easyagents.flow.core.chain.runtime.ChainExecutor;
|
||||||
import com.easyagents.flow.core.parser.ChainParser;
|
|
||||||
import com.mybatisflex.core.query.QueryWrapper;
|
import com.mybatisflex.core.query.QueryWrapper;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
import org.springframework.web.context.request.RequestContextHolder;
|
||||||
|
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
import tech.easyflow.admin.controller.ai.support.AiResourceCreatorNameSupport;
|
||||||
|
import tech.easyflow.ai.permission.WorkflowVisibilityQueryHelper;
|
||||||
import tech.easyflow.ai.easyagentsflow.entity.ChainInfo;
|
import tech.easyflow.ai.easyagentsflow.entity.ChainInfo;
|
||||||
import tech.easyflow.ai.easyagentsflow.entity.NodeInfo;
|
import tech.easyflow.ai.easyagentsflow.entity.NodeInfo;
|
||||||
import tech.easyflow.ai.easyagentsflow.entity.WorkflowCheckResult;
|
import tech.easyflow.ai.easyagentsflow.entity.WorkflowCheckResult;
|
||||||
@@ -19,7 +21,14 @@ import tech.easyflow.ai.easyagentsflow.entity.WorkflowCheckStage;
|
|||||||
import tech.easyflow.ai.easyagentsflow.service.CodeEngineCapabilityService;
|
import tech.easyflow.ai.easyagentsflow.service.CodeEngineCapabilityService;
|
||||||
import tech.easyflow.ai.easyagentsflow.service.TinyFlowService;
|
import tech.easyflow.ai.easyagentsflow.service.TinyFlowService;
|
||||||
import tech.easyflow.ai.easyagentsflow.service.WorkflowCheckService;
|
import tech.easyflow.ai.easyagentsflow.service.WorkflowCheckService;
|
||||||
|
import tech.easyflow.ai.easyagentsflow.service.WorkflowDatacenterContentService;
|
||||||
|
import tech.easyflow.ai.easyagentsflow.service.WorkflowRunningParameterResolver;
|
||||||
import tech.easyflow.ai.entity.Workflow;
|
import tech.easyflow.ai.entity.Workflow;
|
||||||
|
import tech.easyflow.ai.enums.PublishStatus;
|
||||||
|
import tech.easyflow.ai.publish.WorkflowPublishAppService;
|
||||||
|
import tech.easyflow.ai.service.AiResourceApprovalStateService;
|
||||||
|
import tech.easyflow.ai.vo.OfflineImpactCheckVo;
|
||||||
|
import tech.easyflow.approval.entity.vo.ApprovalActionResult;
|
||||||
import tech.easyflow.ai.service.BotWorkflowService;
|
import tech.easyflow.ai.service.BotWorkflowService;
|
||||||
import tech.easyflow.ai.service.ModelService;
|
import tech.easyflow.ai.service.ModelService;
|
||||||
import tech.easyflow.ai.service.WorkflowService;
|
import tech.easyflow.ai.service.WorkflowService;
|
||||||
@@ -30,6 +39,12 @@ import tech.easyflow.common.satoken.util.SaTokenUtil;
|
|||||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||||
|
import tech.easyflow.system.enums.CategoryResourceType;
|
||||||
|
import tech.easyflow.system.enums.ResourceAction;
|
||||||
|
import tech.easyflow.system.enums.ResourceLookup;
|
||||||
|
import tech.easyflow.system.enums.VisibilityScope;
|
||||||
|
import tech.easyflow.system.permission.resource.RequireResourceAccess;
|
||||||
|
import tech.easyflow.system.service.ResourceAccessService;
|
||||||
import tech.easyflow.system.service.SysApiKeyService;
|
import tech.easyflow.system.service.SysApiKeyService;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
@@ -60,13 +75,25 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
|||||||
@Resource
|
@Resource
|
||||||
private ChainExecutor chainExecutor;
|
private ChainExecutor chainExecutor;
|
||||||
@Resource
|
@Resource
|
||||||
private ChainParser chainParser;
|
|
||||||
@Resource
|
|
||||||
private TinyFlowService tinyFlowService;
|
private TinyFlowService tinyFlowService;
|
||||||
@Resource
|
@Resource
|
||||||
private CodeEngineCapabilityService codeEngineCapabilityService;
|
private CodeEngineCapabilityService codeEngineCapabilityService;
|
||||||
@Resource
|
@Resource
|
||||||
private WorkflowCheckService workflowCheckService;
|
private WorkflowCheckService workflowCheckService;
|
||||||
|
@Resource
|
||||||
|
private WorkflowDatacenterContentService workflowDatacenterContentService;
|
||||||
|
@Resource
|
||||||
|
private WorkflowRunningParameterResolver workflowRunningParameterResolver;
|
||||||
|
@Resource
|
||||||
|
private ResourceAccessService resourceAccessService;
|
||||||
|
@Resource
|
||||||
|
private WorkflowVisibilityQueryHelper workflowVisibilityQueryHelper;
|
||||||
|
@Resource
|
||||||
|
private WorkflowPublishAppService workflowPublishAppService;
|
||||||
|
@Resource
|
||||||
|
private AiResourceApprovalStateService aiResourceApprovalStateService;
|
||||||
|
@Resource
|
||||||
|
private AiResourceCreatorNameSupport aiResourceCreatorNameSupport;
|
||||||
|
|
||||||
public WorkflowController(WorkflowService service, ModelService modelService) {
|
public WorkflowController(WorkflowService service, ModelService modelService) {
|
||||||
super(service);
|
super(service);
|
||||||
@@ -78,6 +105,13 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
|||||||
*/
|
*/
|
||||||
@PostMapping("/singleRun")
|
@PostMapping("/singleRun")
|
||||||
@SaCheckPermission("/api/v1/workflow/save")
|
@SaCheckPermission("/api/v1/workflow/save")
|
||||||
|
@RequireResourceAccess(
|
||||||
|
resource = CategoryResourceType.WORKFLOW,
|
||||||
|
action = ResourceAction.USE,
|
||||||
|
lookup = ResourceLookup.WORKFLOW_ID,
|
||||||
|
idExpr = "#workflowId",
|
||||||
|
denyMessage = "无权限运行工作流"
|
||||||
|
)
|
||||||
public Result<?> singleRun(
|
public Result<?> singleRun(
|
||||||
@JsonBody(value = "workflowId", required = true) BigInteger workflowId,
|
@JsonBody(value = "workflowId", required = true) BigInteger workflowId,
|
||||||
@JsonBody(value = "nodeId", required = true) String nodeId,
|
@JsonBody(value = "nodeId", required = true) String nodeId,
|
||||||
@@ -87,6 +121,13 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
|||||||
if (workflow == null) {
|
if (workflow == null) {
|
||||||
return Result.fail(1, "工作流不存在");
|
return Result.fail(1, "工作流不存在");
|
||||||
}
|
}
|
||||||
|
if (variables == null) {
|
||||||
|
variables = new HashMap<>();
|
||||||
|
}
|
||||||
|
variables = workflowRunningParameterResolver.normalizeRuntimeVariables(workflow.getContent(), variables);
|
||||||
|
if (StpUtil.isLogin()) {
|
||||||
|
variables.put(Constants.LOGIN_USER_KEY, SaTokenUtil.getLoginAccount());
|
||||||
|
}
|
||||||
Map<String, Object> res = chainExecutor.executeNode(workflowId.toString(), nodeId, variables);
|
Map<String, Object> res = chainExecutor.executeNode(workflowId.toString(), nodeId, variables);
|
||||||
return Result.ok(res);
|
return Result.ok(res);
|
||||||
}
|
}
|
||||||
@@ -96,6 +137,13 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
|||||||
*/
|
*/
|
||||||
@PostMapping("/runAsync")
|
@PostMapping("/runAsync")
|
||||||
@SaCheckPermission("/api/v1/workflow/save")
|
@SaCheckPermission("/api/v1/workflow/save")
|
||||||
|
@RequireResourceAccess(
|
||||||
|
resource = CategoryResourceType.WORKFLOW,
|
||||||
|
action = ResourceAction.USE,
|
||||||
|
lookup = ResourceLookup.WORKFLOW_ID,
|
||||||
|
idExpr = "#id",
|
||||||
|
denyMessage = "无权限运行工作流"
|
||||||
|
)
|
||||||
public Result<String> runAsync(@JsonBody(value = "id", required = true) BigInteger id,
|
public Result<String> runAsync(@JsonBody(value = "id", required = true) BigInteger id,
|
||||||
@JsonBody("variables") Map<String, Object> variables) {
|
@JsonBody("variables") Map<String, Object> variables) {
|
||||||
if (variables == null) {
|
if (variables == null) {
|
||||||
@@ -106,6 +154,7 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
|||||||
throw new RuntimeException("工作流不存在");
|
throw new RuntimeException("工作流不存在");
|
||||||
}
|
}
|
||||||
workflowCheckService.checkOrThrow(workflow.getContent(), WorkflowCheckStage.PRE_EXECUTE, workflow.getId());
|
workflowCheckService.checkOrThrow(workflow.getContent(), WorkflowCheckStage.PRE_EXECUTE, workflow.getId());
|
||||||
|
variables = workflowRunningParameterResolver.normalizeRuntimeVariables(workflow.getContent(), variables);
|
||||||
if (StpUtil.isLogin()) {
|
if (StpUtil.isLogin()) {
|
||||||
variables.put(Constants.LOGIN_USER_KEY, SaTokenUtil.getLoginAccount());
|
variables.put(Constants.LOGIN_USER_KEY, SaTokenUtil.getLoginAccount());
|
||||||
}
|
}
|
||||||
@@ -117,6 +166,13 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
|||||||
* 获取工作流运行状态 - v2
|
* 获取工作流运行状态 - v2
|
||||||
*/
|
*/
|
||||||
@PostMapping("/getChainStatus")
|
@PostMapping("/getChainStatus")
|
||||||
|
@RequireResourceAccess(
|
||||||
|
resource = CategoryResourceType.WORKFLOW,
|
||||||
|
action = ResourceAction.USE,
|
||||||
|
lookup = ResourceLookup.EXEC_KEY,
|
||||||
|
idExpr = "#executeId",
|
||||||
|
denyMessage = "无权限访问该执行记录"
|
||||||
|
)
|
||||||
public Result<ChainInfo> getChainStatus(@JsonBody(value = "executeId") String executeId,
|
public Result<ChainInfo> getChainStatus(@JsonBody(value = "executeId") String executeId,
|
||||||
@JsonBody("nodes") List<NodeInfo> nodes) {
|
@JsonBody("nodes") List<NodeInfo> nodes) {
|
||||||
ChainInfo res = tinyFlowService.getChainStatus(executeId, nodes);
|
ChainInfo res = tinyFlowService.getChainStatus(executeId, nodes);
|
||||||
@@ -128,6 +184,13 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
|||||||
*/
|
*/
|
||||||
@PostMapping("/resume")
|
@PostMapping("/resume")
|
||||||
@SaCheckPermission("/api/v1/workflow/save")
|
@SaCheckPermission("/api/v1/workflow/save")
|
||||||
|
@RequireResourceAccess(
|
||||||
|
resource = CategoryResourceType.WORKFLOW,
|
||||||
|
action = ResourceAction.USE,
|
||||||
|
lookup = ResourceLookup.EXEC_KEY,
|
||||||
|
idExpr = "#executeId",
|
||||||
|
denyMessage = "无权限恢复工作流执行"
|
||||||
|
)
|
||||||
public Result<Void> resume(@JsonBody(value = "executeId", required = true) String executeId,
|
public Result<Void> resume(@JsonBody(value = "executeId", required = true) String executeId,
|
||||||
@JsonBody("confirmParams") Map<String, Object> confirmParams) {
|
@JsonBody("confirmParams") Map<String, Object> confirmParams) {
|
||||||
chainExecutor.resumeAsync(executeId, confirmParams);
|
chainExecutor.resumeAsync(executeId, confirmParams);
|
||||||
@@ -137,6 +200,10 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
|||||||
@PostMapping("/importWorkFlow")
|
@PostMapping("/importWorkFlow")
|
||||||
@SaCheckPermission("/api/v1/workflow/save")
|
@SaCheckPermission("/api/v1/workflow/save")
|
||||||
public Result<Void> importWorkFlow(Workflow workflow, MultipartFile jsonFile) throws Exception {
|
public Result<Void> importWorkFlow(Workflow workflow, MultipartFile jsonFile) throws Exception {
|
||||||
|
if (workflow.getId() != null) {
|
||||||
|
Workflow sourceWorkflow = requireWorkflow(String.valueOf(workflow.getId()));
|
||||||
|
resourceAccessService.assertAccess(CategoryResourceType.WORKFLOW, sourceWorkflow, ResourceAction.MANAGE, "无权限管理工作流");
|
||||||
|
}
|
||||||
InputStream is = jsonFile.getInputStream();
|
InputStream is = jsonFile.getInputStream();
|
||||||
String content = IoUtil.read(is, StandardCharsets.UTF_8);
|
String content = IoUtil.read(is, StandardCharsets.UTF_8);
|
||||||
workflow.setContent(content);
|
workflow.setContent(content);
|
||||||
@@ -147,13 +214,30 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
|||||||
|
|
||||||
@GetMapping("/exportWorkFlow")
|
@GetMapping("/exportWorkFlow")
|
||||||
@SaCheckPermission("/api/v1/workflow/save")
|
@SaCheckPermission("/api/v1/workflow/save")
|
||||||
|
@RequireResourceAccess(
|
||||||
|
resource = CategoryResourceType.WORKFLOW,
|
||||||
|
action = ResourceAction.READ,
|
||||||
|
lookup = ResourceLookup.WORKFLOW_ID,
|
||||||
|
idExpr = "#id",
|
||||||
|
denyMessage = "无权限访问工作流"
|
||||||
|
)
|
||||||
public Result<String> exportWorkFlow(BigInteger id) {
|
public Result<String> exportWorkFlow(BigInteger id) {
|
||||||
Workflow workflow = service.getById(id);
|
Workflow workflow = service.getById(id);
|
||||||
|
if (workflow == null) {
|
||||||
|
throw new BusinessException("工作流不存在");
|
||||||
|
}
|
||||||
return Result.ok("", workflow.getContent());
|
return Result.ok("", workflow.getContent());
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("getRunningParameters")
|
@GetMapping("getRunningParameters")
|
||||||
@SaCheckPermission("/api/v1/workflow/query")
|
@SaCheckPermission("/api/v1/workflow/query")
|
||||||
|
@RequireResourceAccess(
|
||||||
|
resource = CategoryResourceType.WORKFLOW,
|
||||||
|
action = ResourceAction.READ,
|
||||||
|
lookup = ResourceLookup.WORKFLOW_ID,
|
||||||
|
idExpr = "#id",
|
||||||
|
denyMessage = "无权限访问工作流"
|
||||||
|
)
|
||||||
public Result<?> getRunningParameters(@RequestParam BigInteger id) {
|
public Result<?> getRunningParameters(@RequestParam BigInteger id) {
|
||||||
Workflow workflow = service.getById(id);
|
Workflow workflow = service.getById(id);
|
||||||
|
|
||||||
@@ -161,20 +245,80 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
|||||||
return Result.fail(1, "can not find the workflow by id: " + id);
|
return Result.fail(1, "can not find the workflow by id: " + id);
|
||||||
}
|
}
|
||||||
workflowCheckService.checkOrThrow(workflow.getContent(), WorkflowCheckStage.PRE_EXECUTE, workflow.getId());
|
workflowCheckService.checkOrThrow(workflow.getContent(), WorkflowCheckStage.PRE_EXECUTE, workflow.getId());
|
||||||
|
Map<String, Object> res = workflowRunningParameterResolver.buildRunningParametersView(workflow);
|
||||||
ChainDefinition definition = chainParser.parse(workflow.getContent());
|
if (res == null) {
|
||||||
if (definition == null) {
|
|
||||||
return Result.fail(2, "节点配置错误,请检查! ");
|
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);
|
return Result.ok(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交发布审批。
|
||||||
|
*
|
||||||
|
* @param id 工作流 ID
|
||||||
|
* @return 审批实例 ID
|
||||||
|
*/
|
||||||
|
@PostMapping("/submitPublishApproval")
|
||||||
|
@SaCheckPermission("/api/v1/workflow/save")
|
||||||
|
public Result<BigInteger> submitPublishApproval(@JsonBody("id") BigInteger id) {
|
||||||
|
return buildApprovalActionResult(
|
||||||
|
workflowPublishAppService.submitPublishApproval(id),
|
||||||
|
"已提交发布审批",
|
||||||
|
"已直接发布"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交下线审批。
|
||||||
|
*
|
||||||
|
* @param id 工作流 ID
|
||||||
|
* @return 审批实例 ID
|
||||||
|
*/
|
||||||
|
@PostMapping("/submitOfflineApproval")
|
||||||
|
@SaCheckPermission("/api/v1/workflow/save")
|
||||||
|
public Result<BigInteger> submitOfflineApproval(@JsonBody("id") BigInteger id) {
|
||||||
|
return buildApprovalActionResult(
|
||||||
|
workflowPublishAppService.submitOfflineApproval(id),
|
||||||
|
"已提交下线审批",
|
||||||
|
"已直接下线"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查工作流下线影响。
|
||||||
|
*
|
||||||
|
* @param id 工作流 ID
|
||||||
|
* @return 下线影响结果
|
||||||
|
*/
|
||||||
|
@GetMapping("/offlineImpactCheck")
|
||||||
|
@SaCheckPermission("/api/v1/workflow/save")
|
||||||
|
@RequireResourceAccess(
|
||||||
|
resource = CategoryResourceType.WORKFLOW,
|
||||||
|
action = ResourceAction.MANAGE,
|
||||||
|
lookup = ResourceLookup.WORKFLOW_ID,
|
||||||
|
idExpr = "#id",
|
||||||
|
denyMessage = "无权限管理工作流"
|
||||||
|
)
|
||||||
|
public Result<OfflineImpactCheckVo> offlineImpactCheck(@RequestParam BigInteger id) {
|
||||||
|
return Result.ok(workflowPublishAppService.checkOfflineImpact(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交删除审批。
|
||||||
|
*
|
||||||
|
* @param id 工作流 ID
|
||||||
|
* @return 审批实例 ID
|
||||||
|
*/
|
||||||
|
@PostMapping("/submitDeleteApproval")
|
||||||
|
@SaCheckPermission("/api/v1/workflow/remove")
|
||||||
|
public Result<BigInteger> submitDeleteApproval(@JsonBody("id") BigInteger id) {
|
||||||
|
return buildApprovalActionResult(
|
||||||
|
workflowPublishAppService.submitDeleteApproval(id),
|
||||||
|
"已提交删除审批",
|
||||||
|
"已直接删除"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/supportedCodeEngines")
|
@GetMapping("/supportedCodeEngines")
|
||||||
@SaCheckPermission("/api/v1/workflow/query")
|
@SaCheckPermission("/api/v1/workflow/query")
|
||||||
public Result<?> supportedCodeEngines() {
|
public Result<?> supportedCodeEngines() {
|
||||||
@@ -186,6 +330,10 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
|||||||
public Result<WorkflowCheckResult> check(@JsonBody("id") BigInteger id,
|
public Result<WorkflowCheckResult> check(@JsonBody("id") BigInteger id,
|
||||||
@JsonBody("content") String content,
|
@JsonBody("content") String content,
|
||||||
@JsonBody(value = "stage", required = true) String stage) {
|
@JsonBody(value = "stage", required = true) String stage) {
|
||||||
|
if (id != null) {
|
||||||
|
Workflow workflow = requireWorkflow(String.valueOf(id));
|
||||||
|
resourceAccessService.assertAccess(CategoryResourceType.WORKFLOW, workflow, ResourceAction.MANAGE, "无权限管理工作流");
|
||||||
|
}
|
||||||
WorkflowCheckStage checkStage = WorkflowCheckStage.from(stage);
|
WorkflowCheckStage checkStage = WorkflowCheckStage.from(stage);
|
||||||
WorkflowCheckResult checkResult;
|
WorkflowCheckResult checkResult;
|
||||||
if (StringUtils.hasLength(content)) {
|
if (StringUtils.hasLength(content)) {
|
||||||
@@ -199,16 +347,35 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@GetMapping("detail")
|
||||||
|
@RequireResourceAccess(
|
||||||
|
resource = CategoryResourceType.WORKFLOW,
|
||||||
|
action = ResourceAction.READ,
|
||||||
|
lookup = ResourceLookup.WORKFLOW_ID,
|
||||||
|
idExpr = "#id",
|
||||||
|
denyMessage = "无权限访问工作流"
|
||||||
|
)
|
||||||
public Result<Workflow> detail(String id) {
|
public Result<Workflow> detail(String id) {
|
||||||
Workflow workflow = service.getDetail(id);
|
Workflow workflow = service.getDetail(id);
|
||||||
|
aiResourceApprovalStateService.fillWorkflowApprovalState(workflow);
|
||||||
return Result.ok(workflow);
|
return Result.ok(workflow);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/copy")
|
@GetMapping("/copy")
|
||||||
@SaCheckPermission("/api/v1/workflow/save")
|
@SaCheckPermission("/api/v1/workflow/save")
|
||||||
|
@RequireResourceAccess(
|
||||||
|
resource = CategoryResourceType.WORKFLOW,
|
||||||
|
action = ResourceAction.READ,
|
||||||
|
lookup = ResourceLookup.WORKFLOW_ID,
|
||||||
|
idExpr = "#id",
|
||||||
|
denyMessage = "无权限访问工作流"
|
||||||
|
)
|
||||||
public Result<Void> copy(BigInteger id) {
|
public Result<Void> copy(BigInteger id) {
|
||||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||||
Workflow workflow = service.getById(id);
|
Workflow workflow = service.getById(id);
|
||||||
|
if (workflow == null) {
|
||||||
|
throw new BusinessException("工作流不存在");
|
||||||
|
}
|
||||||
workflow.setId(null);
|
workflow.setId(null);
|
||||||
workflow.setAlias(IdUtil.fastSimpleUUID());
|
workflow.setAlias(IdUtil.fastSimpleUUID());
|
||||||
commonFiled(workflow, account.getId(), account.getTenantId(), account.getDeptId());
|
commonFiled(workflow, account.getId(), account.getTenantId(), account.getDeptId());
|
||||||
@@ -218,6 +385,11 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Result onSaveOrUpdateBefore(Workflow entity, boolean isSave) {
|
protected Result onSaveOrUpdateBefore(Workflow entity, boolean isSave) {
|
||||||
|
normalizeVisibilityScope(entity, isSave);
|
||||||
|
if (!isSave && entity.getId() != null) {
|
||||||
|
Workflow existed = requireWorkflow(String.valueOf(entity.getId()));
|
||||||
|
resourceAccessService.assertAccess(CategoryResourceType.WORKFLOW, existed, ResourceAction.MANAGE, "无权限管理工作流");
|
||||||
|
}
|
||||||
if (StringUtils.hasLength(entity.getContent())) {
|
if (StringUtils.hasLength(entity.getContent())) {
|
||||||
workflowCheckService.checkOrThrow(entity.getContent(), WorkflowCheckStage.SAVE, entity.getId());
|
workflowCheckService.checkOrThrow(entity.getContent(), WorkflowCheckStage.SAVE, entity.getId());
|
||||||
}
|
}
|
||||||
@@ -241,8 +413,48 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Result<List<Workflow>> list(Workflow entity, Boolean asTree, String sortKey, String sortType) {
|
||||||
|
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
|
||||||
|
workflowVisibilityQueryHelper.applyReadableAccess(queryWrapper);
|
||||||
|
applyPublishedOnlyFilter(queryWrapper);
|
||||||
|
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||||
|
List<Workflow> workflows = service.list(queryWrapper);
|
||||||
|
aiResourceApprovalStateService.fillWorkflowApprovalState(workflows);
|
||||||
|
return Result.ok(workflows);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Page<Workflow> queryPage(Page<Workflow> page, QueryWrapper queryWrapper) {
|
||||||
|
workflowVisibilityQueryHelper.applyReadableAccess(queryWrapper);
|
||||||
|
applyPublishedOnlyFilter(queryWrapper);
|
||||||
|
Page<Workflow> result = super.queryPage(page, queryWrapper);
|
||||||
|
aiResourceApprovalStateService.fillWorkflowApprovalState(result.getRecords());
|
||||||
|
aiResourceCreatorNameSupport.fillWorkflowCreatorNames(result.getRecords());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@PostMapping("remove")
|
||||||
|
public Result<?> remove(@JsonBody(value = "id", required = true) Serializable id) {
|
||||||
|
return Result.fail(1, "请使用发布状态操作删除工作流");
|
||||||
|
}
|
||||||
|
|
||||||
|
private Result<BigInteger> buildApprovalActionResult(ApprovalActionResult actionResult,
|
||||||
|
String approvalMessage,
|
||||||
|
String directMessage) {
|
||||||
|
return Result.ok(
|
||||||
|
actionResult.isApprovalRequired() ? approvalMessage : directMessage,
|
||||||
|
actionResult.getInstanceId()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Result onRemoveBefore(Collection<Serializable> ids) {
|
protected Result onRemoveBefore(Collection<Serializable> ids) {
|
||||||
|
for (Serializable id : ids) {
|
||||||
|
Workflow workflow = requireWorkflow(String.valueOf(id));
|
||||||
|
resourceAccessService.assertAccess(CategoryResourceType.WORKFLOW, workflow, ResourceAction.MANAGE, "无权限管理工作流");
|
||||||
|
}
|
||||||
QueryWrapper queryWrapper = QueryWrapper.create();
|
QueryWrapper queryWrapper = QueryWrapper.create();
|
||||||
queryWrapper.in("workflow_id", ids);
|
queryWrapper.in("workflow_id", ids);
|
||||||
boolean exists = botWorkflowService.exists(queryWrapper);
|
boolean exists = botWorkflowService.exists(queryWrapper);
|
||||||
@@ -251,4 +463,36 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void normalizeVisibilityScope(Workflow entity, boolean isSave) {
|
||||||
|
if (entity == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!StringUtils.hasLength(entity.getVisibilityScope())) {
|
||||||
|
if (isSave) {
|
||||||
|
entity.setVisibilityScope(VisibilityScope.PRIVATE.name());
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
entity.setVisibilityScope(VisibilityScope.from(entity.getVisibilityScope()).name());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Workflow requireWorkflow(String idOrAlias) {
|
||||||
|
Workflow workflow = service.getDetail(idOrAlias);
|
||||||
|
if (workflow == null) {
|
||||||
|
throw new BusinessException("工作流不存在");
|
||||||
|
}
|
||||||
|
return workflow;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyPublishedOnlyFilter(QueryWrapper queryWrapper) {
|
||||||
|
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||||
|
if (attributes == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String publishedOnly = attributes.getRequest().getParameter("publishedOnly");
|
||||||
|
if ("true".equalsIgnoreCase(publishedOnly)) {
|
||||||
|
queryWrapper.eq("publish_status", PublishStatus.PUBLISHED.getCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
package tech.easyflow.admin.controller.ai.support;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import tech.easyflow.agent.entity.Agent;
|
||||||
|
import tech.easyflow.ai.entity.Bot;
|
||||||
|
import tech.easyflow.ai.entity.DocumentCollection;
|
||||||
|
import tech.easyflow.ai.entity.Plugin;
|
||||||
|
import tech.easyflow.ai.entity.Workflow;
|
||||||
|
import tech.easyflow.system.service.SysAccountService;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.function.BiConsumer;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为 AI 资源批量补充创建人展示名称。
|
||||||
|
*
|
||||||
|
* <p>该组件只做展示字段填充,不参与权限或查询逻辑。</p>
|
||||||
|
*
|
||||||
|
* @author Codex
|
||||||
|
* @since 2026-04-12
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class AiResourceCreatorNameSupport {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private SysAccountService sysAccountService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量填充工作流创建人名称。
|
||||||
|
*
|
||||||
|
* @param workflows 工作流集合
|
||||||
|
*/
|
||||||
|
public void fillWorkflowCreatorNames(Collection<Workflow> workflows) {
|
||||||
|
fillCreatorNames(workflows, Workflow::getCreatedBy, Workflow::setCreatedByName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量填充聊天助手创建人名称。
|
||||||
|
*
|
||||||
|
* @param bots 聊天助手集合
|
||||||
|
*/
|
||||||
|
public void fillBotCreatorNames(Collection<Bot> bots) {
|
||||||
|
fillCreatorNames(bots, Bot::getCreatedBy, Bot::setCreatedByName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量填充知识库创建人名称。
|
||||||
|
*
|
||||||
|
* @param collections 知识库集合
|
||||||
|
*/
|
||||||
|
public void fillDocumentCollectionCreatorNames(Collection<DocumentCollection> collections) {
|
||||||
|
fillCreatorNames(collections, DocumentCollection::getCreatedBy, DocumentCollection::setCreatedByName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量填充插件创建人名称。
|
||||||
|
*
|
||||||
|
* @param plugins 插件集合
|
||||||
|
*/
|
||||||
|
public void fillPluginCreatorNames(Collection<Plugin> plugins) {
|
||||||
|
fillCreatorNames(plugins, Plugin::getCreatedBy, Plugin::setCreatedByName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量填充 Agent 创建人名称。
|
||||||
|
*
|
||||||
|
* @param agents Agent 集合
|
||||||
|
*/
|
||||||
|
public void fillAgentCreatorNames(Collection<Agent> agents) {
|
||||||
|
fillCreatorNames(agents, Agent::getCreatedBy, Agent::setCreatedByName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用的创建人名称填充逻辑。
|
||||||
|
*
|
||||||
|
* @param resources 资源集合
|
||||||
|
* @param createdByGetter 创建人 ID 提取函数
|
||||||
|
* @param createdByNameSetter 创建人名称回填函数
|
||||||
|
* @param <T> 资源类型
|
||||||
|
*/
|
||||||
|
private <T> void fillCreatorNames(
|
||||||
|
Collection<T> resources,
|
||||||
|
Function<T, Number> createdByGetter,
|
||||||
|
BiConsumer<T, String> createdByNameSetter
|
||||||
|
) {
|
||||||
|
if (resources == null || resources.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
LinkedHashSet<BigInteger> creatorIds = resources.stream()
|
||||||
|
.map(createdByGetter)
|
||||||
|
.map(this::toBigInteger)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new));
|
||||||
|
if (creatorIds.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<BigInteger, String> displayNameMap = sysAccountService.resolveDisplayNameMap(creatorIds);
|
||||||
|
for (T resource : resources) {
|
||||||
|
BigInteger creatorId = toBigInteger(createdByGetter.apply(resource));
|
||||||
|
if (creatorId == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
createdByNameSetter.accept(resource, displayNameMap.getOrDefault(creatorId, creatorId.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统一把不同数值类型转换为 {@link BigInteger}。
|
||||||
|
*
|
||||||
|
* @param value 原始数值
|
||||||
|
* @return 归一化后的 {@link BigInteger}
|
||||||
|
*/
|
||||||
|
private BigInteger toBigInteger(Number value) {
|
||||||
|
if (value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (value instanceof BigInteger) {
|
||||||
|
return (BigInteger) value;
|
||||||
|
}
|
||||||
|
return BigInteger.valueOf(value.longValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,16 @@
|
|||||||
package tech.easyflow.admin.controller.auth;
|
package tech.easyflow.admin.controller.auth;
|
||||||
|
|
||||||
|
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.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.auth.entity.LoginDTO;
|
import tech.easyflow.auth.entity.LoginDTO;
|
||||||
import tech.easyflow.auth.entity.LoginVO;
|
import tech.easyflow.auth.entity.LoginVO;
|
||||||
import tech.easyflow.auth.service.AuthService;
|
import tech.easyflow.auth.service.AuthService;
|
||||||
import tech.easyflow.common.domain.Result;
|
import tech.easyflow.common.domain.Result;
|
||||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
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 javax.annotation.Resource;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -27,6 +28,12 @@ public class AuthController {
|
|||||||
return Result.ok(res);
|
return Result.ok(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("loginByApiKey")
|
||||||
|
@SaIgnore
|
||||||
|
public Result<LoginVO> loginByApiKey(@JsonBody(value = "apiKey", required = true) String apiKey) {
|
||||||
|
return Result.ok(authService.loginByApiKey(apiKey));
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("logout")
|
@PostMapping("logout")
|
||||||
public Result<Void> logout() {
|
public Result<Void> logout() {
|
||||||
StpUtil.logout();
|
StpUtil.logout();
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package tech.easyflow.admin.controller.dashboard;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import tech.easyflow.admin.model.dashboard.DashboardOverviewQuery;
|
||||||
|
import tech.easyflow.admin.model.dashboard.DashboardOverviewVo;
|
||||||
|
import tech.easyflow.admin.model.dashboard.DashboardUserRankItemVo;
|
||||||
|
import tech.easyflow.admin.model.dashboard.DashboardUserRankQuery;
|
||||||
|
import tech.easyflow.admin.service.dashboard.DashboardService;
|
||||||
|
import tech.easyflow.common.domain.Result;
|
||||||
|
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理端工作台统计接口。
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/dashboard")
|
||||||
|
public class DashboardController {
|
||||||
|
|
||||||
|
private final DashboardService dashboardService;
|
||||||
|
|
||||||
|
public DashboardController(DashboardService dashboardService) {
|
||||||
|
this.dashboardService = dashboardService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/overview")
|
||||||
|
@SaCheckPermission("/api/v1/dashboard/query")
|
||||||
|
public Result<DashboardOverviewVo> overview(DashboardOverviewQuery query) {
|
||||||
|
return Result.ok(dashboardService.getOverview(SaTokenUtil.getLoginAccount(), query));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/user-ranks")
|
||||||
|
@SaCheckPermission("/api/v1/dashboard/query")
|
||||||
|
public Result<List<DashboardUserRankItemVo>> userRanks(DashboardUserRankQuery query) {
|
||||||
|
return Result.ok(dashboardService.getUserRanks(SaTokenUtil.getLoginAccount(), query));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping(value = "/user-ranks/export", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
|
||||||
|
@SaCheckPermission("/api/v1/dashboard/query")
|
||||||
|
public void exportUserRanks(DashboardUserRankQuery query, HttpServletResponse response) throws Exception {
|
||||||
|
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
||||||
|
response.setCharacterEncoding("utf-8");
|
||||||
|
String timestamp = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
|
||||||
|
String fileName = URLEncoder.encode("dashboard_user_ranks_" + timestamp, "UTF-8")
|
||||||
|
.replaceAll("\\+", "%20");
|
||||||
|
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
|
||||||
|
dashboardService.exportUserRanks(
|
||||||
|
SaTokenUtil.getLoginAccount(),
|
||||||
|
query,
|
||||||
|
response.getOutputStream()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package tech.easyflow.admin.controller.datacenter;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
|
import com.mybatisflex.core.paginate.Page;
|
||||||
|
import com.mybatisflex.core.row.Row;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import tech.easyflow.common.domain.Result;
|
||||||
|
import tech.easyflow.common.entity.LoginAccount;
|
||||||
|
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||||
|
import tech.easyflow.datacenter.entity.DatacenterTable;
|
||||||
|
import tech.easyflow.datacenter.execution.model.DatacenterQueryRequest;
|
||||||
|
import tech.easyflow.datacenter.execution.model.DatacenterSchemaResponse;
|
||||||
|
import tech.easyflow.datacenter.execution.model.DatasetRef;
|
||||||
|
import tech.easyflow.datacenter.meta.model.DatacenterBatchRemoveRequest;
|
||||||
|
import tech.easyflow.datacenter.meta.model.DatacenterSaveDescriptionsRequest;
|
||||||
|
import tech.easyflow.datacenter.execution.service.DatacenterDatasetQueryService;
|
||||||
|
import tech.easyflow.datacenter.meta.service.DatacenterDatasetRegistryService;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/datacenterDataset")
|
||||||
|
public class DatacenterDatasetController {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private DatacenterDatasetQueryService queryService;
|
||||||
|
@Resource
|
||||||
|
private DatacenterDatasetRegistryService registryService;
|
||||||
|
|
||||||
|
@PostMapping("/queryPage")
|
||||||
|
@SaCheckPermission("/api/v1/datacenterSource/query")
|
||||||
|
public Result<Page<Row>> queryPage(@RequestBody DatacenterQueryRequest request) {
|
||||||
|
return Result.ok(queryService.queryPage(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/schema")
|
||||||
|
@SaCheckPermission("/api/v1/datacenterSource/query")
|
||||||
|
public Result<DatacenterSchemaResponse> schema(DatasetRef datasetRef) {
|
||||||
|
return Result.ok(queryService.getSchema(datasetRef));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/managedTables")
|
||||||
|
@SaCheckPermission("/api/v1/datacenterSource/query")
|
||||||
|
public Result<List<DatacenterTable>> managedTables(BigInteger sourceId, BigInteger catalogId) {
|
||||||
|
return Result.ok(registryService.listManagedTables(sourceId, catalogId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/removeBatch")
|
||||||
|
@SaCheckPermission("/api/v1/datacenterSource/save")
|
||||||
|
public Result<Integer> removeBatch(@RequestBody DatacenterBatchRemoveRequest request) {
|
||||||
|
return Result.ok(registryService.removeTables(request == null ? List.of() : request.getTableIds()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/saveDescriptions")
|
||||||
|
@SaCheckPermission("/api/v1/datacenterSource/save")
|
||||||
|
public Result<DatacenterSchemaResponse> saveDescriptions(@RequestBody DatacenterSaveDescriptionsRequest request) {
|
||||||
|
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||||
|
DatacenterTable table = registryService.saveDescriptions(
|
||||||
|
request == null ? null : request.getTableId(),
|
||||||
|
request == null ? null : request.getTableDesc(),
|
||||||
|
request == null ? List.of() : request.getFields(),
|
||||||
|
account
|
||||||
|
);
|
||||||
|
return Result.ok(queryService.getSchema(registryService.resolveDatasetRef(table.getId())));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
package tech.easyflow.admin.controller.datacenter;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
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 org.springframework.web.multipart.MultipartFile;
|
||||||
|
import tech.easyflow.common.domain.Result;
|
||||||
|
import tech.easyflow.common.entity.LoginAccount;
|
||||||
|
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||||
|
import tech.easyflow.datacenter.excel.model.DatacenterExcelDeriveRequest;
|
||||||
|
import tech.easyflow.datacenter.excel.model.DatacenterExcelExportRequest;
|
||||||
|
import tech.easyflow.datacenter.excel.model.DatacenterExcelMergeRequest;
|
||||||
|
import tech.easyflow.datacenter.excel.model.DatacenterExcelSplitRequest;
|
||||||
|
import tech.easyflow.datacenter.excel.service.DatacenterExcelImportService;
|
||||||
|
import tech.easyflow.datacenter.meta.entity.DatacenterImportJob;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/datacenterExcel")
|
||||||
|
public class DatacenterExcelController {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private DatacenterExcelImportService excelImportService;
|
||||||
|
|
||||||
|
@PostMapping("/import")
|
||||||
|
@SaCheckPermission("/api/v1/datacenterSource/save")
|
||||||
|
public Result<DatacenterImportJob> importWorkbook(MultipartFile file) throws Exception {
|
||||||
|
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||||
|
return Result.ok(excelImportService.importWorkbook(file, account));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/split")
|
||||||
|
@SaCheckPermission("/api/v1/datacenterSource/save")
|
||||||
|
public Result<DatacenterImportJob> split(@RequestBody DatacenterExcelSplitRequest request) {
|
||||||
|
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||||
|
return Result.ok(excelImportService.splitWorkbook(request, account));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/merge")
|
||||||
|
@SaCheckPermission("/api/v1/datacenterSource/save")
|
||||||
|
public Result<DatacenterImportJob> merge(@RequestBody DatacenterExcelMergeRequest request) {
|
||||||
|
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||||
|
return Result.ok(excelImportService.mergeWorkbook(request, account));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/derive")
|
||||||
|
@SaCheckPermission("/api/v1/datacenterSource/save")
|
||||||
|
public Result<DatacenterImportJob> derive(@RequestBody DatacenterExcelDeriveRequest request) {
|
||||||
|
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||||
|
return Result.ok(excelImportService.deriveWorkbook(request, account));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/export")
|
||||||
|
@SaCheckPermission("/api/v1/datacenterSource/query")
|
||||||
|
public Result<DatacenterImportJob> export(@RequestBody DatacenterExcelExportRequest request) throws Exception {
|
||||||
|
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||||
|
return Result.ok(excelImportService.exportWorkbook(request, account));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/importJob/detail")
|
||||||
|
@SaCheckPermission("/api/v1/datacenterSource/query")
|
||||||
|
public Result<DatacenterImportJob> jobDetail(BigInteger jobId) {
|
||||||
|
return Result.ok(excelImportService.getImportJobDetail(jobId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/job/detail")
|
||||||
|
@SaCheckPermission("/api/v1/datacenterSource/query")
|
||||||
|
public Result<DatacenterImportJob> newJobDetail(BigInteger jobId) {
|
||||||
|
return Result.ok(excelImportService.getImportJobDetail(jobId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/job/list")
|
||||||
|
@SaCheckPermission("/api/v1/datacenterSource/query")
|
||||||
|
public Result<List<DatacenterImportJob>> jobList(BigInteger sourceId, BigInteger tableId) {
|
||||||
|
return Result.ok(excelImportService.listJobs(sourceId, tableId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/download")
|
||||||
|
@SaCheckPermission("/api/v1/datacenterSource/query")
|
||||||
|
public void download(BigInteger jobId, HttpServletResponse response) throws Exception {
|
||||||
|
DatacenterImportJob job = excelImportService.getImportJobDetail(jobId);
|
||||||
|
if (job.getStoragePath() == null || job.getStoragePath().isBlank()) {
|
||||||
|
throw new IllegalStateException("导出文件不存在");
|
||||||
|
}
|
||||||
|
File file = new File(job.getStoragePath());
|
||||||
|
if (!file.exists()) {
|
||||||
|
throw new IllegalStateException("导出文件不存在");
|
||||||
|
}
|
||||||
|
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
||||||
|
response.setCharacterEncoding("utf-8");
|
||||||
|
String fileName = URLEncoder.encode(job.getFileName(), "UTF-8").replaceAll("\\+", "%20");
|
||||||
|
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName);
|
||||||
|
try (FileInputStream inputStream = new FileInputStream(file)) {
|
||||||
|
inputStream.transferTo(response.getOutputStream());
|
||||||
|
response.flushBuffer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package tech.easyflow.admin.controller.datacenter;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
|
import com.mybatisflex.core.paginate.Page;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import tech.easyflow.common.domain.Result;
|
||||||
|
import tech.easyflow.common.entity.LoginAccount;
|
||||||
|
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||||
|
import tech.easyflow.datacenter.entity.DatacenterTable;
|
||||||
|
import tech.easyflow.datacenter.execution.model.DatacenterConnectionTestResult;
|
||||||
|
import tech.easyflow.datacenter.meta.entity.DatacenterSource;
|
||||||
|
import tech.easyflow.datacenter.meta.model.DatacenterBatchRegisterRequest;
|
||||||
|
import tech.easyflow.datacenter.meta.model.DatacenterCatalogMeta;
|
||||||
|
import tech.easyflow.datacenter.meta.model.DatacenterRemoveSourceRequest;
|
||||||
|
import tech.easyflow.datacenter.meta.model.DatacenterTableDetailMeta;
|
||||||
|
import tech.easyflow.datacenter.meta.service.DatacenterSourceService;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/datacenterSource")
|
||||||
|
public class DatacenterSourceController {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private DatacenterSourceService sourceService;
|
||||||
|
|
||||||
|
@PostMapping("/testConnection")
|
||||||
|
@SaCheckPermission("/api/v1/datacenterSource/query")
|
||||||
|
public Result<DatacenterConnectionTestResult> testConnection(@RequestBody DatacenterSource source) {
|
||||||
|
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||||
|
return Result.ok(sourceService.testConnection(source, account));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/save")
|
||||||
|
@SaCheckPermission("/api/v1/datacenterSource/save")
|
||||||
|
public Result<DatacenterSource> save(@RequestBody DatacenterSource source) {
|
||||||
|
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||||
|
return Result.ok(sourceService.saveSource(source, account));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/page")
|
||||||
|
@SaCheckPermission("/api/v1/datacenterSource/query")
|
||||||
|
public Result<Page<DatacenterSource>> page(Long pageNumber, Long pageSize) {
|
||||||
|
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||||
|
return Result.ok(sourceService.pageSources(pageNumber, pageSize, account));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/catalogs")
|
||||||
|
@SaCheckPermission("/api/v1/datacenterSource/query")
|
||||||
|
public Result<List<DatacenterCatalogMeta>> catalogs(BigInteger sourceId) {
|
||||||
|
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||||
|
return Result.ok(sourceService.listCatalogs(sourceId, account));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/tables")
|
||||||
|
@SaCheckPermission("/api/v1/datacenterSource/query")
|
||||||
|
public Result<List<DatacenterTable>> tables(BigInteger sourceId, String catalogName) {
|
||||||
|
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||||
|
return Result.ok(sourceService.listTables(sourceId, catalogName, account));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/tableDetail")
|
||||||
|
@SaCheckPermission("/api/v1/datacenterSource/query")
|
||||||
|
public Result<DatacenterTableDetailMeta> tableDetail(BigInteger sourceId, String catalogName, String tableName,
|
||||||
|
@RequestParam(defaultValue = "false") boolean register) {
|
||||||
|
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||||
|
return Result.ok(sourceService.getTableDetail(sourceId, catalogName, tableName, register, account));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/registerBatch")
|
||||||
|
@SaCheckPermission("/api/v1/datacenterSource/save")
|
||||||
|
public Result<List<DatacenterTable>> registerBatch(@RequestBody DatacenterBatchRegisterRequest request) {
|
||||||
|
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||||
|
return Result.ok(sourceService.batchRegisterTables(request, account));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/remove")
|
||||||
|
@SaCheckPermission("/api/v1/datacenterSource/save")
|
||||||
|
public Result<Void> remove(@RequestBody DatacenterRemoveSourceRequest request) {
|
||||||
|
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||||
|
sourceService.removeSource(request == null ? null : request.getSourceId(), account);
|
||||||
|
return Result.ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,195 +0,0 @@
|
|||||||
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<>()); // 写入空数据,只生成模板
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
package tech.easyflow.admin.controller.system;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
|
import com.mybatisflex.core.paginate.Page;
|
||||||
|
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.satoken.util.SaTokenUtil;
|
||||||
|
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||||
|
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||||
|
import tech.easyflow.approval.entity.vo.ApprovalAssigneeOptionVo;
|
||||||
|
import tech.easyflow.approval.entity.vo.ApprovalFlowDetailVo;
|
||||||
|
import tech.easyflow.approval.entity.vo.ApprovalFlowPageVo;
|
||||||
|
import tech.easyflow.approval.service.ApprovalAssigneeService;
|
||||||
|
import tech.easyflow.approval.service.ApprovalFlowService;
|
||||||
|
import tech.easyflow.system.service.CategoryPermissionService;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 审批流程管理控制器。
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/approvalFlow")
|
||||||
|
public class ApprovalFlowController {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ApprovalFlowService approvalFlowService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private CategoryPermissionService categoryPermissionService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ApprovalAssigneeService approvalAssigneeService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询审批流程。
|
||||||
|
*
|
||||||
|
* @param name 流程名称
|
||||||
|
* @param resourceType 资源类型
|
||||||
|
* @param actionType 动作类型
|
||||||
|
* @param status 流程状态
|
||||||
|
* @param pageNumber 页码
|
||||||
|
* @param pageSize 每页数量
|
||||||
|
* @return 分页结果
|
||||||
|
*/
|
||||||
|
@GetMapping("/page")
|
||||||
|
@SaCheckPermission("/api/v1/approvalFlow/query")
|
||||||
|
public Result<Page<ApprovalFlowPageVo>> page(String name, String resourceType, String actionType, String status,
|
||||||
|
Long pageNumber, Long pageSize) {
|
||||||
|
return Result.ok(approvalFlowService.pageFlows(name, resourceType, actionType, status, pageNumber, pageSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询审批流程详情。
|
||||||
|
*
|
||||||
|
* @param id 流程ID
|
||||||
|
* @return 流程详情
|
||||||
|
*/
|
||||||
|
@GetMapping("/detail")
|
||||||
|
@SaCheckPermission("/api/v1/approvalFlow/query")
|
||||||
|
public Result<ApprovalFlowDetailVo> detail(BigInteger id) {
|
||||||
|
return Result.ok(approvalFlowService.getFlowDetail(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询审批角色选项。
|
||||||
|
*
|
||||||
|
* @return 角色选项
|
||||||
|
*/
|
||||||
|
@GetMapping("/assigneeRoleOptions")
|
||||||
|
@SaCheckPermission("/api/v1/approvalFlow/query")
|
||||||
|
public Result<List<ApprovalAssigneeOptionVo>> assigneeRoleOptions() {
|
||||||
|
return Result.ok(approvalAssigneeService.listRoleOptions());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询审批用户选项。
|
||||||
|
*
|
||||||
|
* @param keyword 关键词
|
||||||
|
* @param pageNumber 页码
|
||||||
|
* @param pageSize 每页数量
|
||||||
|
* @return 用户选项分页
|
||||||
|
*/
|
||||||
|
@GetMapping("/assigneeAccountPage")
|
||||||
|
@SaCheckPermission("/api/v1/approvalFlow/query")
|
||||||
|
public Result<Page<ApprovalAssigneeOptionVo>> assigneeAccountPage(String keyword, Long pageNumber, Long pageSize) {
|
||||||
|
return Result.ok(approvalAssigneeService.pageAccountOptions(keyword, pageNumber, pageSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增审批流程。
|
||||||
|
*
|
||||||
|
* @param request 流程请求体
|
||||||
|
* @return 新增流程ID
|
||||||
|
*/
|
||||||
|
@PostMapping("/save")
|
||||||
|
@SaCheckPermission("/api/v1/approvalFlow/save")
|
||||||
|
public Result<BigInteger> save(@JsonBody ApprovalFlowDetailVo request) {
|
||||||
|
assertSuperAdmin();
|
||||||
|
BigInteger operatorId = SaTokenUtil.getLoginAccount().getId();
|
||||||
|
return Result.ok(approvalFlowService.saveFlow(request, operatorId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新审批流程。
|
||||||
|
*
|
||||||
|
* @param request 流程请求体
|
||||||
|
* @return 处理结果
|
||||||
|
*/
|
||||||
|
@PostMapping("/update")
|
||||||
|
@SaCheckPermission("/api/v1/approvalFlow/save")
|
||||||
|
public Result<Void> update(@JsonBody ApprovalFlowDetailVo request) {
|
||||||
|
assertSuperAdmin();
|
||||||
|
BigInteger operatorId = SaTokenUtil.getLoginAccount().getId();
|
||||||
|
approvalFlowService.updateFlow(request, operatorId);
|
||||||
|
return Result.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启用审批流程。
|
||||||
|
*
|
||||||
|
* @param id 流程ID
|
||||||
|
* @return 处理结果
|
||||||
|
*/
|
||||||
|
@PostMapping("/enable")
|
||||||
|
@SaCheckPermission("/api/v1/approvalFlow/enable")
|
||||||
|
public Result<Void> enable(@JsonBody(value = "id", required = true) BigInteger id) {
|
||||||
|
assertSuperAdmin();
|
||||||
|
approvalFlowService.enableFlow(id, SaTokenUtil.getLoginAccount().getId());
|
||||||
|
return Result.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停用审批流程。
|
||||||
|
*
|
||||||
|
* @param id 流程ID
|
||||||
|
* @return 处理结果
|
||||||
|
*/
|
||||||
|
@PostMapping("/disable")
|
||||||
|
@SaCheckPermission("/api/v1/approvalFlow/disable")
|
||||||
|
public Result<Void> disable(@JsonBody(value = "id", required = true) BigInteger id) {
|
||||||
|
assertSuperAdmin();
|
||||||
|
approvalFlowService.disableFlow(id, SaTokenUtil.getLoginAccount().getId());
|
||||||
|
return Result.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除审批流程。
|
||||||
|
*
|
||||||
|
* @param id 流程ID
|
||||||
|
* @return 处理结果
|
||||||
|
*/
|
||||||
|
@PostMapping("/remove")
|
||||||
|
@SaCheckPermission("/api/v1/approvalFlow/remove")
|
||||||
|
public Result<Void> remove(@JsonBody(value = "id", required = true) BigInteger id) {
|
||||||
|
assertSuperAdmin();
|
||||||
|
approvalFlowService.removeFlow(id);
|
||||||
|
return Result.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertSuperAdmin() {
|
||||||
|
if (!categoryPermissionService.isCurrentSuperAdmin()) {
|
||||||
|
throw new BusinessException("仅超级管理员可管理审批流程");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
package tech.easyflow.admin.controller.system;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
|
import com.mybatisflex.core.paginate.Page;
|
||||||
|
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.satoken.util.SaTokenUtil;
|
||||||
|
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||||
|
import tech.easyflow.approval.entity.vo.ApprovalActionRequest;
|
||||||
|
import tech.easyflow.approval.entity.vo.ApprovalInstanceDetailVo;
|
||||||
|
import tech.easyflow.approval.entity.vo.ApprovalInstancePageVo;
|
||||||
|
import tech.easyflow.approval.service.ApprovalInstanceService;
|
||||||
|
import tech.easyflow.approval.service.ApprovalQueryService;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 审批实例控制器。
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/approvalInstance")
|
||||||
|
public class ApprovalInstanceController {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ApprovalQueryService approvalQueryService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ApprovalInstanceService approvalInstanceService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询待审批列表。
|
||||||
|
*
|
||||||
|
* @param resourceType 资源类型
|
||||||
|
* @param actionType 动作类型
|
||||||
|
* @param keyword 关键词
|
||||||
|
* @param pageNumber 页码
|
||||||
|
* @param pageSize 每页数量
|
||||||
|
* @return 分页结果
|
||||||
|
*/
|
||||||
|
@GetMapping("/pendingPage")
|
||||||
|
@SaCheckPermission("/api/v1/approvalInstance/query")
|
||||||
|
public Result<Page<ApprovalInstancePageVo>> pendingPage(String resourceType, String actionType, String keyword,
|
||||||
|
Long pageNumber, Long pageSize) {
|
||||||
|
return Result.ok(approvalQueryService.pendingPage(resourceType, actionType, keyword, pageNumber, pageSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询已审批列表。
|
||||||
|
*
|
||||||
|
* @param resourceType 资源类型
|
||||||
|
* @param actionType 动作类型
|
||||||
|
* @param keyword 关键词
|
||||||
|
* @param pageNumber 页码
|
||||||
|
* @param pageSize 每页数量
|
||||||
|
* @return 分页结果
|
||||||
|
*/
|
||||||
|
@GetMapping("/processedPage")
|
||||||
|
@SaCheckPermission("/api/v1/approvalInstance/query")
|
||||||
|
public Result<Page<ApprovalInstancePageVo>> processedPage(String resourceType, String actionType, String keyword,
|
||||||
|
Long pageNumber, Long pageSize) {
|
||||||
|
return Result.ok(approvalQueryService.processedPage(resourceType, actionType, keyword, pageNumber, pageSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询我发起的审批列表。
|
||||||
|
*
|
||||||
|
* @param resourceType 资源类型
|
||||||
|
* @param actionType 动作类型
|
||||||
|
* @param keyword 关键词
|
||||||
|
* @param pageNumber 页码
|
||||||
|
* @param pageSize 每页数量
|
||||||
|
* @return 分页结果
|
||||||
|
*/
|
||||||
|
@GetMapping("/initiatedPage")
|
||||||
|
@SaCheckPermission("/api/v1/approvalInstance/query")
|
||||||
|
public Result<Page<ApprovalInstancePageVo>> initiatedPage(String resourceType, String actionType, String keyword,
|
||||||
|
Long pageNumber, Long pageSize) {
|
||||||
|
return Result.ok(approvalQueryService.initiatedPage(resourceType, actionType, keyword, pageNumber, pageSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询审批实例详情。
|
||||||
|
*
|
||||||
|
* @param id 审批实例ID
|
||||||
|
* @return 详情结果
|
||||||
|
*/
|
||||||
|
@GetMapping("/detail")
|
||||||
|
@SaCheckPermission("/api/v1/approvalInstance/query")
|
||||||
|
public Result<ApprovalInstanceDetailVo> detail(BigInteger id) {
|
||||||
|
return Result.ok(approvalQueryService.detail(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过审批。
|
||||||
|
*
|
||||||
|
* @param request 审批动作请求
|
||||||
|
* @return 处理结果
|
||||||
|
*/
|
||||||
|
@PostMapping("/approve")
|
||||||
|
@SaCheckPermission("/api/v1/approvalInstance/approve")
|
||||||
|
public Result<Void> approve(@JsonBody ApprovalActionRequest request) {
|
||||||
|
approvalInstanceService.approve(request.getInstanceId(), request.getComment(), currentOperatorId());
|
||||||
|
return Result.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 驳回审批。
|
||||||
|
*
|
||||||
|
* @param request 审批动作请求
|
||||||
|
* @return 处理结果
|
||||||
|
*/
|
||||||
|
@PostMapping("/reject")
|
||||||
|
@SaCheckPermission("/api/v1/approvalInstance/reject")
|
||||||
|
public Result<Void> reject(@JsonBody ApprovalActionRequest request) {
|
||||||
|
approvalInstanceService.reject(request.getInstanceId(), request.getComment(), currentOperatorId());
|
||||||
|
return Result.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 撤回审批。
|
||||||
|
*
|
||||||
|
* @param request 审批动作请求
|
||||||
|
* @return 处理结果
|
||||||
|
*/
|
||||||
|
@PostMapping("/revoke")
|
||||||
|
@SaCheckPermission("/api/v1/approvalInstance/revoke")
|
||||||
|
public Result<Void> revoke(@JsonBody ApprovalActionRequest request) {
|
||||||
|
approvalInstanceService.revoke(request.getInstanceId(), request.getComment(), currentOperatorId());
|
||||||
|
return Result.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
private BigInteger currentOperatorId() {
|
||||||
|
return SaTokenUtil.getLoginAccount().getId();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,27 +1,33 @@
|
|||||||
package tech.easyflow.admin.controller.system;
|
package tech.easyflow.admin.controller.system;
|
||||||
|
|
||||||
import tech.easyflow.common.constant.enums.EnumAccountType;
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
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.dev33.satoken.stp.StpUtil;
|
||||||
import cn.hutool.crypto.digest.BCrypt;
|
import cn.hutool.crypto.digest.BCrypt;
|
||||||
import com.mybatisflex.core.paginate.Page;
|
import com.mybatisflex.core.paginate.Page;
|
||||||
import com.mybatisflex.core.query.QueryWrapper;
|
import com.mybatisflex.core.query.QueryWrapper;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import org.springframework.dao.DuplicateKeyException;
|
import org.springframework.dao.DuplicateKeyException;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
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.satoken.util.SaTokenUtil;
|
||||||
|
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.entity.vo.SysAccountBatchActionResultVo;
|
||||||
|
import tech.easyflow.system.entity.vo.SysAccountImportResultVo;
|
||||||
|
import tech.easyflow.system.service.SysAccountService;
|
||||||
|
import tech.easyflow.system.util.SysPasswordPolicy;
|
||||||
|
|
||||||
|
import java.net.URLEncoder;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
@@ -62,13 +68,21 @@ public class SysAccountController extends BaseCurdController<SysAccountService,
|
|||||||
return Result.fail(1, "用户名已存在");
|
return Result.fail(1, "用户名已存在");
|
||||||
}
|
}
|
||||||
String password = entity.getPassword();
|
String password = entity.getPassword();
|
||||||
if (StringUtil.hasText(password)) {
|
if (!StringUtil.hasText(password)) {
|
||||||
entity.setPassword(BCrypt.hashpw(password));
|
return Result.fail(1, "密码不能为空");
|
||||||
}
|
}
|
||||||
|
SysPasswordPolicy.validateStrongPassword(password);
|
||||||
|
entity.setPassword(BCrypt.hashpw(password));
|
||||||
Integer status = entity.getStatus();
|
Integer status = entity.getStatus();
|
||||||
if (status == null) {
|
if (status == null) {
|
||||||
entity.setStatus(EnumDataStatus.AVAILABLE.getCode());
|
entity.setStatus(EnumDataStatus.AVAILABLE.getCode());
|
||||||
}
|
}
|
||||||
|
if (entity.getAccountType() == null) {
|
||||||
|
entity.setAccountType(EnumAccountType.NORMAL.getCode());
|
||||||
|
}
|
||||||
|
if (entity.getPasswordResetRequired() == null) {
|
||||||
|
entity.setPasswordResetRequired(false);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
SysAccount record = service.getById(entity.getId());
|
SysAccount record = service.getById(entity.getId());
|
||||||
// 如果修改了部门,就将用户踢下线,避免用户操作数据造成数据错误
|
// 如果修改了部门,就将用户踢下线,避免用户操作数据造成数据错误
|
||||||
@@ -149,15 +163,62 @@ public class SysAccountController extends BaseCurdController<SysAccountService,
|
|||||||
if (!newPassword.equals(confirmPassword)) {
|
if (!newPassword.equals(confirmPassword)) {
|
||||||
return Result.fail(2, "两次密码不一致");
|
return Result.fail(2, "两次密码不一致");
|
||||||
}
|
}
|
||||||
|
SysPasswordPolicy.validateStrongPassword(newPassword);
|
||||||
SysAccount update = new SysAccount();
|
SysAccount update = new SysAccount();
|
||||||
update.setId(loginAccountId);
|
update.setId(loginAccountId);
|
||||||
update.setPassword(BCrypt.hashpw(newPassword));
|
update.setPassword(BCrypt.hashpw(newPassword));
|
||||||
|
update.setPasswordResetRequired(false);
|
||||||
update.setModified(new Date());
|
update.setModified(new Date());
|
||||||
update.setModifiedBy(loginAccountId);
|
update.setModifiedBy(loginAccountId);
|
||||||
service.updateById(update);
|
service.updateById(update);
|
||||||
return Result.ok();
|
return Result.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/resetPassword")
|
||||||
|
@SaCheckPermission("/api/v1/sysAccount/save")
|
||||||
|
public Result<Void> resetPassword(@JsonBody(value = "id", required = true) BigInteger id) {
|
||||||
|
service.resetPassword(id, SaTokenUtil.getLoginAccount().getId());
|
||||||
|
return Result.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/removeBatchWithResult")
|
||||||
|
@SaCheckPermission("/api/v1/sysAccount/remove")
|
||||||
|
@LogRecord("批量删除用户")
|
||||||
|
public Result<SysAccountBatchActionResultVo> removeBatchWithResult(
|
||||||
|
@JsonBody(value = "ids", required = true) List<BigInteger> ids) {
|
||||||
|
if (ids == null || ids.isEmpty()) {
|
||||||
|
return Result.fail("ids不能为空", null);
|
||||||
|
}
|
||||||
|
return Result.ok(service.removeBatchWithResult(ids));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/resetPasswordBatch")
|
||||||
|
@SaCheckPermission("/api/v1/sysAccount/save")
|
||||||
|
@LogRecord("批量重置用户密码")
|
||||||
|
public Result<SysAccountBatchActionResultVo> resetPasswordBatch(
|
||||||
|
@JsonBody(value = "ids", required = true) List<BigInteger> ids) {
|
||||||
|
if (ids == null || ids.isEmpty()) {
|
||||||
|
return Result.fail("ids不能为空", null);
|
||||||
|
}
|
||||||
|
return Result.ok(service.resetPasswordBatch(ids, SaTokenUtil.getLoginAccount().getId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/importExcel")
|
||||||
|
@SaCheckPermission("/api/v1/sysAccount/save")
|
||||||
|
public Result<SysAccountImportResultVo> importExcel(MultipartFile file) {
|
||||||
|
return Result.ok(service.importAccounts(file, SaTokenUtil.getLoginAccount()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/downloadImportTemplate")
|
||||||
|
@SaCheckPermission("/api/v1/sysAccount/query")
|
||||||
|
public void downloadImportTemplate(HttpServletResponse response) throws Exception {
|
||||||
|
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
||||||
|
response.setCharacterEncoding("utf-8");
|
||||||
|
String fileName = URLEncoder.encode("user_import_template", "UTF-8").replaceAll("\\+", "%20");
|
||||||
|
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
|
||||||
|
service.writeImportTemplate(response.getOutputStream());
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@PostMapping("save")
|
@PostMapping("save")
|
||||||
public Result<?> save(@JsonBody SysAccount entity) {
|
public Result<?> save(@JsonBody SysAccount entity) {
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import org.springframework.web.bind.annotation.GetMapping;
|
|||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import tech.easyflow.ai.service.KnowledgeSharePermissionService;
|
||||||
|
import tech.easyflow.ai.service.WorkflowApiPermissionService;
|
||||||
import tech.easyflow.common.domain.Result;
|
import tech.easyflow.common.domain.Result;
|
||||||
import tech.easyflow.common.entity.LoginAccount;
|
import tech.easyflow.common.entity.LoginAccount;
|
||||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||||
@@ -43,6 +45,10 @@ public class SysApiKeyController extends BaseCurdController<SysApiKeyService, Sy
|
|||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private SysApiKeyResourceMappingService sysApiKeyResourceMappingService;
|
private SysApiKeyResourceMappingService sysApiKeyResourceMappingService;
|
||||||
|
@Resource
|
||||||
|
private KnowledgeSharePermissionService knowledgeSharePermissionService;
|
||||||
|
@Resource
|
||||||
|
private WorkflowApiPermissionService workflowApiPermissionService;
|
||||||
/**
|
/**
|
||||||
* 添加(保存)数据
|
* 添加(保存)数据
|
||||||
*
|
*
|
||||||
@@ -79,10 +85,23 @@ public class SysApiKeyController extends BaseCurdController<SysApiKeyService, Sy
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onSaveOrUpdateAfter(SysApiKey entity, boolean isSave) {
|
protected void onSaveOrUpdateAfter(SysApiKey entity, boolean isSave) {
|
||||||
if (!isSave && entity.getPermissionIds() != null && !entity.getPermissionIds().isEmpty()) {
|
if (entity.getPermissionIds() != null) {
|
||||||
// 修改的时候绑定授权接口
|
|
||||||
sysApiKeyResourceMappingService.authInterface(entity);
|
sysApiKeyResourceMappingService.authInterface(entity);
|
||||||
}
|
}
|
||||||
|
if (entity.getKnowledgeShareEnabled() != null) {
|
||||||
|
knowledgeSharePermissionService.replaceApiShareEnabled(entity.getId(), entity.getKnowledgeShareEnabled());
|
||||||
|
}
|
||||||
|
if (entity.getWorkflowApiEnabled() != null) {
|
||||||
|
workflowApiPermissionService.replaceWorkflowApiEnabled(entity.getId(), entity.getWorkflowApiEnabled());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@GetMapping("/detail")
|
||||||
|
public Result<SysApiKey> detail(String id) {
|
||||||
|
Result<SysApiKey> result = super.detail(id);
|
||||||
|
fillApiKeyPermissions(result.getData());
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -91,11 +110,36 @@ public class SysApiKeyController extends BaseCurdController<SysApiKeyService, Sy
|
|||||||
Result<Page<SysApiKey>> pageResult = (Result<Page<SysApiKey>>) super.page(request, sortKey, sortType, pageNumber, pageSize);
|
Result<Page<SysApiKey>> pageResult = (Result<Page<SysApiKey>>) super.page(request, sortKey, sortType, pageNumber, pageSize);
|
||||||
Page<SysApiKey> data = pageResult.getData();
|
Page<SysApiKey> data = pageResult.getData();
|
||||||
List<SysApiKey> records = data.getRecords();
|
List<SysApiKey> records = data.getRecords();
|
||||||
records.forEach(record -> {
|
records.forEach(this::fillApiKeyPermissions);
|
||||||
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;
|
return pageResult;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
/**
|
||||||
|
* 回填访问令牌的接口与知识库授权。
|
||||||
|
*
|
||||||
|
* @param entity 访问令牌
|
||||||
|
*/
|
||||||
|
private void fillApiKeyPermissions(SysApiKey entity) {
|
||||||
|
if (entity == null || entity.getId() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
QueryWrapper interfaceWrapper = QueryWrapper.create()
|
||||||
|
.select(SysApiKeyResourceMapping::getApiKeyResourceId)
|
||||||
|
.eq(SysApiKeyResourceMapping::getApiKeyId, entity.getId())
|
||||||
|
.isNull(SysApiKeyResourceMapping::getResourceType);
|
||||||
|
List<BigInteger> resourceIds = sysApiKeyResourceMappingService.listAs(interfaceWrapper, BigInteger.class);
|
||||||
|
entity.setPermissionIds(resourceIds);
|
||||||
|
|
||||||
|
QueryWrapper knowledgeWrapper = QueryWrapper.create()
|
||||||
|
.select(SysApiKeyResourceMapping::getId)
|
||||||
|
.eq(SysApiKeyResourceMapping::getApiKeyId, entity.getId())
|
||||||
|
.eq(SysApiKeyResourceMapping::getResourceType, "KNOWLEDGE");
|
||||||
|
entity.setKnowledgeShareEnabled(sysApiKeyResourceMappingService.count(knowledgeWrapper) > 0);
|
||||||
|
|
||||||
|
QueryWrapper workflowWrapper = QueryWrapper.create()
|
||||||
|
.select(SysApiKeyResourceMapping::getId)
|
||||||
|
.eq(SysApiKeyResourceMapping::getApiKeyId, entity.getId())
|
||||||
|
.eq(SysApiKeyResourceMapping::getResourceType, WorkflowApiPermissionService.RESOURCE_TYPE_WORKFLOW);
|
||||||
|
entity.setWorkflowApiEnabled(sysApiKeyResourceMappingService.count(workflowWrapper) > 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
package tech.easyflow.admin.controller.system;
|
package tech.easyflow.admin.controller.system;
|
||||||
|
|
||||||
|
import com.mybatisflex.core.query.QueryWrapper;
|
||||||
import tech.easyflow.common.annotation.UsePermission;
|
import tech.easyflow.common.annotation.UsePermission;
|
||||||
|
import tech.easyflow.common.domain.Result;
|
||||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import tech.easyflow.system.entity.SysApiKeyResource;
|
import tech.easyflow.system.entity.SysApiKeyResource;
|
||||||
import tech.easyflow.system.service.SysApiKeyResourceService;
|
import tech.easyflow.system.service.SysApiKeyResourceService;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 请求接口表 控制层。
|
* 请求接口表 控制层。
|
||||||
*
|
*
|
||||||
@@ -20,4 +25,26 @@ public class SysApiKeyResourceController extends BaseCurdController<SysApiKeyRes
|
|||||||
public SysApiKeyResourceController(SysApiKeyResourceService service) {
|
public SysApiKeyResourceController(SysApiKeyResourceService service) {
|
||||||
super(service);
|
super(service);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
/**
|
||||||
|
* 查询普通 API Key 接口授权资源。
|
||||||
|
*
|
||||||
|
* <p>工作流 Public API 使用独立的全局授权开关,不进入普通接口授权列表,避免用户误以为勾选
|
||||||
|
* 具体接口资源即可完成工作流调用授权。</p>
|
||||||
|
*
|
||||||
|
* @param entity 查询条件
|
||||||
|
* @param asTree 是否树形返回
|
||||||
|
* @param sortKey 排序字段
|
||||||
|
* @param sortType 排序方向
|
||||||
|
* @return 普通接口授权资源
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@GetMapping("list")
|
||||||
|
public Result<List<SysApiKeyResource>> list(SysApiKeyResource entity, Boolean asTree, String sortKey, String sortType) {
|
||||||
|
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
|
||||||
|
queryWrapper.notLike(SysApiKeyResource::getRequestInterface, "/public-api/workflow/");
|
||||||
|
queryWrapper.notLike(SysApiKeyResource::getRequestInterface, "/public-api/knowledge-share/");
|
||||||
|
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||||
|
return Result.ok(service.list(queryWrapper));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package tech.easyflow.admin.controller.system;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
|
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.satoken.util.SaTokenUtil;
|
||||||
|
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||||
|
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||||
|
import tech.easyflow.system.entity.vo.SysRoleCategoryScopeDetailVo;
|
||||||
|
import tech.easyflow.system.service.CategoryPermissionService;
|
||||||
|
import tech.easyflow.system.service.SysRoleCategoryScopeService;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/sysRoleCategoryScope")
|
||||||
|
public class SysRoleCategoryScopeController {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private SysRoleCategoryScopeService sysRoleCategoryScopeService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private CategoryPermissionService categoryPermissionService;
|
||||||
|
|
||||||
|
@GetMapping("/detail")
|
||||||
|
@SaCheckPermission("/api/v1/sysRole/query")
|
||||||
|
public Result<SysRoleCategoryScopeDetailVo> detail(BigInteger roleId) {
|
||||||
|
SysRoleCategoryScopeDetailVo detail = sysRoleCategoryScopeService.getRoleScopeDetail(roleId);
|
||||||
|
detail.setEditable(categoryPermissionService.isCurrentSuperAdmin());
|
||||||
|
return Result.ok(detail);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/save")
|
||||||
|
@SaCheckPermission("/api/v1/sysRole/save")
|
||||||
|
public Result<Void> save(@JsonBody SysRoleCategoryScopeDetailVo request) {
|
||||||
|
assertSuperAdmin();
|
||||||
|
if (request == null || request.getRoleId() == null) {
|
||||||
|
throw new BusinessException("角色ID不能为空");
|
||||||
|
}
|
||||||
|
BigInteger operatorId = SaTokenUtil.getLoginAccount().getId();
|
||||||
|
sysRoleCategoryScopeService.saveRoleScopes(request.getRoleId(), request.getScopes(), operatorId);
|
||||||
|
return Result.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertSuperAdmin() {
|
||||||
|
if (!categoryPermissionService.isCurrentSuperAdmin()) {
|
||||||
|
throw new BusinessException("仅超级管理员可配置分类权限");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -80,13 +80,13 @@ public class SysRoleController extends BaseCurdController<SysRoleService, SysRol
|
|||||||
*/
|
*/
|
||||||
@PostMapping("saveRole")
|
@PostMapping("saveRole")
|
||||||
@SaCheckPermission("/api/v1/sysRole/save")
|
@SaCheckPermission("/api/v1/sysRole/save")
|
||||||
public Result<Void> saveRole(@JsonBody SysRole entity) {
|
public Result<BigInteger> saveRole(@JsonBody SysRole entity) {
|
||||||
LoginAccount loginUser = SaTokenUtil.getLoginAccount();
|
LoginAccount loginUser = SaTokenUtil.getLoginAccount();
|
||||||
if (entity.getId() == null) {
|
if (entity.getId() == null) {
|
||||||
commonFiled(entity, loginUser.getId(), loginUser.getTenantId(), loginUser.getDeptId());
|
commonFiled(entity, loginUser.getId(), loginUser.getTenantId(), loginUser.getDeptId());
|
||||||
}
|
}
|
||||||
service.saveRole(entity);
|
service.saveRole(entity);
|
||||||
return Result.ok();
|
return Result.ok(entity.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -115,4 +115,4 @@ public class SysRoleController extends BaseCurdController<SysRoleService, SysRol
|
|||||||
}
|
}
|
||||||
return super.onRemoveBefore(ids);
|
return super.onRemoveBefore(ids);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package tech.easyflow.admin.dto.chatworkspace;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作台助手展示快照。
|
||||||
|
*/
|
||||||
|
public class ChatWorkspaceAssistantView implements Serializable {
|
||||||
|
|
||||||
|
private BigInteger id;
|
||||||
|
private String alias;
|
||||||
|
private String title;
|
||||||
|
private String description;
|
||||||
|
private String icon;
|
||||||
|
|
||||||
|
public BigInteger getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(BigInteger id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAlias() {
|
||||||
|
return alias;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAlias(String alias) {
|
||||||
|
this.alias = alias;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTitle() {
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTitle(String title) {
|
||||||
|
this.title = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDescription() {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDescription(String description) {
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getIcon() {
|
||||||
|
return icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIcon(String icon) {
|
||||||
|
this.icon = icon;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package tech.easyflow.admin.dto.chatworkspace;
|
||||||
|
|
||||||
|
import tech.easyflow.chatlog.domain.dto.ChatMessageRecord;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理端聊天工作台完整会话视图。
|
||||||
|
*/
|
||||||
|
public class ChatWorkspaceConversationView implements Serializable {
|
||||||
|
|
||||||
|
private long total;
|
||||||
|
private List<ChatMessageRecord> records = new ArrayList<>();
|
||||||
|
private Map<String, List<ChatMessageRecord>> variantsByRound = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前主线可见消息数量。
|
||||||
|
*
|
||||||
|
* @return 主线消息数量
|
||||||
|
*/
|
||||||
|
public long getTotal() {
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置当前主线可见消息数量。
|
||||||
|
*
|
||||||
|
* @param total 主线消息数量
|
||||||
|
*/
|
||||||
|
public void setTotal(long total) {
|
||||||
|
this.total = total;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前主线可见消息。
|
||||||
|
*
|
||||||
|
* @return 当前主线可见消息
|
||||||
|
*/
|
||||||
|
public List<ChatMessageRecord> getRecords() {
|
||||||
|
return records;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置当前主线可见消息。
|
||||||
|
*
|
||||||
|
* @param records 当前主线可见消息
|
||||||
|
*/
|
||||||
|
public void setRecords(List<ChatMessageRecord> records) {
|
||||||
|
this.records = records == null ? new ArrayList<>() : records;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取按轮次分组的全部答案版本。
|
||||||
|
*
|
||||||
|
* @return roundId 到答案版本列表的映射
|
||||||
|
*/
|
||||||
|
public Map<String, List<ChatMessageRecord>> getVariantsByRound() {
|
||||||
|
return variantsByRound;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置按轮次分组的全部答案版本。
|
||||||
|
*
|
||||||
|
* @param variantsByRound roundId 到答案版本列表的映射
|
||||||
|
*/
|
||||||
|
public void setVariantsByRound(Map<String, List<ChatMessageRecord>> variantsByRound) {
|
||||||
|
this.variantsByRound = variantsByRound == null ? new LinkedHashMap<>() : variantsByRound;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package tech.easyflow.admin.dto.chatworkspace;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作台知识库展示对象。
|
||||||
|
*/
|
||||||
|
public class ChatWorkspaceKnowledgeView implements Serializable {
|
||||||
|
|
||||||
|
private BigInteger id;
|
||||||
|
private String alias;
|
||||||
|
private String title;
|
||||||
|
private String description;
|
||||||
|
private String icon;
|
||||||
|
|
||||||
|
public BigInteger getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(BigInteger id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAlias() {
|
||||||
|
return alias;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAlias(String alias) {
|
||||||
|
this.alias = alias;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTitle() {
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTitle(String title) {
|
||||||
|
this.title = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDescription() {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDescription(String description) {
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getIcon() {
|
||||||
|
return icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIcon(String icon) {
|
||||||
|
this.icon = icon;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package tech.easyflow.admin.dto.chatworkspace;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理端聊天工作台只读原因。
|
||||||
|
*/
|
||||||
|
public enum ChatWorkspaceReadOnlyReason {
|
||||||
|
ASSISTANT_OFFLINE,
|
||||||
|
ASSISTANT_DELETED,
|
||||||
|
NO_PERMISSION
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package tech.easyflow.admin.dto.chatworkspace;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作台会话详情。
|
||||||
|
*/
|
||||||
|
public class ChatWorkspaceSessionDetailView extends ChatWorkspaceSessionView implements Serializable {
|
||||||
|
|
||||||
|
private ChatWorkspaceAssistantView assistant;
|
||||||
|
private List<ChatWorkspaceKnowledgeView> boundKnowledges = new ArrayList<>();
|
||||||
|
private List<ChatWorkspaceKnowledgeView> extraKnowledges = new ArrayList<>();
|
||||||
|
private List<String> removedExtraKnowledgeNames = new ArrayList<>();
|
||||||
|
|
||||||
|
public ChatWorkspaceAssistantView getAssistant() {
|
||||||
|
return assistant;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAssistant(ChatWorkspaceAssistantView assistant) {
|
||||||
|
this.assistant = assistant;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ChatWorkspaceKnowledgeView> getBoundKnowledges() {
|
||||||
|
return boundKnowledges;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBoundKnowledges(List<ChatWorkspaceKnowledgeView> boundKnowledges) {
|
||||||
|
this.boundKnowledges = boundKnowledges == null ? new ArrayList<>() : boundKnowledges;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ChatWorkspaceKnowledgeView> getExtraKnowledges() {
|
||||||
|
return extraKnowledges;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setExtraKnowledges(List<ChatWorkspaceKnowledgeView> extraKnowledges) {
|
||||||
|
this.extraKnowledges = extraKnowledges == null ? new ArrayList<>() : extraKnowledges;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getRemovedExtraKnowledgeNames() {
|
||||||
|
return removedExtraKnowledgeNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRemovedExtraKnowledgeNames(List<String> removedExtraKnowledgeNames) {
|
||||||
|
this.removedExtraKnowledgeNames = removedExtraKnowledgeNames == null ? new ArrayList<>() : removedExtraKnowledgeNames;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package tech.easyflow.admin.dto.chatworkspace;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作台会话分页结果。
|
||||||
|
*/
|
||||||
|
public class ChatWorkspaceSessionPage implements Serializable {
|
||||||
|
|
||||||
|
private Long total;
|
||||||
|
private Long pageNumber;
|
||||||
|
private Long pageSize;
|
||||||
|
private List<ChatWorkspaceSessionView> records = new ArrayList<>();
|
||||||
|
|
||||||
|
public Long getTotal() {
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTotal(Long total) {
|
||||||
|
this.total = total;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getPageNumber() {
|
||||||
|
return pageNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPageNumber(Long pageNumber) {
|
||||||
|
this.pageNumber = pageNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getPageSize() {
|
||||||
|
return pageSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPageSize(Long pageSize) {
|
||||||
|
this.pageSize = pageSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ChatWorkspaceSessionView> getRecords() {
|
||||||
|
return records;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRecords(List<ChatWorkspaceSessionView> records) {
|
||||||
|
this.records = records == null ? new ArrayList<>() : records;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
package tech.easyflow.admin.dto.chatworkspace;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作台会话摘要。
|
||||||
|
*/
|
||||||
|
public class ChatWorkspaceSessionView implements Serializable {
|
||||||
|
|
||||||
|
private BigInteger sessionId;
|
||||||
|
private BigInteger assistantId;
|
||||||
|
private String assistantCode;
|
||||||
|
private String assistantName;
|
||||||
|
private String title;
|
||||||
|
private String lastMessagePreview;
|
||||||
|
private Integer messageCount;
|
||||||
|
private Date accessAt;
|
||||||
|
private Date lastMessageAt;
|
||||||
|
private Boolean continuable;
|
||||||
|
private ChatWorkspaceReadOnlyReason readOnlyReason;
|
||||||
|
|
||||||
|
public BigInteger getSessionId() {
|
||||||
|
return sessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSessionId(BigInteger sessionId) {
|
||||||
|
this.sessionId = sessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigInteger getAssistantId() {
|
||||||
|
return assistantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAssistantId(BigInteger assistantId) {
|
||||||
|
this.assistantId = assistantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAssistantCode() {
|
||||||
|
return assistantCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAssistantCode(String assistantCode) {
|
||||||
|
this.assistantCode = assistantCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAssistantName() {
|
||||||
|
return assistantName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAssistantName(String assistantName) {
|
||||||
|
this.assistantName = assistantName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTitle() {
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTitle(String title) {
|
||||||
|
this.title = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLastMessagePreview() {
|
||||||
|
return lastMessagePreview;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLastMessagePreview(String lastMessagePreview) {
|
||||||
|
this.lastMessagePreview = lastMessagePreview;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getMessageCount() {
|
||||||
|
return messageCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMessageCount(Integer messageCount) {
|
||||||
|
this.messageCount = messageCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Date getAccessAt() {
|
||||||
|
return accessAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAccessAt(Date accessAt) {
|
||||||
|
this.accessAt = accessAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Date getLastMessageAt() {
|
||||||
|
return lastMessageAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLastMessageAt(Date lastMessageAt) {
|
||||||
|
this.lastMessageAt = lastMessageAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean getContinuable() {
|
||||||
|
return continuable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setContinuable(Boolean continuable) {
|
||||||
|
this.continuable = continuable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ChatWorkspaceReadOnlyReason getReadOnlyReason() {
|
||||||
|
return readOnlyReason;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setReadOnlyReason(ChatWorkspaceReadOnlyReason readOnlyReason) {
|
||||||
|
this.readOnlyReason = readOnlyReason;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package tech.easyflow.admin.model.ai;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 素材预览内容返回对象。
|
||||||
|
*/
|
||||||
|
public class ResourcePreviewVo {
|
||||||
|
|
||||||
|
private String content;
|
||||||
|
|
||||||
|
private boolean truncated;
|
||||||
|
|
||||||
|
public String getContent() {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setContent(String content) {
|
||||||
|
this.content = content;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isTruncated() {
|
||||||
|
return truncated;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTruncated(boolean truncated) {
|
||||||
|
this.truncated = truncated;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package tech.easyflow.admin.model.dashboard;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作台智能体趋势点位。
|
||||||
|
*/
|
||||||
|
public class DashboardAssistantTrendPointVo {
|
||||||
|
|
||||||
|
private String key;
|
||||||
|
|
||||||
|
private String label;
|
||||||
|
|
||||||
|
private Long sessionTotal;
|
||||||
|
|
||||||
|
public String getKey() {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setKey(String key) {
|
||||||
|
this.key = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLabel() {
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLabel(String label) {
|
||||||
|
this.label = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getSessionTotal() {
|
||||||
|
return sessionTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSessionTotal(Long sessionTotal) {
|
||||||
|
this.sessionTotal = sessionTotal;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package tech.easyflow.admin.model.dashboard;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作台智能体趋势序列。
|
||||||
|
*/
|
||||||
|
public class DashboardAssistantTrendSeriesVo {
|
||||||
|
|
||||||
|
private BigInteger assistantId;
|
||||||
|
|
||||||
|
private String label;
|
||||||
|
|
||||||
|
private Long totalSessionCount;
|
||||||
|
|
||||||
|
private List<DashboardAssistantTrendPointVo> points;
|
||||||
|
|
||||||
|
public BigInteger getAssistantId() {
|
||||||
|
return assistantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAssistantId(BigInteger assistantId) {
|
||||||
|
this.assistantId = assistantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLabel() {
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLabel(String label) {
|
||||||
|
this.label = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getTotalSessionCount() {
|
||||||
|
return totalSessionCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTotalSessionCount(Long totalSessionCount) {
|
||||||
|
this.totalSessionCount = totalSessionCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<DashboardAssistantTrendPointVo> getPoints() {
|
||||||
|
return points;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPoints(List<DashboardAssistantTrendPointVo> points) {
|
||||||
|
this.points = points;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package tech.easyflow.admin.model.dashboard;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 聊天统计可用状态。
|
||||||
|
*/
|
||||||
|
public class DashboardChatStatusVo {
|
||||||
|
|
||||||
|
private Boolean available;
|
||||||
|
|
||||||
|
private String message;
|
||||||
|
|
||||||
|
public Boolean getAvailable() {
|
||||||
|
return available;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAvailable(Boolean available) {
|
||||||
|
this.available = available;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMessage() {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMessage(String message) {
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
package tech.easyflow.admin.model.dashboard;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作台分布/排行项。
|
||||||
|
*/
|
||||||
|
public class DashboardDistributionItemVo {
|
||||||
|
|
||||||
|
private String key;
|
||||||
|
|
||||||
|
private String label;
|
||||||
|
|
||||||
|
private Long value;
|
||||||
|
|
||||||
|
private Long userTotal;
|
||||||
|
|
||||||
|
private Long activeUserTotal;
|
||||||
|
|
||||||
|
private Long botTotal;
|
||||||
|
|
||||||
|
private Long workflowTotal;
|
||||||
|
|
||||||
|
private Long knowledgeBaseTotal;
|
||||||
|
|
||||||
|
private BigInteger assistantId;
|
||||||
|
|
||||||
|
private Long messageTotal;
|
||||||
|
|
||||||
|
private Long sessionTotal;
|
||||||
|
|
||||||
|
private Double avgSessionPerUser;
|
||||||
|
|
||||||
|
private Double avgMessagePerSession;
|
||||||
|
|
||||||
|
public String getKey() {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setKey(String key) {
|
||||||
|
this.key = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLabel() {
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLabel(String label) {
|
||||||
|
this.label = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getValue() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setValue(Long value) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getUserTotal() {
|
||||||
|
return userTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUserTotal(Long userTotal) {
|
||||||
|
this.userTotal = userTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getActiveUserTotal() {
|
||||||
|
return activeUserTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setActiveUserTotal(Long activeUserTotal) {
|
||||||
|
this.activeUserTotal = activeUserTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getBotTotal() {
|
||||||
|
return botTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBotTotal(Long botTotal) {
|
||||||
|
this.botTotal = botTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getWorkflowTotal() {
|
||||||
|
return workflowTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setWorkflowTotal(Long workflowTotal) {
|
||||||
|
this.workflowTotal = workflowTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getKnowledgeBaseTotal() {
|
||||||
|
return knowledgeBaseTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setKnowledgeBaseTotal(Long knowledgeBaseTotal) {
|
||||||
|
this.knowledgeBaseTotal = knowledgeBaseTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigInteger getAssistantId() {
|
||||||
|
return assistantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAssistantId(BigInteger assistantId) {
|
||||||
|
this.assistantId = assistantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getMessageTotal() {
|
||||||
|
return messageTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMessageTotal(Long messageTotal) {
|
||||||
|
this.messageTotal = messageTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getSessionTotal() {
|
||||||
|
return sessionTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSessionTotal(Long sessionTotal) {
|
||||||
|
this.sessionTotal = sessionTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Double getAvgSessionPerUser() {
|
||||||
|
return avgSessionPerUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAvgSessionPerUser(Double avgSessionPerUser) {
|
||||||
|
this.avgSessionPerUser = avgSessionPerUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Double getAvgMessagePerSession() {
|
||||||
|
return avgMessagePerSession;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAvgMessagePerSession(Double avgMessagePerSession) {
|
||||||
|
this.avgMessagePerSession = avgMessagePerSession;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package tech.easyflow.admin.model.dashboard;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作台统计查询参数。
|
||||||
|
*/
|
||||||
|
public class DashboardOverviewQuery {
|
||||||
|
|
||||||
|
private String range;
|
||||||
|
private String startDate;
|
||||||
|
private String endDate;
|
||||||
|
|
||||||
|
public String getRange() {
|
||||||
|
return range;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRange(String range) {
|
||||||
|
this.range = range;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getStartDate() {
|
||||||
|
return startDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStartDate(String startDate) {
|
||||||
|
this.startDate = startDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getEndDate() {
|
||||||
|
return endDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEndDate(String endDate) {
|
||||||
|
this.endDate = endDate;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package tech.easyflow.admin.model.dashboard;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作台总览返回对象。
|
||||||
|
*/
|
||||||
|
public class DashboardOverviewVo {
|
||||||
|
|
||||||
|
private DashboardSummaryVo summary;
|
||||||
|
|
||||||
|
private DashboardChatStatusVo chatStatus;
|
||||||
|
|
||||||
|
private List<DashboardTrendItemVo> trends;
|
||||||
|
|
||||||
|
private List<DashboardAssistantTrendSeriesVo> assistantTrends;
|
||||||
|
|
||||||
|
private List<DashboardDistributionItemVo> distribution;
|
||||||
|
|
||||||
|
private DashboardOverviewQuery query;
|
||||||
|
|
||||||
|
private Date updatedAt;
|
||||||
|
|
||||||
|
public DashboardSummaryVo getSummary() {
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSummary(DashboardSummaryVo summary) {
|
||||||
|
this.summary = summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<DashboardTrendItemVo> getTrends() {
|
||||||
|
return trends;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTrends(List<DashboardTrendItemVo> trends) {
|
||||||
|
this.trends = trends;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DashboardChatStatusVo getChatStatus() {
|
||||||
|
return chatStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setChatStatus(DashboardChatStatusVo chatStatus) {
|
||||||
|
this.chatStatus = chatStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<DashboardDistributionItemVo> getDistribution() {
|
||||||
|
return distribution;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDistribution(List<DashboardDistributionItemVo> distribution) {
|
||||||
|
this.distribution = distribution;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<DashboardAssistantTrendSeriesVo> getAssistantTrends() {
|
||||||
|
return assistantTrends;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAssistantTrends(List<DashboardAssistantTrendSeriesVo> assistantTrends) {
|
||||||
|
this.assistantTrends = assistantTrends;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DashboardOverviewQuery getQuery() {
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setQuery(DashboardOverviewQuery query) {
|
||||||
|
this.query = query;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Date getUpdatedAt() {
|
||||||
|
return updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUpdatedAt(Date updatedAt) {
|
||||||
|
this.updatedAt = updatedAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package tech.easyflow.admin.model.dashboard;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作台汇总指标。
|
||||||
|
*/
|
||||||
|
public class DashboardSummaryVo {
|
||||||
|
|
||||||
|
private Long userTotal;
|
||||||
|
|
||||||
|
private Long activeUserTotal;
|
||||||
|
|
||||||
|
private Long botTotal;
|
||||||
|
|
||||||
|
private Long workflowTotal;
|
||||||
|
|
||||||
|
private Long knowledgeBaseTotal;
|
||||||
|
|
||||||
|
private Long chatMessageTotal;
|
||||||
|
|
||||||
|
private Long chatSessionTotal;
|
||||||
|
|
||||||
|
private Long activeAssistantTotal;
|
||||||
|
|
||||||
|
private Long chatActiveUserTotal;
|
||||||
|
|
||||||
|
public Long getUserTotal() {
|
||||||
|
return userTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUserTotal(Long userTotal) {
|
||||||
|
this.userTotal = userTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getActiveUserTotal() {
|
||||||
|
return activeUserTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setActiveUserTotal(Long activeUserTotal) {
|
||||||
|
this.activeUserTotal = activeUserTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getBotTotal() {
|
||||||
|
return botTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBotTotal(Long botTotal) {
|
||||||
|
this.botTotal = botTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getWorkflowTotal() {
|
||||||
|
return workflowTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setWorkflowTotal(Long workflowTotal) {
|
||||||
|
this.workflowTotal = workflowTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getKnowledgeBaseTotal() {
|
||||||
|
return knowledgeBaseTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setKnowledgeBaseTotal(Long knowledgeBaseTotal) {
|
||||||
|
this.knowledgeBaseTotal = knowledgeBaseTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getChatMessageTotal() {
|
||||||
|
return chatMessageTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setChatMessageTotal(Long chatMessageTotal) {
|
||||||
|
this.chatMessageTotal = chatMessageTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getChatSessionTotal() {
|
||||||
|
return chatSessionTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setChatSessionTotal(Long chatSessionTotal) {
|
||||||
|
this.chatSessionTotal = chatSessionTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getActiveAssistantTotal() {
|
||||||
|
return activeAssistantTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setActiveAssistantTotal(Long activeAssistantTotal) {
|
||||||
|
this.activeAssistantTotal = activeAssistantTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getChatActiveUserTotal() {
|
||||||
|
return chatActiveUserTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setChatActiveUserTotal(Long chatActiveUserTotal) {
|
||||||
|
this.chatActiveUserTotal = chatActiveUserTotal;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package tech.easyflow.admin.model.dashboard;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作台趋势项。
|
||||||
|
*/
|
||||||
|
public class DashboardTrendItemVo {
|
||||||
|
|
||||||
|
private String key;
|
||||||
|
|
||||||
|
private String label;
|
||||||
|
|
||||||
|
private Long activeUserTotal;
|
||||||
|
|
||||||
|
private Long chatMessageTotal;
|
||||||
|
|
||||||
|
private Long chatSessionTotal;
|
||||||
|
|
||||||
|
public String getKey() {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setKey(String key) {
|
||||||
|
this.key = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLabel() {
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLabel(String label) {
|
||||||
|
this.label = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getActiveUserTotal() {
|
||||||
|
return activeUserTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setActiveUserTotal(Long activeUserTotal) {
|
||||||
|
this.activeUserTotal = activeUserTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getChatMessageTotal() {
|
||||||
|
return chatMessageTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setChatMessageTotal(Long chatMessageTotal) {
|
||||||
|
this.chatMessageTotal = chatMessageTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getChatSessionTotal() {
|
||||||
|
return chatSessionTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setChatSessionTotal(Long chatSessionTotal) {
|
||||||
|
this.chatSessionTotal = chatSessionTotal;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package tech.easyflow.admin.model.dashboard;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作台用户活跃排行项。
|
||||||
|
*/
|
||||||
|
public class DashboardUserRankItemVo {
|
||||||
|
|
||||||
|
private BigInteger userId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最终展示名称。
|
||||||
|
*/
|
||||||
|
private String label;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录账号。
|
||||||
|
*/
|
||||||
|
private String loginName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 昵称。
|
||||||
|
*/
|
||||||
|
private String nickname;
|
||||||
|
|
||||||
|
private Long sessionTotal;
|
||||||
|
|
||||||
|
private Long messageTotal;
|
||||||
|
|
||||||
|
public BigInteger getUserId() {
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUserId(BigInteger userId) {
|
||||||
|
this.userId = userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLabel() {
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLabel(String label) {
|
||||||
|
this.label = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLoginName() {
|
||||||
|
return loginName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLoginName(String loginName) {
|
||||||
|
this.loginName = loginName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getNickname() {
|
||||||
|
return nickname;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setNickname(String nickname) {
|
||||||
|
this.nickname = nickname;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getSessionTotal() {
|
||||||
|
return sessionTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSessionTotal(Long sessionTotal) {
|
||||||
|
this.sessionTotal = sessionTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getMessageTotal() {
|
||||||
|
return messageTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMessageTotal(Long messageTotal) {
|
||||||
|
this.messageTotal = messageTotal;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package tech.easyflow.admin.model.dashboard;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户活跃榜查询参数。
|
||||||
|
*/
|
||||||
|
public class DashboardUserRankQuery {
|
||||||
|
|
||||||
|
private String range;
|
||||||
|
private String startDate;
|
||||||
|
private String endDate;
|
||||||
|
private BigInteger assistantId;
|
||||||
|
|
||||||
|
public String getRange() {
|
||||||
|
return range;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRange(String range) {
|
||||||
|
this.range = range;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getStartDate() {
|
||||||
|
return startDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStartDate(String startDate) {
|
||||||
|
this.startDate = startDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getEndDate() {
|
||||||
|
return endDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEndDate(String endDate) {
|
||||||
|
this.endDate = endDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigInteger getAssistantId() {
|
||||||
|
return assistantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAssistantId(BigInteger assistantId) {
|
||||||
|
this.assistantId = assistantId;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,420 @@
|
|||||||
|
package tech.easyflow.admin.service.agent;
|
||||||
|
|
||||||
|
import com.mybatisflex.core.query.QueryWrapper;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
import tech.easyflow.admin.dto.chatworkspace.*;
|
||||||
|
import tech.easyflow.agent.entity.Agent;
|
||||||
|
import tech.easyflow.agent.runtime.AgentRuntimeStateCleanupService;
|
||||||
|
import tech.easyflow.agent.service.AgentService;
|
||||||
|
import tech.easyflow.ai.entity.DocumentCollection;
|
||||||
|
import tech.easyflow.ai.enums.PublishStatus;
|
||||||
|
import tech.easyflow.ai.service.DocumentCollectionService;
|
||||||
|
import tech.easyflow.chatlog.domain.command.ChatSessionUpsertCommand;
|
||||||
|
import tech.easyflow.chatlog.domain.dto.*;
|
||||||
|
import tech.easyflow.chatlog.domain.query.ChatPageQuery;
|
||||||
|
import tech.easyflow.chatlog.service.ChatSessionCommandService;
|
||||||
|
import tech.easyflow.chatlog.service.ChatSessionQueryService;
|
||||||
|
import tech.easyflow.chatlog.support.ChatJsonSupport;
|
||||||
|
import tech.easyflow.common.entity.LoginAccount;
|
||||||
|
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||||
|
import tech.easyflow.system.enums.CategoryResourceType;
|
||||||
|
import tech.easyflow.system.enums.ResourceAction;
|
||||||
|
import tech.easyflow.system.service.ResourceAccessService;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Agent 管理端会话服务。
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class AgentSessionService {
|
||||||
|
|
||||||
|
private static final String ASSISTANT_CODE = "AGENT";
|
||||||
|
|
||||||
|
private final ChatSessionQueryService chatSessionQueryService;
|
||||||
|
private final ChatSessionCommandService chatSessionCommandService;
|
||||||
|
private final AgentService agentService;
|
||||||
|
private final DocumentCollectionService documentCollectionService;
|
||||||
|
private final ResourceAccessService resourceAccessService;
|
||||||
|
private final AgentRuntimeStateCleanupService agentRuntimeStateCleanupService;
|
||||||
|
private final ChatJsonSupport chatJsonSupport;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建 Agent 管理端会话服务。
|
||||||
|
*
|
||||||
|
* @param chatSessionQueryService 聊天会话查询服务
|
||||||
|
* @param chatSessionCommandService 聊天会话命令服务
|
||||||
|
* @param agentService Agent 服务
|
||||||
|
* @param documentCollectionService 知识库服务
|
||||||
|
* @param resourceAccessService 资源访问服务
|
||||||
|
* @param agentRuntimeStateCleanupService Agent 运行态清理服务
|
||||||
|
* @param chatJsonSupport 聊天 JSON 工具
|
||||||
|
*/
|
||||||
|
public AgentSessionService(ChatSessionQueryService chatSessionQueryService,
|
||||||
|
ChatSessionCommandService chatSessionCommandService,
|
||||||
|
AgentService agentService,
|
||||||
|
DocumentCollectionService documentCollectionService,
|
||||||
|
ResourceAccessService resourceAccessService,
|
||||||
|
AgentRuntimeStateCleanupService agentRuntimeStateCleanupService,
|
||||||
|
ChatJsonSupport chatJsonSupport) {
|
||||||
|
this.chatSessionQueryService = chatSessionQueryService;
|
||||||
|
this.chatSessionCommandService = chatSessionCommandService;
|
||||||
|
this.agentService = agentService;
|
||||||
|
this.documentCollectionService = documentCollectionService;
|
||||||
|
this.resourceAccessService = resourceAccessService;
|
||||||
|
this.agentRuntimeStateCleanupService = agentRuntimeStateCleanupService;
|
||||||
|
this.chatJsonSupport = chatJsonSupport;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询当前用户的 Agent 会话分页。
|
||||||
|
*
|
||||||
|
* @param account 当前登录账号
|
||||||
|
* @param agentId Agent ID
|
||||||
|
* @param query 分页参数
|
||||||
|
* @return Agent 会话分页
|
||||||
|
*/
|
||||||
|
public ChatWorkspaceSessionPage queryCurrentUserSessions(LoginAccount account, BigInteger agentId, ChatPageQuery query) {
|
||||||
|
ChatSessionPage page = chatSessionQueryService.pageSessions(account.getId(), agentId, ASSISTANT_CODE, query);
|
||||||
|
Map<BigInteger, AgentAvailability> availabilityMap = resolveAgentAvailability(page.getRecords());
|
||||||
|
ChatWorkspaceSessionPage result = new ChatWorkspaceSessionPage();
|
||||||
|
result.setTotal(page.getTotal());
|
||||||
|
result.setPageNumber(page.getPageNumber());
|
||||||
|
result.setPageSize(page.getPageSize());
|
||||||
|
List<ChatWorkspaceSessionView> records = new ArrayList<>();
|
||||||
|
for (ChatSessionSummary summary : page.getRecords()) {
|
||||||
|
records.add(toSessionView(summary, availabilityMap.get(summary.getAssistantId())));
|
||||||
|
}
|
||||||
|
result.setRecords(records);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询当前用户的 Agent 会话详情。
|
||||||
|
*
|
||||||
|
* @param account 当前登录账号
|
||||||
|
* @param sessionId 会话 ID
|
||||||
|
* @return Agent 会话详情
|
||||||
|
*/
|
||||||
|
public ChatWorkspaceSessionDetailView getCurrentUserSession(LoginAccount account, BigInteger sessionId) {
|
||||||
|
ChatSessionSummary summary = requireUserAgentSession(account, sessionId);
|
||||||
|
AgentAvailability availability = resolveAgentAvailability(List.of(summary)).get(summary.getAssistantId());
|
||||||
|
ChatWorkspaceSessionDetailView detail = new ChatWorkspaceSessionDetailView();
|
||||||
|
fillSessionView(detail, summary, availability);
|
||||||
|
Agent displayAgent = availability == null ? null : availability.displayAgent();
|
||||||
|
detail.setAssistant(toAssistantView(displayAgent, summary));
|
||||||
|
detail.setBoundKnowledges(resolveBoundKnowledges(displayAgent));
|
||||||
|
ExtraKnowledgeResolution extraKnowledgeResolution = resolveExtraKnowledges(summary);
|
||||||
|
detail.setExtraKnowledges(extraKnowledgeResolution.validKnowledges());
|
||||||
|
detail.setRemovedExtraKnowledgeNames(extraKnowledgeResolution.removedNames());
|
||||||
|
if (extraKnowledgeResolution.shouldSync()) {
|
||||||
|
syncSessionExtraKnowledges(summary, extraKnowledgeResolution.validKnowledgeIds(), account.getId());
|
||||||
|
}
|
||||||
|
return detail;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询当前用户的 Agent 会话消息。
|
||||||
|
*
|
||||||
|
* @param account 当前登录账号
|
||||||
|
* @param sessionId 会话 ID
|
||||||
|
* @param query 分页参数
|
||||||
|
* @return 消息分页
|
||||||
|
*/
|
||||||
|
public ChatHistoryPage queryCurrentUserMessages(LoginAccount account, BigInteger sessionId, ChatPageQuery query) {
|
||||||
|
requireUserAgentSession(account, sessionId);
|
||||||
|
return chatSessionQueryService.pageMainlineMessages(sessionId, query);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询当前用户的 Agent 完整会话。
|
||||||
|
*
|
||||||
|
* @param account 当前登录账号
|
||||||
|
* @param sessionId 会话 ID
|
||||||
|
* @return 完整会话
|
||||||
|
*/
|
||||||
|
public ChatWorkspaceConversationView getCurrentUserConversation(LoginAccount account, BigInteger sessionId) {
|
||||||
|
requireUserAgentSession(account, sessionId);
|
||||||
|
List<ChatMessageRecord> records = chatSessionQueryService.listMainlineMessages(sessionId);
|
||||||
|
ChatWorkspaceConversationView view = new ChatWorkspaceConversationView();
|
||||||
|
view.setRecords(records);
|
||||||
|
view.setTotal(records.size());
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重命名当前用户的 Agent 会话。
|
||||||
|
*
|
||||||
|
* @param account 当前登录账号
|
||||||
|
* @param sessionId 会话 ID
|
||||||
|
* @param title 新标题
|
||||||
|
*/
|
||||||
|
public void renameCurrentUserSession(LoginAccount account, BigInteger sessionId, String title) {
|
||||||
|
if (!StringUtils.hasText(title)) {
|
||||||
|
throw new BusinessException("标题不能为空");
|
||||||
|
}
|
||||||
|
requireUserAgentSession(account, sessionId);
|
||||||
|
chatSessionCommandService.renameSession(sessionId, account.getId(), title.trim(), account.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存当前用户 Agent 会话的临时知识库。
|
||||||
|
*
|
||||||
|
* @param account 当前登录账号
|
||||||
|
* @param sessionId 会话 ID
|
||||||
|
* @param knowledgeIds 临时知识库 ID
|
||||||
|
* @return 更新后的会话详情
|
||||||
|
*/
|
||||||
|
public ChatWorkspaceSessionDetailView saveCurrentUserExtraKnowledges(LoginAccount account,
|
||||||
|
BigInteger sessionId,
|
||||||
|
List<BigInteger> knowledgeIds) {
|
||||||
|
ChatSessionSummary summary = requireUserAgentSession(account, sessionId);
|
||||||
|
ExtraKnowledgeResolution resolution = resolveVisibleKnowledgeViews(normalizeExtraKnowledgeIds(knowledgeIds));
|
||||||
|
if (!resolution.removedNames().isEmpty()) {
|
||||||
|
throw new BusinessException("所选知识库已失效或无权限使用");
|
||||||
|
}
|
||||||
|
syncSessionExtraKnowledges(summary, resolution.validKnowledgeIds(), account.getId());
|
||||||
|
return getCurrentUserSession(account, sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除当前用户的 Agent 会话。
|
||||||
|
*
|
||||||
|
* @param account 当前登录账号
|
||||||
|
* @param sessionId 会话 ID
|
||||||
|
*/
|
||||||
|
public void deleteCurrentUserSession(LoginAccount account, BigInteger sessionId) {
|
||||||
|
requireUserAgentSession(account, sessionId);
|
||||||
|
agentRuntimeStateCleanupService.clearChatSession(sessionId, account.getId());
|
||||||
|
chatSessionCommandService.deleteSession(sessionId, account.getId(), account.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
private ChatSessionSummary requireUserAgentSession(LoginAccount account, BigInteger sessionId) {
|
||||||
|
ChatSessionSummary summary = chatSessionQueryService.getSessionSummary(sessionId);
|
||||||
|
if (summary == null || Integer.valueOf(1).equals(summary.getIsDeleted())
|
||||||
|
|| !ASSISTANT_CODE.equals(summary.getAssistantCode())) {
|
||||||
|
throw new BusinessException("Agent 会话不存在");
|
||||||
|
}
|
||||||
|
if (!Objects.equals(summary.getUserId(), account.getId())) {
|
||||||
|
throw new BusinessException("无权访问该 Agent 会话");
|
||||||
|
}
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<BigInteger, AgentAvailability> resolveAgentAvailability(List<ChatSessionSummary> sessions) {
|
||||||
|
Map<BigInteger, AgentAvailability> result = new LinkedHashMap<>();
|
||||||
|
if (sessions == null || sessions.isEmpty()) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
Set<BigInteger> agentIds = new LinkedHashSet<>();
|
||||||
|
for (ChatSessionSummary session : sessions) {
|
||||||
|
if (session != null && session.getAssistantId() != null) {
|
||||||
|
agentIds.add(session.getAssistantId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (agentIds.isEmpty()) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
List<Agent> agents = agentService.list(QueryWrapper.create().in("id", agentIds));
|
||||||
|
Map<BigInteger, Agent> agentMap = new LinkedHashMap<>();
|
||||||
|
for (Agent agent : agents) {
|
||||||
|
agentMap.put(agent.getId(), agent);
|
||||||
|
}
|
||||||
|
for (BigInteger agentId : agentIds) {
|
||||||
|
Agent currentAgent = agentMap.get(agentId);
|
||||||
|
if (currentAgent == null) {
|
||||||
|
result.put(agentId, new AgentAvailability(false, ChatWorkspaceReadOnlyReason.ASSISTANT_DELETED, null));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!resourceAccessService.canAccess(CategoryResourceType.AGENT, currentAgent, ResourceAction.USE)) {
|
||||||
|
result.put(agentId, new AgentAvailability(false, ChatWorkspaceReadOnlyReason.NO_PERMISSION, null));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
boolean online = Integer.valueOf(1).equals(currentAgent.getStatus())
|
||||||
|
&& PublishStatus.from(currentAgent.getPublishStatus()) == PublishStatus.PUBLISHED;
|
||||||
|
result.put(agentId, new AgentAvailability(
|
||||||
|
online,
|
||||||
|
online ? null : ChatWorkspaceReadOnlyReason.ASSISTANT_OFFLINE,
|
||||||
|
toDisplayAgent(currentAgent)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Agent toDisplayAgent(Agent currentAgent) {
|
||||||
|
if (currentAgent.getPublishedSnapshotJson() != null && !currentAgent.getPublishedSnapshotJson().isEmpty()) {
|
||||||
|
return agentService.fromSnapshot(currentAgent.getPublishedSnapshotJson());
|
||||||
|
}
|
||||||
|
return currentAgent;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ChatWorkspaceSessionView toSessionView(ChatSessionSummary summary, AgentAvailability availability) {
|
||||||
|
ChatWorkspaceSessionView view = new ChatWorkspaceSessionView();
|
||||||
|
fillSessionView(view, summary, availability);
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fillSessionView(ChatWorkspaceSessionView view, ChatSessionSummary summary, AgentAvailability availability) {
|
||||||
|
view.setSessionId(summary.getId());
|
||||||
|
view.setAssistantId(summary.getAssistantId());
|
||||||
|
view.setAssistantCode(summary.getAssistantCode());
|
||||||
|
view.setAssistantName(summary.getAssistantName());
|
||||||
|
view.setTitle(summary.getTitle());
|
||||||
|
view.setLastMessagePreview(summary.getLastMessagePreview());
|
||||||
|
view.setMessageCount(summary.getMessageCount());
|
||||||
|
view.setAccessAt(summary.getAccessAt());
|
||||||
|
view.setLastMessageAt(summary.getLastMessageAt());
|
||||||
|
view.setContinuable(availability != null && availability.continuable());
|
||||||
|
view.setReadOnlyReason(availability == null ? ChatWorkspaceReadOnlyReason.ASSISTANT_DELETED : availability.reason());
|
||||||
|
}
|
||||||
|
|
||||||
|
private ChatWorkspaceAssistantView toAssistantView(Agent agent, ChatSessionSummary summary) {
|
||||||
|
ChatWorkspaceAssistantView view = new ChatWorkspaceAssistantView();
|
||||||
|
if (agent != null) {
|
||||||
|
view.setId(agent.getId());
|
||||||
|
view.setAlias(agent.getId() == null ? null : agent.getId().toString());
|
||||||
|
view.setTitle(agent.getName());
|
||||||
|
view.setDescription(agent.getDescription());
|
||||||
|
view.setIcon(agent.getAvatar());
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
view.setId(summary == null ? null : summary.getAssistantId());
|
||||||
|
view.setAlias(summary == null ? null : summary.getAssistantCode());
|
||||||
|
view.setTitle(summary == null ? null : summary.getAssistantName());
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private List<ChatWorkspaceKnowledgeView> resolveBoundKnowledges(Agent displayAgent) {
|
||||||
|
if (displayAgent == null || displayAgent.getKnowledgeBindings() == null || displayAgent.getKnowledgeBindings().isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
List<BigInteger> knowledgeIds = displayAgent.getKnowledgeBindings().stream()
|
||||||
|
.map(binding -> binding.getKnowledgeId())
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.toList();
|
||||||
|
if (knowledgeIds.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
List<DocumentCollection> collections = documentCollectionService.listByIds(knowledgeIds);
|
||||||
|
Map<BigInteger, DocumentCollection> collectionMap = new LinkedHashMap<>();
|
||||||
|
for (DocumentCollection collection : collections) {
|
||||||
|
collectionMap.put(collection.getId(), collection);
|
||||||
|
}
|
||||||
|
List<ChatWorkspaceKnowledgeView> views = new ArrayList<>();
|
||||||
|
for (BigInteger knowledgeId : knowledgeIds) {
|
||||||
|
DocumentCollection collection = collectionMap.get(knowledgeId);
|
||||||
|
if (collection == null || PublishStatus.from(collection.getPublishStatus()) != PublishStatus.PUBLISHED) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
views.add(toKnowledgeView(documentCollectionService.toPublishedView(collection)));
|
||||||
|
}
|
||||||
|
return views;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ChatWorkspaceKnowledgeView toKnowledgeView(DocumentCollection collection) {
|
||||||
|
ChatWorkspaceKnowledgeView view = new ChatWorkspaceKnowledgeView();
|
||||||
|
view.setId(collection.getId());
|
||||||
|
view.setAlias(collection.getAlias());
|
||||||
|
view.setTitle(collection.getTitle());
|
||||||
|
view.setDescription(collection.getDescription());
|
||||||
|
view.setIcon(collection.getIcon());
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ExtraKnowledgeResolution resolveExtraKnowledges(ChatSessionSummary summary) {
|
||||||
|
ChatSessionExtPayload payload = chatJsonSupport.fromJson(summary.getExtJson(), ChatSessionExtPayload.class);
|
||||||
|
List<BigInteger> extraKnowledgeIds = payload == null ? List.of() : payload.getExtraKnowledgeIds();
|
||||||
|
return resolveVisibleKnowledgeViews(extraKnowledgeIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ExtraKnowledgeResolution resolveVisibleKnowledgeViews(List<BigInteger> knowledgeIds) {
|
||||||
|
if (knowledgeIds == null || knowledgeIds.isEmpty()) {
|
||||||
|
return new ExtraKnowledgeResolution(List.of(), List.of(), List.of(), false);
|
||||||
|
}
|
||||||
|
List<BigInteger> normalizedIds = normalizeExtraKnowledgeIds(knowledgeIds);
|
||||||
|
if (normalizedIds.isEmpty()) {
|
||||||
|
return new ExtraKnowledgeResolution(List.of(), List.of(), List.of(), false);
|
||||||
|
}
|
||||||
|
List<DocumentCollection> collections = documentCollectionService.listByIds(normalizedIds);
|
||||||
|
Map<BigInteger, DocumentCollection> collectionMap = new LinkedHashMap<>();
|
||||||
|
for (DocumentCollection collection : collections) {
|
||||||
|
collectionMap.put(collection.getId(), collection);
|
||||||
|
}
|
||||||
|
List<ChatWorkspaceKnowledgeView> validKnowledges = new ArrayList<>();
|
||||||
|
List<BigInteger> validKnowledgeIds = new ArrayList<>();
|
||||||
|
List<String> removedNames = new ArrayList<>();
|
||||||
|
boolean changed = false;
|
||||||
|
for (BigInteger knowledgeId : normalizedIds) {
|
||||||
|
DocumentCollection current = collectionMap.get(knowledgeId);
|
||||||
|
if (current == null) {
|
||||||
|
removedNames.add("知识库#" + knowledgeId);
|
||||||
|
changed = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (PublishStatus.from(current.getPublishStatus()) != PublishStatus.PUBLISHED) {
|
||||||
|
removedNames.add(current.getTitle());
|
||||||
|
changed = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!resourceAccessService.canAccess(CategoryResourceType.KNOWLEDGE, current, ResourceAction.USE)) {
|
||||||
|
removedNames.add(current.getTitle());
|
||||||
|
changed = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
validKnowledges.add(toKnowledgeView(documentCollectionService.toPublishedView(current)));
|
||||||
|
validKnowledgeIds.add(current.getId());
|
||||||
|
}
|
||||||
|
if (!Objects.equals(normalizedIds, validKnowledgeIds)) {
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
return new ExtraKnowledgeResolution(validKnowledges, validKnowledgeIds, removedNames, changed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<BigInteger> normalizeExtraKnowledgeIds(List<BigInteger> knowledgeIds) {
|
||||||
|
if (knowledgeIds == null || knowledgeIds.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
List<BigInteger> normalizedIds = new ArrayList<>();
|
||||||
|
for (BigInteger knowledgeId : knowledgeIds) {
|
||||||
|
if (knowledgeId != null && !normalizedIds.contains(knowledgeId)) {
|
||||||
|
normalizedIds.add(knowledgeId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (normalizedIds.size() > 3) {
|
||||||
|
throw new BusinessException("临时知识库最多选择 3 个");
|
||||||
|
}
|
||||||
|
return normalizedIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void syncSessionExtraKnowledges(ChatSessionSummary summary, List<BigInteger> validKnowledgeIds, BigInteger operatorId) {
|
||||||
|
ChatSessionExtPayload payload = new ChatSessionExtPayload();
|
||||||
|
payload.setExtraKnowledgeIds(validKnowledgeIds);
|
||||||
|
ChatSessionUpsertCommand command = new ChatSessionUpsertCommand();
|
||||||
|
command.setSessionId(summary.getId());
|
||||||
|
command.setTenantId(summary.getTenantId());
|
||||||
|
command.setDeptId(summary.getDeptId());
|
||||||
|
command.setUserId(summary.getUserId());
|
||||||
|
command.setUserAccount(summary.getUserAccount());
|
||||||
|
command.setAssistantId(summary.getAssistantId());
|
||||||
|
command.setAssistantCode(summary.getAssistantCode());
|
||||||
|
command.setAssistantName(summary.getAssistantName());
|
||||||
|
command.setTitle(summary.getTitle());
|
||||||
|
command.setExtJson(chatJsonSupport.toJson(payload));
|
||||||
|
command.setOperatorId(operatorId);
|
||||||
|
chatSessionCommandService.createOrTouchSession(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
private record AgentAvailability(boolean continuable,
|
||||||
|
ChatWorkspaceReadOnlyReason reason,
|
||||||
|
Agent displayAgent) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private record ExtraKnowledgeResolution(List<ChatWorkspaceKnowledgeView> validKnowledges,
|
||||||
|
List<BigInteger> validKnowledgeIds,
|
||||||
|
List<String> removedNames,
|
||||||
|
boolean shouldSync) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,515 @@
|
|||||||
|
package tech.easyflow.admin.service.ai;
|
||||||
|
|
||||||
|
import com.mybatisflex.core.query.QueryWrapper;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
import tech.easyflow.admin.dto.chatworkspace.ChatWorkspaceAssistantView;
|
||||||
|
import tech.easyflow.admin.dto.chatworkspace.ChatWorkspaceConversationView;
|
||||||
|
import tech.easyflow.admin.dto.chatworkspace.ChatWorkspaceKnowledgeView;
|
||||||
|
import tech.easyflow.admin.dto.chatworkspace.ChatWorkspaceReadOnlyReason;
|
||||||
|
import tech.easyflow.admin.dto.chatworkspace.ChatWorkspaceSessionDetailView;
|
||||||
|
import tech.easyflow.admin.dto.chatworkspace.ChatWorkspaceSessionPage;
|
||||||
|
import tech.easyflow.admin.dto.chatworkspace.ChatWorkspaceSessionView;
|
||||||
|
import tech.easyflow.ai.entity.Bot;
|
||||||
|
import tech.easyflow.ai.entity.DocumentCollection;
|
||||||
|
import tech.easyflow.ai.enums.PublishStatus;
|
||||||
|
import tech.easyflow.ai.permission.KnowledgeReadAccessSnapshot;
|
||||||
|
import tech.easyflow.ai.permission.KnowledgeVisibilityQueryHelper;
|
||||||
|
import tech.easyflow.ai.service.BotService;
|
||||||
|
import tech.easyflow.ai.service.DocumentCollectionService;
|
||||||
|
import tech.easyflow.chatlog.domain.command.ChatSessionUpsertCommand;
|
||||||
|
import tech.easyflow.chatlog.domain.dto.ChatHistoryPage;
|
||||||
|
import tech.easyflow.chatlog.domain.dto.ChatMessageRecord;
|
||||||
|
import tech.easyflow.chatlog.domain.dto.ChatSessionExtPayload;
|
||||||
|
import tech.easyflow.chatlog.domain.dto.ChatSessionPage;
|
||||||
|
import tech.easyflow.chatlog.domain.dto.ChatSessionSummary;
|
||||||
|
import tech.easyflow.chatlog.domain.query.ChatPageQuery;
|
||||||
|
import tech.easyflow.chatlog.service.ChatRoundOperateService;
|
||||||
|
import tech.easyflow.chatlog.service.ChatSessionCommandService;
|
||||||
|
import tech.easyflow.chatlog.service.ChatSessionQueryService;
|
||||||
|
import tech.easyflow.chatlog.support.ChatJsonSupport;
|
||||||
|
import tech.easyflow.common.entity.LoginAccount;
|
||||||
|
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||||
|
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
|
||||||
|
import tech.easyflow.system.service.CategoryPermissionService;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static tech.easyflow.ai.entity.table.BotTableDef.BOT;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理端聊天工作台服务。
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class ChatWorkspaceService {
|
||||||
|
|
||||||
|
private final ChatSessionQueryService chatSessionQueryService;
|
||||||
|
private final ChatSessionCommandService chatSessionCommandService;
|
||||||
|
private final ChatRoundOperateService chatRoundOperateService;
|
||||||
|
private final BotService botService;
|
||||||
|
private final DocumentCollectionService documentCollectionService;
|
||||||
|
private final CategoryPermissionService categoryPermissionService;
|
||||||
|
private final KnowledgeVisibilityQueryHelper knowledgeVisibilityQueryHelper;
|
||||||
|
private final ChatJsonSupport chatJsonSupport;
|
||||||
|
|
||||||
|
public ChatWorkspaceService(ChatSessionQueryService chatSessionQueryService,
|
||||||
|
ChatSessionCommandService chatSessionCommandService,
|
||||||
|
ChatRoundOperateService chatRoundOperateService,
|
||||||
|
BotService botService,
|
||||||
|
DocumentCollectionService documentCollectionService,
|
||||||
|
CategoryPermissionService categoryPermissionService,
|
||||||
|
KnowledgeVisibilityQueryHelper knowledgeVisibilityQueryHelper,
|
||||||
|
ChatJsonSupport chatJsonSupport) {
|
||||||
|
this.chatSessionQueryService = chatSessionQueryService;
|
||||||
|
this.chatSessionCommandService = chatSessionCommandService;
|
||||||
|
this.chatRoundOperateService = chatRoundOperateService;
|
||||||
|
this.botService = botService;
|
||||||
|
this.documentCollectionService = documentCollectionService;
|
||||||
|
this.categoryPermissionService = categoryPermissionService;
|
||||||
|
this.knowledgeVisibilityQueryHelper = knowledgeVisibilityQueryHelper;
|
||||||
|
this.chatJsonSupport = chatJsonSupport;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询当前用户会话分页。
|
||||||
|
*
|
||||||
|
* @param account 当前登录用户
|
||||||
|
* @param assistantId 助手过滤条件
|
||||||
|
* @param query 分页参数
|
||||||
|
* @return 工作台会话分页
|
||||||
|
*/
|
||||||
|
public ChatWorkspaceSessionPage queryCurrentUserSessions(LoginAccount account, BigInteger assistantId, ChatPageQuery query) {
|
||||||
|
ChatSessionPage page = chatSessionQueryService.pageSessions(account.getId(), assistantId, query);
|
||||||
|
Map<BigInteger, AssistantAvailability> availabilityMap = resolveAssistantAvailability(account, page.getRecords());
|
||||||
|
ChatWorkspaceSessionPage result = new ChatWorkspaceSessionPage();
|
||||||
|
result.setTotal(page.getTotal());
|
||||||
|
result.setPageNumber(page.getPageNumber());
|
||||||
|
result.setPageSize(page.getPageSize());
|
||||||
|
List<ChatWorkspaceSessionView> records = new ArrayList<>();
|
||||||
|
for (ChatSessionSummary summary : page.getRecords()) {
|
||||||
|
records.add(toSessionView(summary, availabilityMap.get(summary.getAssistantId())));
|
||||||
|
}
|
||||||
|
result.setRecords(records);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询当前用户会话详情。
|
||||||
|
*
|
||||||
|
* @param account 当前登录用户
|
||||||
|
* @param sessionId 会话 ID
|
||||||
|
* @return 工作台会话详情
|
||||||
|
*/
|
||||||
|
public ChatWorkspaceSessionDetailView getCurrentUserSession(LoginAccount account, BigInteger sessionId) {
|
||||||
|
ChatSessionSummary summary = requireUserSession(account, sessionId);
|
||||||
|
AssistantAvailability availability = resolveAssistantAvailability(account, List.of(summary)).get(summary.getAssistantId());
|
||||||
|
ChatWorkspaceSessionDetailView detail = new ChatWorkspaceSessionDetailView();
|
||||||
|
fillSessionView(detail, summary, availability);
|
||||||
|
if (availability != null && availability.displayBot() != null) {
|
||||||
|
detail.setAssistant(toAssistantView(availability.displayBot(), summary));
|
||||||
|
detail.setBoundKnowledges(resolveBoundKnowledges(availability.displayBot()));
|
||||||
|
} else {
|
||||||
|
detail.setAssistant(toAssistantView(null, summary));
|
||||||
|
}
|
||||||
|
ExtraKnowledgeResolution extraKnowledgeResolution = resolveExtraKnowledges(summary);
|
||||||
|
detail.setExtraKnowledges(extraKnowledgeResolution.validKnowledges());
|
||||||
|
detail.setRemovedExtraKnowledgeNames(extraKnowledgeResolution.removedNames());
|
||||||
|
if (extraKnowledgeResolution.shouldSync()) {
|
||||||
|
syncSessionExtraKnowledges(summary, extraKnowledgeResolution.validKnowledgeIds(), account.getId());
|
||||||
|
}
|
||||||
|
return detail;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询当前用户会话消息。
|
||||||
|
*
|
||||||
|
* @param account 当前登录用户
|
||||||
|
* @param sessionId 会话 ID
|
||||||
|
* @param query 分页参数
|
||||||
|
* @return 历史消息分页
|
||||||
|
*/
|
||||||
|
public ChatHistoryPage queryCurrentUserMessages(LoginAccount account, BigInteger sessionId, ChatPageQuery query) {
|
||||||
|
ChatSessionSummary summary = requireUserSession(account, sessionId);
|
||||||
|
ChatHistoryPage firstPage = restoreRecentMessages(summary, query);
|
||||||
|
if (firstPage != null) {
|
||||||
|
return firstPage;
|
||||||
|
}
|
||||||
|
return chatSessionQueryService.pageMainlineMessages(sessionId, query);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询当前用户完整工作台会话。
|
||||||
|
*
|
||||||
|
* @param account 当前登录用户
|
||||||
|
* @param sessionId 会话 ID
|
||||||
|
* @return 完整会话视图
|
||||||
|
*/
|
||||||
|
public ChatWorkspaceConversationView getCurrentUserConversation(LoginAccount account, BigInteger sessionId) {
|
||||||
|
requireUserSession(account, sessionId);
|
||||||
|
List<ChatMessageRecord> records = chatSessionQueryService.listMainlineMessages(sessionId);
|
||||||
|
Map<String, List<ChatMessageRecord>> variantsByRound = new LinkedHashMap<>();
|
||||||
|
Set<BigInteger> roundIds = new LinkedHashSet<>();
|
||||||
|
for (ChatMessageRecord record : records) {
|
||||||
|
if (record == null || record.getRoundId() == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Integer variantCount = record.getVariantCount();
|
||||||
|
if (variantCount != null && variantCount > 1) {
|
||||||
|
roundIds.add(record.getRoundId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (BigInteger roundId : roundIds) {
|
||||||
|
variantsByRound.put(roundId.toString(), chatRoundOperateService.listVariants(sessionId, roundId));
|
||||||
|
}
|
||||||
|
ChatWorkspaceConversationView view = new ChatWorkspaceConversationView();
|
||||||
|
view.setRecords(records);
|
||||||
|
view.setVariantsByRound(variantsByRound);
|
||||||
|
view.setTotal(records.size());
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重命名当前用户会话。
|
||||||
|
*
|
||||||
|
* @param account 当前登录用户
|
||||||
|
* @param sessionId 会话 ID
|
||||||
|
* @param title 新标题
|
||||||
|
*/
|
||||||
|
public void renameCurrentUserSession(LoginAccount account, BigInteger sessionId, String title) {
|
||||||
|
if (!StringUtils.hasText(title)) {
|
||||||
|
throw new BusinessException("标题不能为空");
|
||||||
|
}
|
||||||
|
requireUserSession(account, sessionId);
|
||||||
|
chatSessionCommandService.renameSession(sessionId, account.getId(), title.trim(), account.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除当前用户会话。
|
||||||
|
*
|
||||||
|
* @param account 当前登录用户
|
||||||
|
* @param sessionId 会话 ID
|
||||||
|
*/
|
||||||
|
public void deleteCurrentUserSession(LoginAccount account, BigInteger sessionId) {
|
||||||
|
requireUserSession(account, sessionId);
|
||||||
|
chatSessionCommandService.deleteSession(sessionId, account.getId(), account.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ChatMessageRecord> listCurrentUserRoundVariants(LoginAccount account, BigInteger sessionId, BigInteger roundId) {
|
||||||
|
requireUserSession(account, sessionId);
|
||||||
|
return chatRoundOperateService.listVariants(sessionId, roundId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ChatMessageRecord selectCurrentUserRoundVariant(LoginAccount account, BigInteger sessionId, BigInteger roundId, Integer variantIndex) {
|
||||||
|
requireUserSession(account, sessionId);
|
||||||
|
return chatRoundOperateService.selectVariant(sessionId, roundId, variantIndex, account.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送前校验会话是否仍可继续聊天。
|
||||||
|
*
|
||||||
|
* @param account 当前登录用户
|
||||||
|
* @param sessionId 会话 ID
|
||||||
|
* @param requestBotId 本次请求助手 ID
|
||||||
|
*/
|
||||||
|
public void assertSessionContinuable(LoginAccount account, BigInteger sessionId, BigInteger requestBotId) {
|
||||||
|
ChatSessionSummary summary = chatSessionQueryService.getSessionSummary(sessionId);
|
||||||
|
if (summary == null || Integer.valueOf(1).equals(summary.getIsDeleted())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!Objects.equals(summary.getUserId(), account.getId())) {
|
||||||
|
throw new BusinessException("无权访问该会话");
|
||||||
|
}
|
||||||
|
if (requestBotId != null && summary.getAssistantId() != null && !Objects.equals(summary.getAssistantId(), requestBotId)) {
|
||||||
|
throw new BusinessException("当前会话与所选聊天助手不匹配");
|
||||||
|
}
|
||||||
|
AssistantAvailability availability = resolveAssistantAvailability(account, List.of(summary)).get(summary.getAssistantId());
|
||||||
|
if (availability == null || !availability.continuable()) {
|
||||||
|
throw new BusinessException(buildReadOnlyMessage(availability == null ? ChatWorkspaceReadOnlyReason.ASSISTANT_DELETED : availability.reason()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ChatSessionSummary requireUserSession(LoginAccount account, BigInteger sessionId) {
|
||||||
|
ChatSessionSummary summary = chatSessionQueryService.getSessionSummary(sessionId);
|
||||||
|
if (summary == null || Integer.valueOf(1).equals(summary.getIsDeleted())) {
|
||||||
|
throw new BusinessException("会话不存在");
|
||||||
|
}
|
||||||
|
if (!Objects.equals(summary.getUserId(), account.getId())) {
|
||||||
|
throw new BusinessException("无权访问该会话");
|
||||||
|
}
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 首屏优先从热态恢复最近消息,避免分析库延迟导致刚完成的回复不可见。
|
||||||
|
*
|
||||||
|
* @param summary 会话摘要
|
||||||
|
* @param query 分页参数
|
||||||
|
* @return 命中热态时返回恢复结果,否则返回 null 继续走历史库
|
||||||
|
*/
|
||||||
|
private ChatHistoryPage restoreRecentMessages(ChatSessionSummary summary, ChatPageQuery query) {
|
||||||
|
if (summary == null || query == null || query.getPageNumber() != 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
List<tech.easyflow.chatlog.domain.dto.ChatMessageRecord> records =
|
||||||
|
chatSessionQueryService.getRecentTail(summary.getId(), Math.toIntExact(query.getPageSize()));
|
||||||
|
if (records == null || records.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!isRestoredTailReliable(records)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
ChatHistoryPage page = new ChatHistoryPage();
|
||||||
|
page.setPageNumber(query.getPageNumber());
|
||||||
|
page.setPageSize(query.getPageSize());
|
||||||
|
page.setRecords(records);
|
||||||
|
long total = summary.getMessageCount() == null ? 0L : summary.getMessageCount();
|
||||||
|
page.setTotal(Math.max(total, records.size()));
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验 Redis tail 是否仍符合当前主线版本语义。
|
||||||
|
*
|
||||||
|
* @param records Redis tail 消息
|
||||||
|
* @return true 表示可直接用于首屏恢复
|
||||||
|
*/
|
||||||
|
private boolean isRestoredTailReliable(List<ChatMessageRecord> records) {
|
||||||
|
Map<BigInteger, Integer> selectedVariantByRound = new LinkedHashMap<>();
|
||||||
|
Map<BigInteger, Set<Integer>> assistantVariantsByRound = new LinkedHashMap<>();
|
||||||
|
for (ChatMessageRecord record : records) {
|
||||||
|
if (record == null || record.getRoundId() == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Integer selectedVariantIndex = record.getSelectedVariantIndex();
|
||||||
|
if (selectedVariantIndex != null && selectedVariantIndex > 0) {
|
||||||
|
Integer previous = selectedVariantByRound.putIfAbsent(record.getRoundId(), selectedVariantIndex);
|
||||||
|
if (previous != null && !Objects.equals(previous, selectedVariantIndex)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ("assistant".equalsIgnoreCase(record.getSenderRole())
|
||||||
|
&& record.getVariantIndex() != null
|
||||||
|
&& record.getVariantIndex() > 0) {
|
||||||
|
assistantVariantsByRound
|
||||||
|
.computeIfAbsent(record.getRoundId(), key -> new LinkedHashSet<>())
|
||||||
|
.add(record.getVariantIndex());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (Map.Entry<BigInteger, Integer> entry : selectedVariantByRound.entrySet()) {
|
||||||
|
Set<Integer> visibleVariants = assistantVariantsByRound.get(entry.getKey());
|
||||||
|
if (visibleVariants != null && !visibleVariants.isEmpty() && !visibleVariants.contains(entry.getValue())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<BigInteger, AssistantAvailability> resolveAssistantAvailability(LoginAccount account, List<ChatSessionSummary> sessions) {
|
||||||
|
Map<BigInteger, AssistantAvailability> result = new LinkedHashMap<>();
|
||||||
|
if (sessions == null || sessions.isEmpty()) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
Set<BigInteger> assistantIds = new LinkedHashSet<>();
|
||||||
|
for (ChatSessionSummary session : sessions) {
|
||||||
|
if (session != null && session.getAssistantId() != null) {
|
||||||
|
assistantIds.add(session.getAssistantId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (assistantIds.isEmpty()) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
List<Bot> bots = botService.list(QueryWrapper.create().where(BOT.ID.in(assistantIds)));
|
||||||
|
Map<BigInteger, Bot> botMap = new LinkedHashMap<>();
|
||||||
|
for (Bot bot : bots) {
|
||||||
|
botMap.put(bot.getId(), bot);
|
||||||
|
}
|
||||||
|
RoleCategoryAccessSnapshot accessSnapshot = categoryPermissionService.getAccess("BOT", account);
|
||||||
|
for (BigInteger assistantId : assistantIds) {
|
||||||
|
Bot currentBot = botMap.get(assistantId);
|
||||||
|
if (currentBot == null) {
|
||||||
|
result.put(assistantId, new AssistantAvailability(false, ChatWorkspaceReadOnlyReason.ASSISTANT_DELETED, null));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!accessSnapshot.canAccess(currentBot.getCreatedBy(), currentBot.getCategoryId())) {
|
||||||
|
result.put(assistantId, new AssistantAvailability(false, ChatWorkspaceReadOnlyReason.NO_PERMISSION, null));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Bot displayBot = botService.toPublishedView(currentBot);
|
||||||
|
boolean online = Integer.valueOf(1).equals(currentBot.getStatus())
|
||||||
|
&& PublishStatus.from(currentBot.getPublishStatus()) == PublishStatus.PUBLISHED;
|
||||||
|
result.put(assistantId, new AssistantAvailability(
|
||||||
|
online,
|
||||||
|
online ? null : ChatWorkspaceReadOnlyReason.ASSISTANT_OFFLINE,
|
||||||
|
displayBot
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ChatWorkspaceSessionView toSessionView(ChatSessionSummary summary, AssistantAvailability availability) {
|
||||||
|
ChatWorkspaceSessionView view = new ChatWorkspaceSessionView();
|
||||||
|
fillSessionView(view, summary, availability);
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fillSessionView(ChatWorkspaceSessionView view, ChatSessionSummary summary, AssistantAvailability availability) {
|
||||||
|
view.setSessionId(summary.getId());
|
||||||
|
view.setAssistantId(summary.getAssistantId());
|
||||||
|
view.setAssistantCode(summary.getAssistantCode());
|
||||||
|
view.setAssistantName(summary.getAssistantName());
|
||||||
|
view.setTitle(summary.getTitle());
|
||||||
|
view.setLastMessagePreview(summary.getLastMessagePreview());
|
||||||
|
view.setMessageCount(summary.getMessageCount());
|
||||||
|
view.setAccessAt(summary.getAccessAt());
|
||||||
|
view.setLastMessageAt(summary.getLastMessageAt());
|
||||||
|
view.setContinuable(availability != null && availability.continuable());
|
||||||
|
view.setReadOnlyReason(availability == null ? ChatWorkspaceReadOnlyReason.ASSISTANT_DELETED : availability.reason());
|
||||||
|
}
|
||||||
|
|
||||||
|
private ChatWorkspaceAssistantView toAssistantView(Bot bot, ChatSessionSummary summary) {
|
||||||
|
ChatWorkspaceAssistantView view = new ChatWorkspaceAssistantView();
|
||||||
|
if (bot != null) {
|
||||||
|
view.setId(bot.getId());
|
||||||
|
view.setAlias(bot.getAlias());
|
||||||
|
view.setTitle(bot.getTitle());
|
||||||
|
view.setDescription(bot.getDescription());
|
||||||
|
view.setIcon(bot.getIcon());
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
view.setId(summary == null ? null : summary.getAssistantId());
|
||||||
|
view.setAlias(summary == null ? null : summary.getAssistantCode());
|
||||||
|
view.setTitle(summary == null ? null : summary.getAssistantName());
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ChatWorkspaceKnowledgeView> resolveBoundKnowledges(Bot displayBot) {
|
||||||
|
if (displayBot == null || displayBot.getPublishedSnapshotJson() == null) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
Object rawBindings = displayBot.getPublishedSnapshotJson().get("knowledgeBindings");
|
||||||
|
if (!(rawBindings instanceof List<?> bindings) || bindings.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
List<BigInteger> knowledgeIds = new ArrayList<>();
|
||||||
|
for (Object binding : bindings) {
|
||||||
|
if (!(binding instanceof Map<?, ?> bindingMap) || bindingMap.get("knowledgeId") == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
knowledgeIds.add(new BigInteger(String.valueOf(bindingMap.get("knowledgeId"))));
|
||||||
|
}
|
||||||
|
return resolveVisibleKnowledgeViews(knowledgeIds).validKnowledges();
|
||||||
|
}
|
||||||
|
|
||||||
|
private ExtraKnowledgeResolution resolveExtraKnowledges(ChatSessionSummary summary) {
|
||||||
|
ChatSessionExtPayload payload = chatJsonSupport.fromJson(summary.getExtJson(), ChatSessionExtPayload.class);
|
||||||
|
List<BigInteger> extraKnowledgeIds = payload == null ? List.of() : payload.getExtraKnowledgeIds();
|
||||||
|
return resolveVisibleKnowledgeViews(extraKnowledgeIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ExtraKnowledgeResolution resolveVisibleKnowledgeViews(List<BigInteger> knowledgeIds) {
|
||||||
|
if (knowledgeIds == null || knowledgeIds.isEmpty()) {
|
||||||
|
return new ExtraKnowledgeResolution(List.of(), List.of(), List.of(), false);
|
||||||
|
}
|
||||||
|
List<BigInteger> normalizedIds = new ArrayList<>();
|
||||||
|
for (BigInteger knowledgeId : knowledgeIds) {
|
||||||
|
if (knowledgeId != null && !normalizedIds.contains(knowledgeId)) {
|
||||||
|
normalizedIds.add(knowledgeId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (normalizedIds.isEmpty()) {
|
||||||
|
return new ExtraKnowledgeResolution(List.of(), List.of(), List.of(), false);
|
||||||
|
}
|
||||||
|
List<DocumentCollection> collections = documentCollectionService.listByIds(normalizedIds);
|
||||||
|
Map<BigInteger, DocumentCollection> collectionMap = new LinkedHashMap<>();
|
||||||
|
for (DocumentCollection collection : collections) {
|
||||||
|
collectionMap.put(collection.getId(), collection);
|
||||||
|
}
|
||||||
|
KnowledgeReadAccessSnapshot accessSnapshot = knowledgeVisibilityQueryHelper.getCurrentReadSnapshot();
|
||||||
|
List<ChatWorkspaceKnowledgeView> validKnowledges = new ArrayList<>();
|
||||||
|
List<BigInteger> validKnowledgeIds = new ArrayList<>();
|
||||||
|
List<String> removedNames = new ArrayList<>();
|
||||||
|
boolean changed = false;
|
||||||
|
for (BigInteger knowledgeId : normalizedIds) {
|
||||||
|
DocumentCollection current = collectionMap.get(knowledgeId);
|
||||||
|
if (current == null) {
|
||||||
|
removedNames.add("知识库#" + knowledgeId);
|
||||||
|
changed = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (PublishStatus.from(current.getPublishStatus()) != PublishStatus.PUBLISHED) {
|
||||||
|
removedNames.add(current.getTitle());
|
||||||
|
changed = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!knowledgeVisibilityQueryHelper.canRead(current, accessSnapshot)) {
|
||||||
|
removedNames.add(current.getTitle());
|
||||||
|
changed = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
validKnowledges.add(toKnowledgeView(documentCollectionService.toPublishedView(current)));
|
||||||
|
validKnowledgeIds.add(current.getId());
|
||||||
|
}
|
||||||
|
if (!Objects.equals(normalizedIds, validKnowledgeIds)) {
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
return new ExtraKnowledgeResolution(validKnowledges, validKnowledgeIds, removedNames, changed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ChatWorkspaceKnowledgeView toKnowledgeView(DocumentCollection collection) {
|
||||||
|
ChatWorkspaceKnowledgeView view = new ChatWorkspaceKnowledgeView();
|
||||||
|
view.setId(collection.getId());
|
||||||
|
view.setAlias(collection.getAlias());
|
||||||
|
view.setTitle(collection.getTitle());
|
||||||
|
view.setDescription(collection.getDescription());
|
||||||
|
view.setIcon(collection.getIcon());
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void syncSessionExtraKnowledges(ChatSessionSummary summary, List<BigInteger> validKnowledgeIds, BigInteger operatorId) {
|
||||||
|
ChatSessionExtPayload payload = new ChatSessionExtPayload();
|
||||||
|
payload.setExtraKnowledgeIds(validKnowledgeIds);
|
||||||
|
ChatSessionUpsertCommand command = new ChatSessionUpsertCommand();
|
||||||
|
command.setSessionId(summary.getId());
|
||||||
|
command.setTenantId(summary.getTenantId());
|
||||||
|
command.setDeptId(summary.getDeptId());
|
||||||
|
command.setUserId(summary.getUserId());
|
||||||
|
command.setUserAccount(summary.getUserAccount());
|
||||||
|
command.setAssistantId(summary.getAssistantId());
|
||||||
|
command.setAssistantCode(summary.getAssistantCode());
|
||||||
|
command.setAssistantName(summary.getAssistantName());
|
||||||
|
command.setTitle(summary.getTitle());
|
||||||
|
command.setExtJson(chatJsonSupport.toJson(payload));
|
||||||
|
command.setOperatorId(operatorId);
|
||||||
|
command.setOperateAt(new Date());
|
||||||
|
chatSessionCommandService.createOrTouchSession(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildReadOnlyMessage(ChatWorkspaceReadOnlyReason reason) {
|
||||||
|
if (reason == ChatWorkspaceReadOnlyReason.NO_PERMISSION) {
|
||||||
|
return "当前会话对应的聊天助手已无权限访问,仅支持查看历史记录";
|
||||||
|
}
|
||||||
|
if (reason == ChatWorkspaceReadOnlyReason.ASSISTANT_OFFLINE) {
|
||||||
|
return "当前会话对应的聊天助手已下架,无法继续聊天";
|
||||||
|
}
|
||||||
|
return "当前会话对应的聊天助手已删除,无法继续聊天";
|
||||||
|
}
|
||||||
|
|
||||||
|
private record AssistantAvailability(boolean continuable,
|
||||||
|
ChatWorkspaceReadOnlyReason reason,
|
||||||
|
Bot displayBot) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private record ExtraKnowledgeResolution(List<ChatWorkspaceKnowledgeView> validKnowledges,
|
||||||
|
List<BigInteger> validKnowledgeIds,
|
||||||
|
List<String> removedNames,
|
||||||
|
boolean shouldSync) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package tech.easyflow.admin.service.dashboard;
|
||||||
|
|
||||||
|
import tech.easyflow.admin.model.dashboard.DashboardOverviewQuery;
|
||||||
|
import tech.easyflow.admin.model.dashboard.DashboardOverviewVo;
|
||||||
|
import tech.easyflow.admin.model.dashboard.DashboardUserRankItemVo;
|
||||||
|
import tech.easyflow.admin.model.dashboard.DashboardUserRankQuery;
|
||||||
|
import tech.easyflow.common.entity.LoginAccount;
|
||||||
|
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作台统计服务。
|
||||||
|
*/
|
||||||
|
public interface DashboardService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取工作台总览。
|
||||||
|
*
|
||||||
|
* @param loginAccount 当前登录账号
|
||||||
|
* @param query 查询条件
|
||||||
|
* @return 工作台总览
|
||||||
|
*/
|
||||||
|
DashboardOverviewVo getOverview(LoginAccount loginAccount, DashboardOverviewQuery query);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户活跃榜。
|
||||||
|
*
|
||||||
|
* @param loginAccount 当前登录账号
|
||||||
|
* @param query 查询条件
|
||||||
|
* @return 用户活跃榜
|
||||||
|
*/
|
||||||
|
List<DashboardUserRankItemVo> getUserRanks(LoginAccount loginAccount, DashboardUserRankQuery query);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出用户活跃榜。
|
||||||
|
*
|
||||||
|
* @param loginAccount 当前登录账号
|
||||||
|
* @param query 查询条件
|
||||||
|
* @param outputStream 输出流
|
||||||
|
*/
|
||||||
|
void exportUserRanks(LoginAccount loginAccount, DashboardUserRankQuery query, OutputStream outputStream);
|
||||||
|
}
|
||||||
@@ -0,0 +1,971 @@
|
|||||||
|
package tech.easyflow.admin.service.dashboard.impl;
|
||||||
|
|
||||||
|
import cn.idev.excel.EasyExcel;
|
||||||
|
import cn.idev.excel.write.style.column.SimpleColumnWidthStyleStrategy;
|
||||||
|
import com.mybatisflex.core.query.QueryWrapper;
|
||||||
|
import com.mybatisflex.core.row.Db;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
import tech.easyflow.ai.entity.Bot;
|
||||||
|
import tech.easyflow.ai.service.BotService;
|
||||||
|
import tech.easyflow.admin.model.dashboard.DashboardChatStatusVo;
|
||||||
|
import tech.easyflow.admin.model.dashboard.DashboardAssistantTrendPointVo;
|
||||||
|
import tech.easyflow.admin.model.dashboard.DashboardAssistantTrendSeriesVo;
|
||||||
|
import tech.easyflow.admin.model.dashboard.DashboardDistributionItemVo;
|
||||||
|
import tech.easyflow.admin.model.dashboard.DashboardOverviewQuery;
|
||||||
|
import tech.easyflow.admin.model.dashboard.DashboardOverviewVo;
|
||||||
|
import tech.easyflow.admin.model.dashboard.DashboardSummaryVo;
|
||||||
|
import tech.easyflow.admin.model.dashboard.DashboardTrendItemVo;
|
||||||
|
import tech.easyflow.admin.model.dashboard.DashboardUserRankItemVo;
|
||||||
|
import tech.easyflow.admin.model.dashboard.DashboardUserRankQuery;
|
||||||
|
import tech.easyflow.admin.service.dashboard.DashboardService;
|
||||||
|
import tech.easyflow.chatlog.domain.dto.ChatActiveUserRank;
|
||||||
|
import tech.easyflow.chatlog.domain.dto.ChatAssistantSessionTrend;
|
||||||
|
import tech.easyflow.chatlog.domain.dto.ChatAssistantUsageRank;
|
||||||
|
import tech.easyflow.chatlog.domain.dto.ChatDashboardSummary;
|
||||||
|
import tech.easyflow.chatlog.domain.dto.ChatDashboardTrend;
|
||||||
|
import tech.easyflow.chatlog.service.ChatDashboardQueryService;
|
||||||
|
import tech.easyflow.common.constant.Constants;
|
||||||
|
import tech.easyflow.common.entity.LoginAccount;
|
||||||
|
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||||
|
import tech.easyflow.system.entity.SysAccount;
|
||||||
|
import tech.easyflow.system.entity.SysAccountRole;
|
||||||
|
import tech.easyflow.system.entity.SysRole;
|
||||||
|
import tech.easyflow.system.service.CategoryPermissionService;
|
||||||
|
import tech.easyflow.system.service.SysAccountService;
|
||||||
|
import tech.easyflow.system.service.SysAccountRoleService;
|
||||||
|
import tech.easyflow.system.service.SysRoleService;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.LocalTime;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作台统计服务实现。
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class DashboardServiceImpl implements DashboardService {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(DashboardServiceImpl.class);
|
||||||
|
private static final ZoneId DEFAULT_ZONE_ID = ZoneId.systemDefault();
|
||||||
|
private static final String CHAT_UNAVAILABLE_MESSAGE = "聊天数据不可用";
|
||||||
|
private static final int DEFAULT_ASSISTANT_RANK_LIMIT = 5;
|
||||||
|
private static final int DEFAULT_ASSISTANT_TREND_LIMIT = 8;
|
||||||
|
private static final int DEFAULT_USER_RANK_LIMIT = 5;
|
||||||
|
private static final int USER_RANK_EXPORT_COLUMN_WIDTH = 20;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private SysAccountRoleService sysAccountRoleService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private SysRoleService sysRoleService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private SysAccountService sysAccountService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ChatDashboardQueryService chatDashboardQueryService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private BotService botService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private CategoryPermissionService categoryPermissionService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取工作台总览信息。
|
||||||
|
*
|
||||||
|
* @param loginAccount 当前登录账号
|
||||||
|
* @param query 查询条件
|
||||||
|
* @return 工作台总览
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public DashboardOverviewVo getOverview(LoginAccount loginAccount, DashboardOverviewQuery query) {
|
||||||
|
DashboardQueryContext context = buildContext(loginAccount, query);
|
||||||
|
DashboardSummaryVo summary = buildSummary(context);
|
||||||
|
ChatDashboardPayload chatPayload = buildChatPayload(context, summary);
|
||||||
|
|
||||||
|
DashboardOverviewVo result = new DashboardOverviewVo();
|
||||||
|
result.setSummary(summary);
|
||||||
|
result.setChatStatus(chatPayload.chatStatus);
|
||||||
|
result.setTrends(chatPayload.trends);
|
||||||
|
result.setAssistantTrends(chatPayload.assistantTrends);
|
||||||
|
result.setDistribution(chatPayload.distribution);
|
||||||
|
|
||||||
|
DashboardOverviewQuery normalizedQuery = new DashboardOverviewQuery();
|
||||||
|
normalizedQuery.setRange(context.range);
|
||||||
|
normalizedQuery.setStartDate(context.queryStartDate);
|
||||||
|
normalizedQuery.setEndDate(context.queryEndDate);
|
||||||
|
result.setQuery(normalizedQuery);
|
||||||
|
result.setUpdatedAt(new Date());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户活跃榜。
|
||||||
|
*
|
||||||
|
* @param loginAccount 当前登录账号
|
||||||
|
* @param query 查询条件
|
||||||
|
* @return 用户活跃榜
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public List<DashboardUserRankItemVo> getUserRanks(LoginAccount loginAccount, DashboardUserRankQuery query) {
|
||||||
|
DashboardQueryContext context = buildContext(loginAccount, query);
|
||||||
|
return queryUserRanks(context, DEFAULT_USER_RANK_LIMIT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出用户活跃榜。
|
||||||
|
*
|
||||||
|
* @param loginAccount 当前登录账号
|
||||||
|
* @param query 查询条件
|
||||||
|
* @param outputStream 输出流
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void exportUserRanks(LoginAccount loginAccount, DashboardUserRankQuery query, OutputStream outputStream) {
|
||||||
|
DashboardQueryContext context = buildContext(loginAccount, query);
|
||||||
|
List<DashboardUserRankItemVo> userRanks = queryUserRanks(context, null);
|
||||||
|
EasyExcel.write(outputStream)
|
||||||
|
.head(buildUserRankExportHead())
|
||||||
|
.registerWriteHandler(new SimpleColumnWidthStyleStrategy(USER_RANK_EXPORT_COLUMN_WIDTH))
|
||||||
|
.sheet("用户活跃榜")
|
||||||
|
.doWrite(buildUserRankExportRows(userRanks));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建顶部汇总卡片。
|
||||||
|
*
|
||||||
|
* @param context 查询上下文
|
||||||
|
* @return 汇总结果
|
||||||
|
*/
|
||||||
|
private DashboardSummaryVo buildSummary(DashboardQueryContext context) {
|
||||||
|
DashboardSummaryVo summary = new DashboardSummaryVo();
|
||||||
|
summary.setUserTotal(countScopedTable("tb_sys_account", "a", true, context));
|
||||||
|
summary.setActiveUserTotal(countActiveUsers(context));
|
||||||
|
summary.setBotTotal(countScopedTable("tb_bot", "b", false, context));
|
||||||
|
summary.setWorkflowTotal(countScopedTable("tb_workflow", "w", false, context));
|
||||||
|
summary.setKnowledgeBaseTotal(countScopedTable("tb_document_collection", "d", false, context));
|
||||||
|
summary.setChatMessageTotal(0L);
|
||||||
|
summary.setChatSessionTotal(0L);
|
||||||
|
summary.setActiveAssistantTotal(0L);
|
||||||
|
summary.setChatActiveUserTotal(0L);
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建聊天统计载荷。
|
||||||
|
*
|
||||||
|
* @param context 查询上下文
|
||||||
|
* @param summary 汇总结果
|
||||||
|
* @return 聊天统计载荷
|
||||||
|
*/
|
||||||
|
private ChatDashboardPayload buildChatPayload(DashboardQueryContext context, DashboardSummaryVo summary) {
|
||||||
|
DashboardChatStatusVo chatStatus = new DashboardChatStatusVo();
|
||||||
|
chatStatus.setAvailable(Boolean.TRUE);
|
||||||
|
chatStatus.setMessage("");
|
||||||
|
|
||||||
|
if (!chatDashboardQueryService.available()) {
|
||||||
|
chatStatus.setAvailable(Boolean.FALSE);
|
||||||
|
chatStatus.setMessage(CHAT_UNAVAILABLE_MESSAGE);
|
||||||
|
summary.setChatMessageTotal(0L);
|
||||||
|
summary.setChatSessionTotal(0L);
|
||||||
|
summary.setActiveAssistantTotal(0L);
|
||||||
|
summary.setChatActiveUserTotal(0L);
|
||||||
|
return new ChatDashboardPayload(
|
||||||
|
chatStatus,
|
||||||
|
new ArrayList<>(),
|
||||||
|
new ArrayList<>(),
|
||||||
|
new ArrayList<>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalDate startDate = context.startTime.toLocalDate();
|
||||||
|
LocalDate endDate = context.endTime.toLocalDate();
|
||||||
|
try {
|
||||||
|
ChatDashboardSummary chatSummary = chatDashboardQueryService.querySummary(startDate, endDate, context.tenantFilterId);
|
||||||
|
summary.setChatMessageTotal(chatSummary.messageTotal());
|
||||||
|
summary.setChatSessionTotal(chatSummary.sessionTotal());
|
||||||
|
summary.setActiveAssistantTotal(chatSummary.activeAssistantTotal());
|
||||||
|
summary.setChatActiveUserTotal(chatSummary.chatActiveUserTotal());
|
||||||
|
|
||||||
|
List<ChatDashboardTrend> rawTrends = useHourlyBuckets(context)
|
||||||
|
? chatDashboardQueryService.queryHourlyTrends(context.startTime, context.endTime, context.tenantFilterId)
|
||||||
|
: chatDashboardQueryService.queryTrends(startDate, endDate, context.tenantFilterId);
|
||||||
|
List<DashboardTrendItemVo> trends = buildTrendItems(context, rawTrends);
|
||||||
|
|
||||||
|
List<ChatAssistantUsageRank> rawRanks = chatDashboardQueryService.queryAssistantUsageRanks(
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
context.tenantFilterId,
|
||||||
|
DEFAULT_ASSISTANT_TREND_LIMIT
|
||||||
|
);
|
||||||
|
List<DashboardAssistantTrendSeriesVo> assistantTrends = buildAssistantTrendSeries(
|
||||||
|
context,
|
||||||
|
rawRanks
|
||||||
|
);
|
||||||
|
List<DashboardDistributionItemVo> distribution = buildAssistantDistribution(
|
||||||
|
rawRanks.subList(0, Math.min(DEFAULT_ASSISTANT_RANK_LIMIT, rawRanks.size()))
|
||||||
|
);
|
||||||
|
return new ChatDashboardPayload(chatStatus, trends, assistantTrends, distribution);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.warn("加载工作台聊天统计失败,已降级为不可用状态,range={}, tenantId={}",
|
||||||
|
context.range,
|
||||||
|
context.tenantFilterId,
|
||||||
|
ex);
|
||||||
|
chatStatus.setAvailable(Boolean.FALSE);
|
||||||
|
chatStatus.setMessage(CHAT_UNAVAILABLE_MESSAGE);
|
||||||
|
summary.setChatMessageTotal(0L);
|
||||||
|
summary.setChatSessionTotal(0L);
|
||||||
|
summary.setActiveAssistantTotal(0L);
|
||||||
|
summary.setChatActiveUserTotal(0L);
|
||||||
|
return new ChatDashboardPayload(
|
||||||
|
chatStatus,
|
||||||
|
new ArrayList<>(),
|
||||||
|
new ArrayList<>(),
|
||||||
|
new ArrayList<>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建聊天趋势项,缺失日期补 0。
|
||||||
|
*
|
||||||
|
* @param range 时间范围
|
||||||
|
* @param rawTrends 原始趋势
|
||||||
|
* @return 趋势项
|
||||||
|
*/
|
||||||
|
private List<DashboardTrendItemVo> buildTrendItems(DashboardQueryContext context, List<ChatDashboardTrend> rawTrends) {
|
||||||
|
List<TimeBucket> buckets = buildBuckets(
|
||||||
|
context.range,
|
||||||
|
context.startTime.toLocalDate(),
|
||||||
|
context.endTime.toLocalDate().minusDays(1)
|
||||||
|
);
|
||||||
|
Map<String, ChatDashboardTrend> trendMap = new HashMap<>();
|
||||||
|
for (ChatDashboardTrend rawTrend : rawTrends) {
|
||||||
|
trendMap.put(rawTrend.bucketKey(), rawTrend);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<DashboardTrendItemVo> items = new ArrayList<>(buckets.size());
|
||||||
|
for (TimeBucket bucket : buckets) {
|
||||||
|
ChatDashboardTrend trend = trendMap.get(bucket.key);
|
||||||
|
DashboardTrendItemVo item = new DashboardTrendItemVo();
|
||||||
|
item.setKey(bucket.key);
|
||||||
|
item.setLabel(bucket.label);
|
||||||
|
item.setActiveUserTotal(trend == null ? 0L : trend.activeUserTotal());
|
||||||
|
item.setChatMessageTotal(trend == null ? 0L : trend.messageTotal());
|
||||||
|
item.setChatSessionTotal(trend == null ? 0L : trend.sessionTotal());
|
||||||
|
items.add(item);
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建智能体使用排行。
|
||||||
|
*
|
||||||
|
* @param ranks 原始排行数据
|
||||||
|
* @return 页面排行项
|
||||||
|
*/
|
||||||
|
private List<DashboardDistributionItemVo> buildAssistantDistribution(List<ChatAssistantUsageRank> ranks) {
|
||||||
|
List<DashboardDistributionItemVo> items = new ArrayList<>(ranks.size());
|
||||||
|
for (ChatAssistantUsageRank rank : ranks) {
|
||||||
|
DashboardDistributionItemVo item = new DashboardDistributionItemVo();
|
||||||
|
item.setKey(rank.assistantId() == null ? "" : rank.assistantId().toString());
|
||||||
|
item.setAssistantId(rank.assistantId());
|
||||||
|
item.setLabel(resolveAssistantLabel(rank.assistantId(), rank.assistantName()));
|
||||||
|
item.setUserTotal(rank.userTotal());
|
||||||
|
item.setMessageTotal(rank.messageTotal());
|
||||||
|
item.setSessionTotal(rank.sessionTotal());
|
||||||
|
item.setAvgMessagePerSession(calculateAvg(rank.messageTotal(), rank.sessionTotal()));
|
||||||
|
item.setAvgSessionPerUser(calculateAvg(rank.sessionTotal(), rank.userTotal()));
|
||||||
|
item.setValue(rank.sessionTotal());
|
||||||
|
items.add(item);
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建智能体活跃趋势序列。
|
||||||
|
*
|
||||||
|
* @param context 查询上下文
|
||||||
|
* @param ranks 智能体排行
|
||||||
|
* @return 趋势序列
|
||||||
|
*/
|
||||||
|
private List<DashboardAssistantTrendSeriesVo> buildAssistantTrendSeries(DashboardQueryContext context,
|
||||||
|
List<ChatAssistantUsageRank> ranks) {
|
||||||
|
if (ranks == null || ranks.isEmpty()) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
List<TimeBucket> buckets = buildBuckets(
|
||||||
|
context.range,
|
||||||
|
context.startTime.toLocalDate(),
|
||||||
|
context.endTime.toLocalDate().minusDays(1)
|
||||||
|
);
|
||||||
|
Map<BigInteger, ChatAssistantUsageRank> rankMap = new LinkedHashMap<>();
|
||||||
|
for (ChatAssistantUsageRank rank : ranks) {
|
||||||
|
rankMap.putIfAbsent(rank.assistantId(), rank);
|
||||||
|
}
|
||||||
|
List<BigInteger> assistantIds = new ArrayList<>(rankMap.keySet());
|
||||||
|
List<ChatAssistantSessionTrend> rawAssistantTrends = useHourlyBuckets(context)
|
||||||
|
? chatDashboardQueryService.queryAssistantHourlyTrends(
|
||||||
|
context.startTime,
|
||||||
|
context.endTime,
|
||||||
|
context.tenantFilterId,
|
||||||
|
assistantIds
|
||||||
|
)
|
||||||
|
: chatDashboardQueryService.queryAssistantTrends(
|
||||||
|
context.startTime.toLocalDate(),
|
||||||
|
context.endTime.toLocalDate(),
|
||||||
|
context.tenantFilterId,
|
||||||
|
assistantIds
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<BigInteger, Map<String, ChatAssistantSessionTrend>> trendMap = new HashMap<>();
|
||||||
|
for (ChatAssistantSessionTrend rawTrend : rawAssistantTrends) {
|
||||||
|
trendMap.computeIfAbsent(rawTrend.assistantId(), key -> new HashMap<>())
|
||||||
|
.put(rawTrend.bucketKey(), rawTrend);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<DashboardAssistantTrendSeriesVo> seriesList = new ArrayList<>(rankMap.size());
|
||||||
|
for (ChatAssistantUsageRank rank : rankMap.values()) {
|
||||||
|
BigInteger assistantId = rank.assistantId();
|
||||||
|
DashboardAssistantTrendSeriesVo series = new DashboardAssistantTrendSeriesVo();
|
||||||
|
series.setAssistantId(assistantId);
|
||||||
|
series.setLabel(resolveAssistantLabel(assistantId, rank.assistantName()));
|
||||||
|
series.setTotalSessionCount(rank.sessionTotal());
|
||||||
|
|
||||||
|
List<DashboardAssistantTrendPointVo> points = new ArrayList<>(buckets.size());
|
||||||
|
Map<String, ChatAssistantSessionTrend> assistantTrendMap =
|
||||||
|
trendMap.getOrDefault(assistantId, new HashMap<>());
|
||||||
|
for (TimeBucket bucket : buckets) {
|
||||||
|
ChatAssistantSessionTrend trend = assistantTrendMap.get(bucket.key);
|
||||||
|
DashboardAssistantTrendPointVo point = new DashboardAssistantTrendPointVo();
|
||||||
|
point.setKey(bucket.key);
|
||||||
|
point.setLabel(bucket.label);
|
||||||
|
point.setSessionTotal(trend == null ? 0L : trend.sessionTotal());
|
||||||
|
points.add(point);
|
||||||
|
}
|
||||||
|
series.setPoints(points);
|
||||||
|
seriesList.add(series);
|
||||||
|
}
|
||||||
|
return seriesList;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询用户活跃排行。
|
||||||
|
*
|
||||||
|
* @param context 查询上下文
|
||||||
|
* @param limit 返回条数,空表示全部
|
||||||
|
* @return 用户活跃排行
|
||||||
|
*/
|
||||||
|
private List<DashboardUserRankItemVo> queryUserRanks(DashboardQueryContext context, Integer limit) {
|
||||||
|
if (!chatDashboardQueryService.available()) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
List<ChatActiveUserRank> rawUserRanks = chatDashboardQueryService.queryActiveUserRanks(
|
||||||
|
context.startTime.toLocalDate(),
|
||||||
|
context.endTime.toLocalDate(),
|
||||||
|
context.tenantFilterId,
|
||||||
|
context.assistantId,
|
||||||
|
limit
|
||||||
|
);
|
||||||
|
return buildUserRanks(rawUserRanks);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建用户活跃排行。
|
||||||
|
*
|
||||||
|
* @param ranks 原始排行数据
|
||||||
|
* @return 页面排行项
|
||||||
|
*/
|
||||||
|
private List<DashboardUserRankItemVo> buildUserRanks(List<ChatActiveUserRank> ranks) {
|
||||||
|
if (ranks == null || ranks.isEmpty()) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
List<DashboardUserRankItemVo> items = new ArrayList<>(ranks.size());
|
||||||
|
Map<BigInteger, AccountIdentitySnapshot> identityMap = resolveAccountIdentityMap(ranks);
|
||||||
|
for (ChatActiveUserRank rank : ranks) {
|
||||||
|
ResolvedUserIdentity identity = resolveUserIdentity(
|
||||||
|
rank.userId(),
|
||||||
|
rank.userAccount(),
|
||||||
|
identityMap.get(rank.userId())
|
||||||
|
);
|
||||||
|
DashboardUserRankItemVo item = new DashboardUserRankItemVo();
|
||||||
|
item.setUserId(rank.userId());
|
||||||
|
item.setLabel(identity.label);
|
||||||
|
item.setLoginName(identity.loginName);
|
||||||
|
item.setNickname(identity.nickname);
|
||||||
|
item.setSessionTotal(rank.sessionTotal());
|
||||||
|
item.setMessageTotal(rank.messageTotal());
|
||||||
|
items.add(item);
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建导出表头。
|
||||||
|
*
|
||||||
|
* @return 表头
|
||||||
|
*/
|
||||||
|
private List<List<String>> buildUserRankExportHead() {
|
||||||
|
List<List<String>> head = new ArrayList<>(4);
|
||||||
|
head.add(List.of("登录账号"));
|
||||||
|
head.add(List.of("昵称"));
|
||||||
|
head.add(List.of("会话数"));
|
||||||
|
head.add(List.of("消息数"));
|
||||||
|
return head;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建导出数据行。
|
||||||
|
*
|
||||||
|
* @param userRanks 用户排行
|
||||||
|
* @return 数据行
|
||||||
|
*/
|
||||||
|
private List<List<String>> buildUserRankExportRows(List<DashboardUserRankItemVo> userRanks) {
|
||||||
|
List<List<String>> rows = new ArrayList<>(userRanks.size());
|
||||||
|
for (DashboardUserRankItemVo item : userRanks) {
|
||||||
|
List<String> row = new ArrayList<>(4);
|
||||||
|
row.add(defaultIfBlank(item.getLoginName()));
|
||||||
|
row.add(defaultIfBlank(item.getNickname()));
|
||||||
|
row.add(String.valueOf(item.getSessionTotal() == null ? 0L : item.getSessionTotal()));
|
||||||
|
row.add(String.valueOf(item.getMessageTotal() == null ? 0L : item.getMessageTotal()));
|
||||||
|
rows.add(row);
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按租户统计平台资源数量。
|
||||||
|
*
|
||||||
|
* @param tableName 表名
|
||||||
|
* @param alias 别名
|
||||||
|
* @param containsLogicDelete 是否包含逻辑删除条件
|
||||||
|
* @param context 查询上下文
|
||||||
|
* @return 统计值
|
||||||
|
*/
|
||||||
|
private long countScopedTable(String tableName, String alias, boolean containsLogicDelete, DashboardQueryContext context) {
|
||||||
|
StringBuilder sql = new StringBuilder();
|
||||||
|
List<Object> params = new ArrayList<>();
|
||||||
|
|
||||||
|
sql.append("SELECT COUNT(*) AS total FROM ").append(tableName).append(" ").append(alias).append(" WHERE 1 = 1 ");
|
||||||
|
if (containsLogicDelete) {
|
||||||
|
sql.append("AND ").append(alias).append(".is_deleted IS NULL ");
|
||||||
|
}
|
||||||
|
appendOptionalTenantFilter(sql, params, context.tenantFilterId, alias + ".tenant_id");
|
||||||
|
appendOptionalDeptFilter(sql, params, context.deptFilterId, alias + ".dept_id");
|
||||||
|
return queryForLong(sql.toString(), params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计当前时间范围内活跃用户数。
|
||||||
|
*
|
||||||
|
* @param context 查询上下文
|
||||||
|
* @return 活跃用户数
|
||||||
|
*/
|
||||||
|
private long countActiveUsers(DashboardQueryContext context) {
|
||||||
|
StringBuilder sql = new StringBuilder();
|
||||||
|
List<Object> params = new ArrayList<>();
|
||||||
|
|
||||||
|
sql.append("SELECT COUNT(DISTINCT l.account_id) AS total ")
|
||||||
|
.append("FROM tb_sys_log l ")
|
||||||
|
.append("INNER JOIN tb_sys_account a ON a.id = l.account_id AND a.is_deleted IS NULL ")
|
||||||
|
.append("WHERE l.created >= ? AND l.created < ? ");
|
||||||
|
params.add(toDate(context.startTime));
|
||||||
|
params.add(toDate(context.endTime));
|
||||||
|
appendOptionalTenantFilter(sql, params, context.tenantFilterId, "a.tenant_id");
|
||||||
|
appendOptionalDeptFilter(sql, params, context.deptFilterId, "a.dept_id");
|
||||||
|
return queryForLong(sql.toString(), params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行 count SQL 并返回 long 值。
|
||||||
|
*
|
||||||
|
* @param sql SQL
|
||||||
|
* @param params 参数
|
||||||
|
* @return long 值
|
||||||
|
*/
|
||||||
|
private long queryForLong(String sql, List<Object> params) {
|
||||||
|
Object result = Db.selectObject(sql, params.toArray());
|
||||||
|
return asLong(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 追加租户过滤。
|
||||||
|
*
|
||||||
|
* @param sql SQL 构造器
|
||||||
|
* @param params 参数列表
|
||||||
|
* @param tenantId 租户 ID
|
||||||
|
* @param columnName 列名
|
||||||
|
*/
|
||||||
|
private void appendOptionalTenantFilter(StringBuilder sql, List<Object> params, BigInteger tenantId, String columnName) {
|
||||||
|
if (tenantId != null) {
|
||||||
|
sql.append(" AND ").append(columnName).append(" = ? ");
|
||||||
|
params.add(tenantId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 追加部门过滤。
|
||||||
|
*
|
||||||
|
* @param sql SQL 构造器
|
||||||
|
* @param params 参数列表
|
||||||
|
* @param deptId 部门 ID
|
||||||
|
* @param columnName 列名
|
||||||
|
*/
|
||||||
|
private void appendOptionalDeptFilter(StringBuilder sql, List<Object> params, BigInteger deptId, String columnName) {
|
||||||
|
if (deptId != null) {
|
||||||
|
sql.append(" AND ").append(columnName).append(" = ? ");
|
||||||
|
params.add(deptId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建查询上下文。
|
||||||
|
*
|
||||||
|
* @param loginAccount 当前登录账号
|
||||||
|
* @param query 查询条件
|
||||||
|
* @return 查询上下文
|
||||||
|
*/
|
||||||
|
private DashboardQueryContext buildContext(LoginAccount loginAccount, DashboardOverviewQuery query) {
|
||||||
|
return buildContext(
|
||||||
|
loginAccount,
|
||||||
|
query == null ? null : query.getRange(),
|
||||||
|
query == null ? null : query.getStartDate(),
|
||||||
|
query == null ? null : query.getEndDate()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建用户榜查询上下文。
|
||||||
|
*
|
||||||
|
* @param loginAccount 当前登录账号
|
||||||
|
* @param query 用户榜查询
|
||||||
|
* @return 查询上下文
|
||||||
|
*/
|
||||||
|
private DashboardQueryContext buildContext(LoginAccount loginAccount, DashboardUserRankQuery query) {
|
||||||
|
DashboardQueryContext context = buildContext(
|
||||||
|
loginAccount,
|
||||||
|
query == null ? null : query.getRange(),
|
||||||
|
query == null ? null : query.getStartDate(),
|
||||||
|
query == null ? null : query.getEndDate()
|
||||||
|
);
|
||||||
|
context.assistantId = validateAssistantId(loginAccount, query == null ? null : query.getAssistantId());
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建基础查询上下文。
|
||||||
|
*
|
||||||
|
* @param loginAccount 当前登录账号
|
||||||
|
* @param range 时间范围
|
||||||
|
* @param startDate 开始日期
|
||||||
|
* @param endDate 结束日期
|
||||||
|
* @return 查询上下文
|
||||||
|
*/
|
||||||
|
private DashboardQueryContext buildContext(LoginAccount loginAccount,
|
||||||
|
String range,
|
||||||
|
String startDate,
|
||||||
|
String endDate) {
|
||||||
|
DashboardQueryContext context = new DashboardQueryContext();
|
||||||
|
context.range = normalizeRange(range);
|
||||||
|
context.superAdmin = isSuperAdmin(loginAccount);
|
||||||
|
|
||||||
|
LocalDate today = LocalDate.now(DEFAULT_ZONE_ID);
|
||||||
|
if ("today".equals(context.range)) {
|
||||||
|
context.startTime = LocalDateTime.of(today, LocalTime.MIN);
|
||||||
|
context.endTime = context.startTime.plusDays(1);
|
||||||
|
context.queryStartDate = today.toString();
|
||||||
|
context.queryEndDate = today.toString();
|
||||||
|
} else if ("7d".equals(context.range)) {
|
||||||
|
context.startTime = LocalDateTime.of(today.minusDays(6), LocalTime.MIN);
|
||||||
|
context.endTime = LocalDateTime.of(today.plusDays(1), LocalTime.MIN);
|
||||||
|
context.queryStartDate = today.minusDays(6).toString();
|
||||||
|
context.queryEndDate = today.toString();
|
||||||
|
} else if ("30d".equals(context.range)) {
|
||||||
|
context.startTime = LocalDateTime.of(today.minusDays(29), LocalTime.MIN);
|
||||||
|
context.endTime = LocalDateTime.of(today.plusDays(1), LocalTime.MIN);
|
||||||
|
context.queryStartDate = today.minusDays(29).toString();
|
||||||
|
context.queryEndDate = today.toString();
|
||||||
|
} else {
|
||||||
|
LocalDate customStartDate = parseRequiredDate(startDate, "开始日期不能为空");
|
||||||
|
LocalDate customEndDate = parseRequiredDate(endDate, "结束日期不能为空");
|
||||||
|
if (customStartDate.isAfter(customEndDate)) {
|
||||||
|
throw new BusinessException("开始日期不能晚于结束日期");
|
||||||
|
}
|
||||||
|
context.startTime = LocalDateTime.of(customStartDate, LocalTime.MIN);
|
||||||
|
context.endTime = LocalDateTime.of(customEndDate.plusDays(1), LocalTime.MIN);
|
||||||
|
context.queryStartDate = customStartDate.toString();
|
||||||
|
context.queryEndDate = customEndDate.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
context.tenantFilterId = context.superAdmin || loginAccount == null ? null : loginAccount.getTenantId();
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断当前登录账号是否为超管。
|
||||||
|
*
|
||||||
|
* @param loginAccount 当前登录账号
|
||||||
|
* @return true 表示超管
|
||||||
|
*/
|
||||||
|
private boolean isSuperAdmin(LoginAccount loginAccount) {
|
||||||
|
if (loginAccount == null || loginAccount.getId() == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
QueryWrapper roleMappingWrapper = QueryWrapper.create();
|
||||||
|
roleMappingWrapper.eq(SysAccountRole::getAccountId, loginAccount.getId());
|
||||||
|
List<BigInteger> roleIds = sysAccountRoleService.list(roleMappingWrapper)
|
||||||
|
.stream()
|
||||||
|
.map(SysAccountRole::getRoleId)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
if (roleIds.isEmpty()) {
|
||||||
|
return Constants.SUPER_ADMIN_ID.equals(loginAccount.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryWrapper roleWrapper = QueryWrapper.create();
|
||||||
|
roleWrapper.in(SysRole::getId, roleIds);
|
||||||
|
roleWrapper.eq(SysRole::getRoleKey, Constants.SUPER_ADMIN_ROLE_CODE);
|
||||||
|
return sysRoleService.count(roleWrapper) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 归一化时间范围参数。
|
||||||
|
*
|
||||||
|
* @param range 原始时间范围
|
||||||
|
* @return 规范化后的时间范围
|
||||||
|
*/
|
||||||
|
private String normalizeRange(String range) {
|
||||||
|
if (!StringUtils.hasText(range)) {
|
||||||
|
return "7d";
|
||||||
|
}
|
||||||
|
if ("today".equals(range) || "7d".equals(range) || "30d".equals(range) || "custom".equals(range)) {
|
||||||
|
return range;
|
||||||
|
}
|
||||||
|
throw new BusinessException("不支持的时间范围: " + range);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建时间桶。
|
||||||
|
*
|
||||||
|
* @param range 时间范围
|
||||||
|
* @return 时间桶列表
|
||||||
|
*/
|
||||||
|
private List<TimeBucket> buildBuckets(String range, LocalDate customStartDate, LocalDate customEndDate) {
|
||||||
|
List<TimeBucket> buckets = new ArrayList<>();
|
||||||
|
LocalDate today = LocalDate.now(DEFAULT_ZONE_ID);
|
||||||
|
boolean hourlyBucket = "today".equals(range)
|
||||||
|
|| ("custom".equals(range)
|
||||||
|
&& customStartDate != null
|
||||||
|
&& customEndDate != null
|
||||||
|
&& customStartDate.equals(customEndDate));
|
||||||
|
if (hourlyBucket) {
|
||||||
|
DateTimeFormatter keyFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:00:00");
|
||||||
|
DateTimeFormatter labelFormatter = DateTimeFormatter.ofPattern("HH:00");
|
||||||
|
LocalDate bucketDate = "today".equals(range) ? today : customStartDate;
|
||||||
|
LocalDateTime start = LocalDateTime.of(bucketDate, LocalTime.MIN);
|
||||||
|
for (int hour = 0; hour < 24; hour++) {
|
||||||
|
LocalDateTime current = start.plusHours(hour);
|
||||||
|
buckets.add(new TimeBucket(current.format(keyFormatter), current.format(labelFormatter)));
|
||||||
|
}
|
||||||
|
return buckets;
|
||||||
|
}
|
||||||
|
|
||||||
|
DateTimeFormatter keyFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||||
|
DateTimeFormatter labelFormatter = DateTimeFormatter.ofPattern("MM-dd");
|
||||||
|
int days;
|
||||||
|
LocalDate start;
|
||||||
|
if ("7d".equals(range)) {
|
||||||
|
days = 7;
|
||||||
|
start = today.minusDays(6);
|
||||||
|
} else if ("30d".equals(range)) {
|
||||||
|
days = 30;
|
||||||
|
start = today.minusDays(29);
|
||||||
|
} else {
|
||||||
|
start = customStartDate;
|
||||||
|
days = (int) java.time.temporal.ChronoUnit.DAYS.between(customStartDate, customEndDate) + 1;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < days; i++) {
|
||||||
|
LocalDate current = start.plusDays(i);
|
||||||
|
buckets.add(new TimeBucket(current.format(keyFormatter), current.format(labelFormatter)));
|
||||||
|
}
|
||||||
|
return buckets;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前上下文是否按小时构建趋势。
|
||||||
|
*
|
||||||
|
* @param context 查询上下文
|
||||||
|
* @return true 表示按小时
|
||||||
|
*/
|
||||||
|
private boolean useHourlyBuckets(DashboardQueryContext context) {
|
||||||
|
return "today".equals(context.range)
|
||||||
|
|| ("custom".equals(context.range)
|
||||||
|
&& context.startTime != null
|
||||||
|
&& context.endTime != null
|
||||||
|
&& context.startTime.toLocalDate().equals(context.endTime.toLocalDate().minusDays(1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析必填日期参数。
|
||||||
|
*
|
||||||
|
* @param dateText 日期文本
|
||||||
|
* @param errorMessage 错误信息
|
||||||
|
* @return 日期
|
||||||
|
*/
|
||||||
|
private LocalDate parseRequiredDate(String dateText, String errorMessage) {
|
||||||
|
if (!StringUtils.hasText(dateText)) {
|
||||||
|
throw new BusinessException(errorMessage);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return LocalDate.parse(dateText.trim());
|
||||||
|
} catch (Exception ex) {
|
||||||
|
throw new BusinessException("日期格式不正确: " + dateText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 把 LocalDateTime 转换为 Date。
|
||||||
|
*
|
||||||
|
* @param dateTime 时间
|
||||||
|
* @return Date
|
||||||
|
*/
|
||||||
|
private Date toDate(LocalDateTime dateTime) {
|
||||||
|
return Date.from(dateTime.atZone(DEFAULT_ZONE_ID).toInstant());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析对象为 long 值。
|
||||||
|
*
|
||||||
|
* @param value 原始对象
|
||||||
|
* @return long 值
|
||||||
|
*/
|
||||||
|
private long asLong(Object value) {
|
||||||
|
if (value == null) {
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
|
if (value instanceof Number) {
|
||||||
|
return ((Number) value).longValue();
|
||||||
|
}
|
||||||
|
return Long.parseLong(String.valueOf(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算平均每会话消息数。
|
||||||
|
*
|
||||||
|
* @param messageTotal 消息总数
|
||||||
|
* @param sessionTotal 会话总数
|
||||||
|
* @return 平均值
|
||||||
|
*/
|
||||||
|
private double calculateAvg(long messageTotal, long sessionTotal) {
|
||||||
|
if (sessionTotal <= 0) {
|
||||||
|
return 0D;
|
||||||
|
}
|
||||||
|
return (double) messageTotal / (double) sessionTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量解析用户展示名称映射。
|
||||||
|
*
|
||||||
|
* @param ranks 活跃排行
|
||||||
|
* @return 名称映射
|
||||||
|
*/
|
||||||
|
private Map<BigInteger, AccountIdentitySnapshot> resolveAccountIdentityMap(List<ChatActiveUserRank> ranks) {
|
||||||
|
List<BigInteger> userIds = ranks.stream()
|
||||||
|
.map(ChatActiveUserRank::userId)
|
||||||
|
.filter(java.util.Objects::nonNull)
|
||||||
|
.distinct()
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
if (userIds.isEmpty()) {
|
||||||
|
return new HashMap<>();
|
||||||
|
}
|
||||||
|
QueryWrapper queryWrapper = QueryWrapper.create()
|
||||||
|
.select(SysAccount::getId, SysAccount::getLoginName, SysAccount::getNickname)
|
||||||
|
.in(SysAccount::getId, userIds);
|
||||||
|
List<SysAccount> accounts = sysAccountService.list(queryWrapper);
|
||||||
|
Map<BigInteger, AccountIdentitySnapshot> identityMap = new HashMap<>(userIds.size());
|
||||||
|
if (accounts == null) {
|
||||||
|
return identityMap;
|
||||||
|
}
|
||||||
|
for (SysAccount account : accounts) {
|
||||||
|
if (account == null || account.getId() == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
identityMap.put(
|
||||||
|
account.getId(),
|
||||||
|
new AccountIdentitySnapshot(trimToNull(account.getLoginName()), trimToNull(account.getNickname()))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return identityMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析智能体展示名称。
|
||||||
|
*
|
||||||
|
* @param assistantId 智能体 ID
|
||||||
|
* @param assistantName 智能体名称
|
||||||
|
* @return 展示名称
|
||||||
|
*/
|
||||||
|
private String resolveAssistantLabel(BigInteger assistantId, String assistantName) {
|
||||||
|
if (StringUtils.hasText(assistantName)) {
|
||||||
|
return assistantName.trim();
|
||||||
|
}
|
||||||
|
return assistantId == null ? "智能体-未知" : "智能体-" + assistantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析用户展示名称。
|
||||||
|
*
|
||||||
|
* @param userId 用户 ID
|
||||||
|
* @param userAccount 聊天侧账号快照
|
||||||
|
* @param snapshot 系统账号快照
|
||||||
|
* @return 用户身份
|
||||||
|
*/
|
||||||
|
private ResolvedUserIdentity resolveUserIdentity(BigInteger userId,
|
||||||
|
String userAccount,
|
||||||
|
AccountIdentitySnapshot snapshot) {
|
||||||
|
String loginName = snapshot == null ? null : snapshot.loginName;
|
||||||
|
String nickname = snapshot == null ? null : snapshot.nickname;
|
||||||
|
String trimmedUserAccount = trimToNull(userAccount);
|
||||||
|
if (StringUtils.hasText(loginName)) {
|
||||||
|
if (StringUtils.hasText(nickname)) {
|
||||||
|
return new ResolvedUserIdentity(loginName, nickname, loginName + "(" + nickname + ")");
|
||||||
|
}
|
||||||
|
return new ResolvedUserIdentity(loginName, nickname, loginName);
|
||||||
|
}
|
||||||
|
if (StringUtils.hasText(trimmedUserAccount)) {
|
||||||
|
return new ResolvedUserIdentity(trimmedUserAccount, nickname, trimmedUserAccount);
|
||||||
|
}
|
||||||
|
return new ResolvedUserIdentity(null, nickname, userId == null ? "用户-未知" : "用户-" + userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验智能体筛选条件。
|
||||||
|
*
|
||||||
|
* @param loginAccount 当前登录账号
|
||||||
|
* @param assistantId 智能体 ID
|
||||||
|
* @return 规范化后的智能体 ID
|
||||||
|
*/
|
||||||
|
private BigInteger validateAssistantId(LoginAccount loginAccount, BigInteger assistantId) {
|
||||||
|
if (assistantId == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Bot bot = botService.getById(assistantId);
|
||||||
|
if (bot == null || !Integer.valueOf(1).equals(bot.getStatus())) {
|
||||||
|
throw new BusinessException("聊天助手不存在或未启用");
|
||||||
|
}
|
||||||
|
boolean visible = categoryPermissionService.canAccessCategory(
|
||||||
|
loginAccount,
|
||||||
|
"BOT",
|
||||||
|
bot.getCreatedBy(),
|
||||||
|
bot.getCategoryId()
|
||||||
|
);
|
||||||
|
if (!visible) {
|
||||||
|
throw new BusinessException("聊天助手不存在或未启用");
|
||||||
|
}
|
||||||
|
return assistantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 归一化文本。
|
||||||
|
*
|
||||||
|
* @param value 原始文本
|
||||||
|
* @return 去空白后的文本
|
||||||
|
*/
|
||||||
|
private String trimToNull(String value) {
|
||||||
|
if (!StringUtils.hasText(value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 把空文本转为空串。
|
||||||
|
*
|
||||||
|
* @param value 原始文本
|
||||||
|
* @return 输出文本
|
||||||
|
*/
|
||||||
|
private String defaultIfBlank(String value) {
|
||||||
|
return StringUtils.hasText(value) ? value.trim() : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作台查询上下文。
|
||||||
|
*/
|
||||||
|
private static class DashboardQueryContext {
|
||||||
|
private String range;
|
||||||
|
private BigInteger tenantFilterId;
|
||||||
|
private BigInteger deptFilterId;
|
||||||
|
private BigInteger assistantId;
|
||||||
|
private boolean superAdmin;
|
||||||
|
private LocalDateTime startTime;
|
||||||
|
private LocalDateTime endTime;
|
||||||
|
private String queryStartDate;
|
||||||
|
private String queryEndDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 时间桶。
|
||||||
|
*/
|
||||||
|
private static class TimeBucket {
|
||||||
|
private final String key;
|
||||||
|
private final String label;
|
||||||
|
|
||||||
|
private TimeBucket(String key, String label) {
|
||||||
|
this.key = key;
|
||||||
|
this.label = label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 系统账号身份快照。
|
||||||
|
*/
|
||||||
|
private static class AccountIdentitySnapshot {
|
||||||
|
private final String loginName;
|
||||||
|
private final String nickname;
|
||||||
|
|
||||||
|
private AccountIdentitySnapshot(String loginName, String nickname) {
|
||||||
|
this.loginName = loginName;
|
||||||
|
this.nickname = nickname;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户展示身份。
|
||||||
|
*/
|
||||||
|
private static class ResolvedUserIdentity {
|
||||||
|
private final String loginName;
|
||||||
|
private final String nickname;
|
||||||
|
private final String label;
|
||||||
|
|
||||||
|
private ResolvedUserIdentity(String loginName, String nickname, String label) {
|
||||||
|
this.loginName = loginName;
|
||||||
|
this.nickname = nickname;
|
||||||
|
this.label = label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 聊天统计页面载荷。
|
||||||
|
*/
|
||||||
|
private record ChatDashboardPayload(
|
||||||
|
DashboardChatStatusVo chatStatus,
|
||||||
|
List<DashboardTrendItemVo> trends,
|
||||||
|
List<DashboardAssistantTrendSeriesVo> assistantTrends,
|
||||||
|
List<DashboardDistributionItemVo> distribution
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
package tech.easyflow.admin.controller.ai;
|
||||||
|
|
||||||
|
import com.mybatisflex.core.paginate.Page;
|
||||||
|
import com.mybatisflex.core.query.QueryWrapper;
|
||||||
|
import org.testng.Assert;
|
||||||
|
import org.testng.annotations.BeforeMethod;
|
||||||
|
import org.testng.annotations.Test;
|
||||||
|
import tech.easyflow.admin.controller.ai.support.AiResourceCreatorNameSupport;
|
||||||
|
import tech.easyflow.ai.entity.Bot;
|
||||||
|
import tech.easyflow.ai.service.AiResourceApprovalStateService;
|
||||||
|
import tech.easyflow.ai.service.BotDocumentCollectionService;
|
||||||
|
import tech.easyflow.ai.service.BotMessageService;
|
||||||
|
import tech.easyflow.ai.service.BotService;
|
||||||
|
import tech.easyflow.ai.service.BotWorkflowService;
|
||||||
|
import tech.easyflow.ai.service.ModelService;
|
||||||
|
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
|
||||||
|
import tech.easyflow.system.service.CategoryPermissionService;
|
||||||
|
import tech.easyflow.system.service.SysAccountService;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.doNothing;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link BotController} 测试。
|
||||||
|
*/
|
||||||
|
public class BotControllerTest {
|
||||||
|
|
||||||
|
private BotService botService;
|
||||||
|
private ModelService modelService;
|
||||||
|
private BotWorkflowService botWorkflowService;
|
||||||
|
private BotDocumentCollectionService botDocumentCollectionService;
|
||||||
|
private BotMessageService botMessageService;
|
||||||
|
private CategoryPermissionService categoryPermissionService;
|
||||||
|
private AiResourceApprovalStateService aiResourceApprovalStateService;
|
||||||
|
private SysAccountService sysAccountService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化测试 mock。
|
||||||
|
*/
|
||||||
|
@BeforeMethod
|
||||||
|
public void setUp() {
|
||||||
|
botService = mock(BotService.class);
|
||||||
|
modelService = mock(ModelService.class);
|
||||||
|
botWorkflowService = mock(BotWorkflowService.class);
|
||||||
|
botDocumentCollectionService = mock(BotDocumentCollectionService.class);
|
||||||
|
botMessageService = mock(BotMessageService.class);
|
||||||
|
categoryPermissionService = mock(CategoryPermissionService.class);
|
||||||
|
aiResourceApprovalStateService = mock(AiResourceApprovalStateService.class);
|
||||||
|
sysAccountService = mock(SysAccountService.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证分页结果会补充创建人展示名称,且只做一次批量账号查询。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void shouldFillCreatedByNameForPageRecords() {
|
||||||
|
TestBotController controller = new TestBotController(
|
||||||
|
botService,
|
||||||
|
modelService,
|
||||||
|
botWorkflowService,
|
||||||
|
botDocumentCollectionService,
|
||||||
|
botMessageService
|
||||||
|
);
|
||||||
|
AiResourceCreatorNameSupport creatorNameSupport = new AiResourceCreatorNameSupport();
|
||||||
|
setField(creatorNameSupport, "sysAccountService", sysAccountService);
|
||||||
|
setField(controller, "categoryPermissionService", categoryPermissionService);
|
||||||
|
setField(controller, "aiResourceApprovalStateService", aiResourceApprovalStateService);
|
||||||
|
setField(controller, "aiResourceCreatorNameSupport", creatorNameSupport);
|
||||||
|
|
||||||
|
Bot bot = new Bot();
|
||||||
|
bot.setId(BigInteger.valueOf(101));
|
||||||
|
bot.setCreatedBy(BigInteger.valueOf(7));
|
||||||
|
Page<Bot> page = new Page<>(Collections.singletonList(bot), 1, 10, 1);
|
||||||
|
|
||||||
|
when(categoryPermissionService.getCurrentAccess("BOT"))
|
||||||
|
.thenReturn(new RoleCategoryAccessSnapshot("BOT", BigInteger.ONE, false, true, Collections.emptySet()));
|
||||||
|
when(botService.page(any(Page.class), any(QueryWrapper.class))).thenReturn(page);
|
||||||
|
when(sysAccountService.resolveDisplayNameMap(Collections.singleton(BigInteger.valueOf(7))))
|
||||||
|
.thenReturn(Map.of(BigInteger.valueOf(7), "管理员"));
|
||||||
|
doNothing().when(aiResourceApprovalStateService).fillBotApprovalState(page.getRecords());
|
||||||
|
|
||||||
|
Page<Bot> result = controller.invokeQueryPage(new Page<>(1, 10), QueryWrapper.create());
|
||||||
|
|
||||||
|
Assert.assertEquals(result.getRecords().get(0).getCreatedByName(), "管理员");
|
||||||
|
verify(sysAccountService).resolveDisplayNameMap(Collections.singleton(BigInteger.valueOf(7)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过反射设置字段值。
|
||||||
|
*
|
||||||
|
* @param target 目标对象
|
||||||
|
* @param fieldName 字段名
|
||||||
|
* @param value 字段值
|
||||||
|
*/
|
||||||
|
private static void setField(Object target, String fieldName, Object value) {
|
||||||
|
Class<?> current = target.getClass();
|
||||||
|
while (current != null) {
|
||||||
|
try {
|
||||||
|
java.lang.reflect.Field field = current.getDeclaredField(fieldName);
|
||||||
|
field.setAccessible(true);
|
||||||
|
field.set(target, value);
|
||||||
|
return;
|
||||||
|
} catch (NoSuchFieldException ignored) {
|
||||||
|
current = current.getSuperclass();
|
||||||
|
} catch (IllegalAccessException e) {
|
||||||
|
throw new IllegalStateException("设置测试字段失败: " + fieldName, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException("未找到字段: " + fieldName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 暴露受保护分页方法的测试控制器。
|
||||||
|
*/
|
||||||
|
private static class TestBotController extends BotController {
|
||||||
|
|
||||||
|
TestBotController(
|
||||||
|
BotService service,
|
||||||
|
ModelService modelService,
|
||||||
|
BotWorkflowService botWorkflowService,
|
||||||
|
BotDocumentCollectionService botDocumentCollectionService,
|
||||||
|
BotMessageService botMessageService
|
||||||
|
) {
|
||||||
|
super(service, modelService, botWorkflowService, botDocumentCollectionService, botMessageService);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调用受保护的分页查询方法。
|
||||||
|
*
|
||||||
|
* @param page 分页对象
|
||||||
|
* @param queryWrapper 查询条件
|
||||||
|
* @return 查询结果
|
||||||
|
*/
|
||||||
|
Page<Bot> invokeQueryPage(Page<Bot> page, QueryWrapper queryWrapper) {
|
||||||
|
return super.queryPage(page, queryWrapper);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,626 @@
|
|||||||
|
package tech.easyflow.admin.service.dashboard.impl;
|
||||||
|
|
||||||
|
import com.mybatisflex.core.query.QueryWrapper;
|
||||||
|
import org.apache.poi.ss.usermodel.WorkbookFactory;
|
||||||
|
import org.testng.Assert;
|
||||||
|
import org.testng.annotations.Test;
|
||||||
|
import tech.easyflow.ai.entity.Bot;
|
||||||
|
import tech.easyflow.ai.service.BotService;
|
||||||
|
import tech.easyflow.admin.model.dashboard.DashboardAssistantTrendSeriesVo;
|
||||||
|
import tech.easyflow.admin.model.dashboard.DashboardDistributionItemVo;
|
||||||
|
import tech.easyflow.admin.model.dashboard.DashboardOverviewQuery;
|
||||||
|
import tech.easyflow.admin.model.dashboard.DashboardSummaryVo;
|
||||||
|
import tech.easyflow.admin.model.dashboard.DashboardTrendItemVo;
|
||||||
|
import tech.easyflow.admin.model.dashboard.DashboardUserRankItemVo;
|
||||||
|
import tech.easyflow.admin.model.dashboard.DashboardUserRankQuery;
|
||||||
|
import tech.easyflow.chatlog.domain.dto.ChatActiveUserRank;
|
||||||
|
import tech.easyflow.chatlog.domain.dto.ChatAssistantSessionTrend;
|
||||||
|
import tech.easyflow.chatlog.domain.dto.ChatAssistantUsageRank;
|
||||||
|
import tech.easyflow.chatlog.domain.dto.ChatDashboardSummary;
|
||||||
|
import tech.easyflow.chatlog.domain.dto.ChatDashboardTrend;
|
||||||
|
import tech.easyflow.chatlog.service.ChatDashboardQueryService;
|
||||||
|
import tech.easyflow.common.entity.LoginAccount;
|
||||||
|
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||||
|
import tech.easyflow.system.entity.SysAccount;
|
||||||
|
import tech.easyflow.system.service.CategoryPermissionService;
|
||||||
|
import tech.easyflow.system.service.SysAccountService;
|
||||||
|
import tech.easyflow.system.service.SysAccountRoleService;
|
||||||
|
import tech.easyflow.system.service.SysRoleService;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.lang.reflect.Constructor;
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.LocalTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.ArgumentMatchers.isNull;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link DashboardServiceImpl} 测试。
|
||||||
|
*/
|
||||||
|
public class DashboardServiceImplTest {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证分析库不可用时返回明确不可用状态,且趋势与排行为空。
|
||||||
|
*
|
||||||
|
* @throws Exception 反射调用失败
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void shouldReturnUnavailableChatPayloadWhenAnalyticalDbIsDisabled() throws Exception {
|
||||||
|
DashboardServiceImpl service = new DashboardServiceImpl();
|
||||||
|
ChatDashboardQueryService chatDashboardQueryService = mock(ChatDashboardQueryService.class);
|
||||||
|
when(chatDashboardQueryService.available()).thenReturn(false);
|
||||||
|
setField(service, "chatDashboardQueryService", chatDashboardQueryService);
|
||||||
|
|
||||||
|
Object context = newContext("7d", null);
|
||||||
|
DashboardSummaryVo summary = new DashboardSummaryVo();
|
||||||
|
Object payload = invokeBuildChatPayload(service, context, summary);
|
||||||
|
|
||||||
|
Object chatStatus = readField(payload, "chatStatus");
|
||||||
|
List<?> trends = (List<?>) readField(payload, "trends");
|
||||||
|
List<?> distribution = (List<?>) readField(payload, "distribution");
|
||||||
|
|
||||||
|
Assert.assertFalse(Boolean.TRUE.equals(invokeGetter(chatStatus, "getAvailable")));
|
||||||
|
Assert.assertEquals(invokeGetter(chatStatus, "getMessage"), "聊天数据不可用");
|
||||||
|
Assert.assertTrue(trends.isEmpty());
|
||||||
|
Assert.assertTrue(distribution.isEmpty());
|
||||||
|
Assert.assertEquals(summary.getChatMessageTotal(), Long.valueOf(0L));
|
||||||
|
Assert.assertEquals(summary.getChatSessionTotal(), Long.valueOf(0L));
|
||||||
|
Assert.assertEquals(summary.getActiveAssistantTotal(), Long.valueOf(0L));
|
||||||
|
Assert.assertEquals(summary.getChatActiveUserTotal(), Long.valueOf(0L));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证 today 返回 24 个小时点位,且 overview 不再触发用户榜查询。
|
||||||
|
*
|
||||||
|
* @throws Exception 反射调用失败
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public void shouldBuildHourlyTrendForToday() throws Exception {
|
||||||
|
DashboardServiceImpl service = new DashboardServiceImpl();
|
||||||
|
ChatDashboardQueryService chatDashboardQueryService = mock(ChatDashboardQueryService.class);
|
||||||
|
String currentHourKey = LocalDateTime.of(LocalDate.now(), LocalTime.of(10, 0))
|
||||||
|
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:00:00"));
|
||||||
|
when(chatDashboardQueryService.available()).thenReturn(true);
|
||||||
|
when(chatDashboardQueryService.querySummary(any(), any(), any()))
|
||||||
|
.thenReturn(new ChatDashboardSummary(3L, 9L, 1L, 2L));
|
||||||
|
when(chatDashboardQueryService.queryHourlyTrends(any(), any(), any()))
|
||||||
|
.thenReturn(List.of(new ChatDashboardTrend(currentHourKey, 3L, 9L, 2L)));
|
||||||
|
when(chatDashboardQueryService.queryAssistantUsageRanks(any(), any(), any(), any(Integer.class)))
|
||||||
|
.thenReturn(List.of(new ChatAssistantUsageRank(BigInteger.ONE, "", 2L, 3L, 9L)));
|
||||||
|
when(chatDashboardQueryService.queryAssistantHourlyTrends(any(), any(), any(), any()))
|
||||||
|
.thenReturn(List.of(new ChatAssistantSessionTrend(BigInteger.ONE, "", currentHourKey, 3L)));
|
||||||
|
setField(service, "chatDashboardQueryService", chatDashboardQueryService);
|
||||||
|
|
||||||
|
Object context = newContext("today", BigInteger.valueOf(9));
|
||||||
|
DashboardSummaryVo summary = new DashboardSummaryVo();
|
||||||
|
Object payload = invokeBuildChatPayload(service, context, summary);
|
||||||
|
|
||||||
|
Object chatStatus = readField(payload, "chatStatus");
|
||||||
|
List<DashboardTrendItemVo> trends = (List<DashboardTrendItemVo>) readField(payload, "trends");
|
||||||
|
List<DashboardAssistantTrendSeriesVo> assistantTrends =
|
||||||
|
(List<DashboardAssistantTrendSeriesVo>) readField(payload, "assistantTrends");
|
||||||
|
List<DashboardDistributionItemVo> distribution = (List<DashboardDistributionItemVo>) readField(payload, "distribution");
|
||||||
|
|
||||||
|
Assert.assertTrue(Boolean.TRUE.equals(invokeGetter(chatStatus, "getAvailable")));
|
||||||
|
Assert.assertEquals(trends.size(), 24);
|
||||||
|
Assert.assertEquals(trends.get(0).getLabel(), "00:00");
|
||||||
|
Assert.assertEquals(trends.get(10).getKey(), currentHourKey);
|
||||||
|
Assert.assertEquals(trends.get(10).getLabel(), "10:00");
|
||||||
|
Assert.assertEquals(trends.get(10).getChatMessageTotal(), Long.valueOf(9L));
|
||||||
|
Assert.assertEquals(trends.get(10).getChatSessionTotal(), Long.valueOf(3L));
|
||||||
|
Assert.assertEquals(trends.get(10).getActiveUserTotal(), Long.valueOf(2L));
|
||||||
|
Assert.assertEquals(trends.get(11).getChatMessageTotal(), Long.valueOf(0L));
|
||||||
|
Assert.assertEquals(trends.get(11).getChatSessionTotal(), Long.valueOf(0L));
|
||||||
|
Assert.assertEquals(summary.getChatMessageTotal(), Long.valueOf(9L));
|
||||||
|
Assert.assertEquals(summary.getChatSessionTotal(), Long.valueOf(3L));
|
||||||
|
Assert.assertEquals(summary.getActiveAssistantTotal(), Long.valueOf(1L));
|
||||||
|
Assert.assertEquals(summary.getChatActiveUserTotal(), Long.valueOf(2L));
|
||||||
|
Assert.assertEquals(assistantTrends.size(), 1);
|
||||||
|
Assert.assertEquals(assistantTrends.get(0).getLabel(), "智能体-1");
|
||||||
|
Assert.assertEquals(assistantTrends.get(0).getTotalSessionCount(), Long.valueOf(3L));
|
||||||
|
Assert.assertEquals(assistantTrends.get(0).getPoints().size(), 24);
|
||||||
|
Assert.assertEquals(assistantTrends.get(0).getPoints().get(10).getSessionTotal(), Long.valueOf(3L));
|
||||||
|
Assert.assertEquals(assistantTrends.get(0).getPoints().get(11).getSessionTotal(), Long.valueOf(0L));
|
||||||
|
Assert.assertEquals(distribution.get(0).getLabel(), "智能体-1");
|
||||||
|
Assert.assertEquals(distribution.get(0).getAvgMessagePerSession(), Double.valueOf(3D));
|
||||||
|
Assert.assertEquals(distribution.get(0).getAvgSessionPerUser(), Double.valueOf(1.5D));
|
||||||
|
verify(chatDashboardQueryService, never()).queryActiveUserRanks(any(), any(), any(), any(), any());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证当系统账号名称仅回退为纯 ID 时,仍优先继续回退到聊天侧账号。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void shouldFallbackToUserAccountWhenSystemDisplayNameIsOnlyId() throws Exception {
|
||||||
|
DashboardServiceImpl service = new DashboardServiceImpl();
|
||||||
|
ChatDashboardQueryService chatDashboardQueryService = mock(ChatDashboardQueryService.class);
|
||||||
|
when(chatDashboardQueryService.available()).thenReturn(true);
|
||||||
|
when(chatDashboardQueryService.queryActiveUserRanks(any(), any(), any(), any(), any()))
|
||||||
|
.thenReturn(List.of(new ChatActiveUserRank(BigInteger.valueOf(9), "chat-user", 1L, 1L, 1L)));
|
||||||
|
SysAccountService sysAccountService = mock(SysAccountService.class);
|
||||||
|
when(sysAccountService.list(any(QueryWrapper.class))).thenReturn(List.of(buildSysAccount(9L, "", "仅昵称")));
|
||||||
|
setField(service, "chatDashboardQueryService", chatDashboardQueryService);
|
||||||
|
setField(service, "sysAccountService", sysAccountService);
|
||||||
|
|
||||||
|
DashboardUserRankQuery query = new DashboardUserRankQuery();
|
||||||
|
query.setRange("7d");
|
||||||
|
|
||||||
|
List<DashboardUserRankItemVo> userRanks = service.getUserRanks(new LoginAccount(), query);
|
||||||
|
Assert.assertEquals(userRanks.get(0).getLabel(), "chat-user");
|
||||||
|
Assert.assertEquals(userRanks.get(0).getLoginName(), "chat-user");
|
||||||
|
Assert.assertEquals(userRanks.get(0).getNickname(), "仅昵称");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证日趋势会保留 assistantId 为空的排行项,并补齐 7 天点位。
|
||||||
|
*
|
||||||
|
* @throws Exception 反射调用失败
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public void shouldBuildDailyAssistantTrendSeriesForRankedAssistants() throws Exception {
|
||||||
|
DashboardServiceImpl service = new DashboardServiceImpl();
|
||||||
|
ChatDashboardQueryService chatDashboardQueryService = mock(ChatDashboardQueryService.class);
|
||||||
|
when(chatDashboardQueryService.available()).thenReturn(true);
|
||||||
|
when(chatDashboardQueryService.querySummary(any(), any(), any()))
|
||||||
|
.thenReturn(new ChatDashboardSummary(4L, 12L, 2L, 3L));
|
||||||
|
when(chatDashboardQueryService.queryTrends(any(), any(), any()))
|
||||||
|
.thenReturn(List.of());
|
||||||
|
when(chatDashboardQueryService.queryAssistantUsageRanks(any(), any(), any(), any(Integer.class)))
|
||||||
|
.thenReturn(List.of(
|
||||||
|
new ChatAssistantUsageRank(BigInteger.ONE, "助手-A", 3L, 4L, 12L),
|
||||||
|
new ChatAssistantUsageRank(null, "未知助手", 1L, 2L, 4L)
|
||||||
|
));
|
||||||
|
when(chatDashboardQueryService.queryAssistantTrends(any(), any(), any(), any()))
|
||||||
|
.thenReturn(List.of(
|
||||||
|
new ChatAssistantSessionTrend(BigInteger.ONE, "助手-A", LocalDate.now().minusDays(6).toString(), 2L),
|
||||||
|
new ChatAssistantSessionTrend(BigInteger.ONE, "助手-A", LocalDate.now().toString(), 4L),
|
||||||
|
new ChatAssistantSessionTrend(null, "未知助手", LocalDate.now().minusDays(3).toString(), 2L)
|
||||||
|
));
|
||||||
|
setField(service, "chatDashboardQueryService", chatDashboardQueryService);
|
||||||
|
|
||||||
|
Object context = newContext("7d", BigInteger.ONE);
|
||||||
|
DashboardSummaryVo summary = new DashboardSummaryVo();
|
||||||
|
Object payload = invokeBuildChatPayload(service, context, summary);
|
||||||
|
|
||||||
|
List<DashboardAssistantTrendSeriesVo> assistantTrends =
|
||||||
|
(List<DashboardAssistantTrendSeriesVo>) readField(payload, "assistantTrends");
|
||||||
|
Assert.assertEquals(assistantTrends.size(), 2);
|
||||||
|
Assert.assertEquals(assistantTrends.get(0).getLabel(), "助手-A");
|
||||||
|
Assert.assertEquals(assistantTrends.get(0).getPoints().size(), 7);
|
||||||
|
Assert.assertEquals(assistantTrends.get(0).getPoints().get(0).getSessionTotal(), Long.valueOf(2L));
|
||||||
|
Assert.assertEquals(assistantTrends.get(0).getPoints().get(6).getSessionTotal(), Long.valueOf(4L));
|
||||||
|
Assert.assertEquals(assistantTrends.get(0).getPoints().get(1).getSessionTotal(), Long.valueOf(0L));
|
||||||
|
Assert.assertNull(assistantTrends.get(1).getAssistantId());
|
||||||
|
Assert.assertEquals(assistantTrends.get(1).getLabel(), "未知助手");
|
||||||
|
Assert.assertEquals(assistantTrends.get(1).getPoints().get(3).getSessionTotal(), Long.valueOf(2L));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证自定义单天范围按小时桶构建。
|
||||||
|
*
|
||||||
|
* @throws Exception 反射调用失败
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public void shouldBuildHourlyTrendForCustomSingleDayRange() throws Exception {
|
||||||
|
DashboardServiceImpl service = new DashboardServiceImpl();
|
||||||
|
ChatDashboardQueryService chatDashboardQueryService = mock(ChatDashboardQueryService.class);
|
||||||
|
LocalDate customDate = LocalDate.now().minusDays(2);
|
||||||
|
String currentHourKey = LocalDateTime.of(customDate, LocalTime.of(8, 0))
|
||||||
|
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:00:00"));
|
||||||
|
when(chatDashboardQueryService.available()).thenReturn(true);
|
||||||
|
when(chatDashboardQueryService.querySummary(any(), any(), any()))
|
||||||
|
.thenReturn(new ChatDashboardSummary(2L, 6L, 1L, 1L));
|
||||||
|
when(chatDashboardQueryService.queryHourlyTrends(any(), any(), any()))
|
||||||
|
.thenReturn(List.of(new ChatDashboardTrend(currentHourKey, 2L, 6L, 1L)));
|
||||||
|
when(chatDashboardQueryService.queryAssistantUsageRanks(any(), any(), any(), any(Integer.class)))
|
||||||
|
.thenReturn(List.of());
|
||||||
|
setField(service, "chatDashboardQueryService", chatDashboardQueryService);
|
||||||
|
|
||||||
|
Object context = newContext(
|
||||||
|
"custom",
|
||||||
|
BigInteger.ONE,
|
||||||
|
LocalDateTime.of(customDate, LocalTime.MIN),
|
||||||
|
LocalDateTime.of(customDate.plusDays(1), LocalTime.MIN)
|
||||||
|
);
|
||||||
|
DashboardSummaryVo summary = new DashboardSummaryVo();
|
||||||
|
Object payload = invokeBuildChatPayload(service, context, summary);
|
||||||
|
|
||||||
|
List<DashboardTrendItemVo> trends = (List<DashboardTrendItemVo>) readField(payload, "trends");
|
||||||
|
Assert.assertEquals(trends.size(), 24);
|
||||||
|
Assert.assertEquals(trends.get(8).getKey(), currentHourKey);
|
||||||
|
Assert.assertEquals(trends.get(8).getActiveUserTotal(), Long.valueOf(1L));
|
||||||
|
Assert.assertEquals(trends.get(9).getChatSessionTotal(), Long.valueOf(0L));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证自定义多天范围按天桶构建,并保留查询日期。
|
||||||
|
*
|
||||||
|
* @throws Exception 反射调用失败
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void shouldBuildDailyBucketsForCustomMultiDayRangeContext() throws Exception {
|
||||||
|
DashboardServiceImpl service = new DashboardServiceImpl();
|
||||||
|
DashboardOverviewQuery query = new DashboardOverviewQuery();
|
||||||
|
query.setRange("custom");
|
||||||
|
query.setStartDate("2026-05-01");
|
||||||
|
query.setEndDate("2026-05-03");
|
||||||
|
|
||||||
|
Object context = invokeBuildContext(service, query);
|
||||||
|
Assert.assertEquals(readField(context, "range"), "custom");
|
||||||
|
Assert.assertEquals(
|
||||||
|
readField(context, "startTime"),
|
||||||
|
LocalDateTime.of(LocalDate.of(2026, 5, 1), LocalTime.MIN)
|
||||||
|
);
|
||||||
|
Assert.assertEquals(
|
||||||
|
readField(context, "endTime"),
|
||||||
|
LocalDateTime.of(LocalDate.of(2026, 5, 4), LocalTime.MIN)
|
||||||
|
);
|
||||||
|
Assert.assertEquals(readField(context, "queryStartDate"), "2026-05-01");
|
||||||
|
Assert.assertEquals(readField(context, "queryEndDate"), "2026-05-03");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证近 30 天趋势补齐完整 30 个桶,并按 Top 8 请求智能体活跃排行。
|
||||||
|
*
|
||||||
|
* @throws Exception 反射调用失败
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public void shouldBuildThirtyDayBucketsAndRequestTopEightAssistantRanks() throws Exception {
|
||||||
|
DashboardServiceImpl service = new DashboardServiceImpl();
|
||||||
|
ChatDashboardQueryService chatDashboardQueryService = mock(ChatDashboardQueryService.class);
|
||||||
|
LocalDate startDate = LocalDate.now().minusDays(29);
|
||||||
|
LocalDate endDate = LocalDate.now();
|
||||||
|
when(chatDashboardQueryService.available()).thenReturn(true);
|
||||||
|
when(chatDashboardQueryService.querySummary(any(), any(), any()))
|
||||||
|
.thenReturn(new ChatDashboardSummary(10L, 20L, 8L, 4L));
|
||||||
|
when(chatDashboardQueryService.queryTrends(any(), any(), any()))
|
||||||
|
.thenReturn(List.of(
|
||||||
|
new ChatDashboardTrend(startDate.toString(), 3L, 6L, 2L),
|
||||||
|
new ChatDashboardTrend(endDate.toString(), 7L, 14L, 4L)
|
||||||
|
));
|
||||||
|
when(chatDashboardQueryService.queryAssistantUsageRanks(any(), any(), any(), eq(8)))
|
||||||
|
.thenReturn(IntStream.rangeClosed(1, 8)
|
||||||
|
.mapToObj(index -> new ChatAssistantUsageRank(
|
||||||
|
BigInteger.valueOf(index),
|
||||||
|
"助手-" + index,
|
||||||
|
index,
|
||||||
|
index * 2L,
|
||||||
|
index * 4L
|
||||||
|
))
|
||||||
|
.toList());
|
||||||
|
when(chatDashboardQueryService.queryAssistantTrends(any(), any(), any(), any()))
|
||||||
|
.thenReturn(List.of(
|
||||||
|
new ChatAssistantSessionTrend(BigInteger.ONE, "助手-1", startDate.toString(), 2L),
|
||||||
|
new ChatAssistantSessionTrend(BigInteger.ONE, "助手-1", endDate.toString(), 4L)
|
||||||
|
));
|
||||||
|
setField(service, "chatDashboardQueryService", chatDashboardQueryService);
|
||||||
|
|
||||||
|
Object context = newContext("30d", BigInteger.ONE);
|
||||||
|
DashboardSummaryVo summary = new DashboardSummaryVo();
|
||||||
|
Object payload = invokeBuildChatPayload(service, context, summary);
|
||||||
|
|
||||||
|
List<DashboardTrendItemVo> trends = (List<DashboardTrendItemVo>) readField(payload, "trends");
|
||||||
|
List<DashboardAssistantTrendSeriesVo> assistantTrends =
|
||||||
|
(List<DashboardAssistantTrendSeriesVo>) readField(payload, "assistantTrends");
|
||||||
|
Assert.assertEquals(trends.size(), 30);
|
||||||
|
Assert.assertEquals(trends.get(0).getKey(), startDate.toString());
|
||||||
|
Assert.assertEquals(trends.get(0).getChatSessionTotal(), Long.valueOf(3L));
|
||||||
|
Assert.assertEquals(trends.get(29).getKey(), endDate.toString());
|
||||||
|
Assert.assertEquals(trends.get(29).getChatMessageTotal(), Long.valueOf(14L));
|
||||||
|
Assert.assertEquals(trends.get(1).getChatSessionTotal(), Long.valueOf(0L));
|
||||||
|
Assert.assertEquals(assistantTrends.size(), 8);
|
||||||
|
Assert.assertEquals(assistantTrends.get(0).getPoints().size(), 30);
|
||||||
|
verify(chatDashboardQueryService).queryAssistantUsageRanks(any(), any(), any(), eq(8));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证用户榜筛选会透传 assistantId。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void shouldQueryUserRanksWithAssistantFilter() throws Exception {
|
||||||
|
DashboardServiceImpl service = new DashboardServiceImpl();
|
||||||
|
ChatDashboardQueryService chatDashboardQueryService = mock(ChatDashboardQueryService.class);
|
||||||
|
BotService botService = mock(BotService.class);
|
||||||
|
CategoryPermissionService categoryPermissionService = mock(CategoryPermissionService.class);
|
||||||
|
SysAccountService sysAccountService = mock(SysAccountService.class);
|
||||||
|
SysAccountRoleService sysAccountRoleService = mock(SysAccountRoleService.class);
|
||||||
|
SysRoleService sysRoleService = mock(SysRoleService.class);
|
||||||
|
|
||||||
|
Bot bot = new Bot();
|
||||||
|
bot.setId(BigInteger.TEN);
|
||||||
|
bot.setStatus(1);
|
||||||
|
bot.setCreatedBy(BigInteger.ONE);
|
||||||
|
bot.setCategoryId(BigInteger.valueOf(8));
|
||||||
|
|
||||||
|
when(chatDashboardQueryService.available()).thenReturn(true);
|
||||||
|
when(chatDashboardQueryService.queryActiveUserRanks(any(), any(), any(), eq(BigInteger.TEN), eq(5)))
|
||||||
|
.thenReturn(List.of(new ChatActiveUserRank(BigInteger.valueOf(2), "demo-user", 2L, 4L, 1L)));
|
||||||
|
when(botService.getById(BigInteger.TEN)).thenReturn(bot);
|
||||||
|
when(categoryPermissionService.canAccessCategory(any(LoginAccount.class), eq("BOT"), eq(BigInteger.ONE), eq(BigInteger.valueOf(8))))
|
||||||
|
.thenReturn(true);
|
||||||
|
when(sysAccountService.list(any(QueryWrapper.class))).thenReturn(List.of(buildSysAccount(2L, "demo-user", "演示用户")));
|
||||||
|
when(sysAccountRoleService.list(any(QueryWrapper.class))).thenReturn(Collections.emptyList());
|
||||||
|
|
||||||
|
setField(service, "chatDashboardQueryService", chatDashboardQueryService);
|
||||||
|
setField(service, "botService", botService);
|
||||||
|
setField(service, "categoryPermissionService", categoryPermissionService);
|
||||||
|
setField(service, "sysAccountService", sysAccountService);
|
||||||
|
setField(service, "sysAccountRoleService", sysAccountRoleService);
|
||||||
|
setField(service, "sysRoleService", sysRoleService);
|
||||||
|
|
||||||
|
DashboardUserRankQuery query = new DashboardUserRankQuery();
|
||||||
|
query.setRange("7d");
|
||||||
|
query.setAssistantId(BigInteger.TEN);
|
||||||
|
|
||||||
|
LoginAccount loginAccount = new LoginAccount();
|
||||||
|
loginAccount.setId(BigInteger.valueOf(12));
|
||||||
|
loginAccount.setTenantId(BigInteger.valueOf(33));
|
||||||
|
|
||||||
|
List<DashboardUserRankItemVo> userRanks = service.getUserRanks(loginAccount, query);
|
||||||
|
Assert.assertEquals(userRanks.size(), 1);
|
||||||
|
Assert.assertEquals(userRanks.get(0).getLabel(), "demo-user(演示用户)");
|
||||||
|
verify(chatDashboardQueryService).queryActiveUserRanks(any(), any(), any(), eq(BigInteger.TEN), eq(5));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证未启用智能体会被拒绝。
|
||||||
|
*/
|
||||||
|
@Test(expectedExceptions = BusinessException.class, expectedExceptionsMessageRegExp = "聊天助手不存在或未启用")
|
||||||
|
public void shouldRejectDisabledAssistantFilter() {
|
||||||
|
DashboardServiceImpl service = new DashboardServiceImpl();
|
||||||
|
BotService botService = mock(BotService.class);
|
||||||
|
ChatDashboardQueryService chatDashboardQueryService = mock(ChatDashboardQueryService.class);
|
||||||
|
|
||||||
|
Bot bot = new Bot();
|
||||||
|
bot.setId(BigInteger.TEN);
|
||||||
|
bot.setStatus(0);
|
||||||
|
|
||||||
|
when(botService.getById(BigInteger.TEN)).thenReturn(bot);
|
||||||
|
when(chatDashboardQueryService.available()).thenReturn(true);
|
||||||
|
|
||||||
|
setFieldSilently(service, "botService", botService);
|
||||||
|
setFieldSilently(service, "chatDashboardQueryService", chatDashboardQueryService);
|
||||||
|
setFieldSilently(service, "categoryPermissionService", mock(CategoryPermissionService.class));
|
||||||
|
setFieldSilently(service, "sysAccountService", mock(SysAccountService.class));
|
||||||
|
|
||||||
|
DashboardUserRankQuery query = new DashboardUserRankQuery();
|
||||||
|
query.setRange("7d");
|
||||||
|
query.setAssistantId(BigInteger.TEN);
|
||||||
|
service.getUserRanks(new LoginAccount(), query);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证当前作用域不可见的智能体会被拒绝。
|
||||||
|
*/
|
||||||
|
@Test(expectedExceptions = BusinessException.class, expectedExceptionsMessageRegExp = "聊天助手不存在或未启用")
|
||||||
|
public void shouldRejectInvisibleAssistantFilter() {
|
||||||
|
DashboardServiceImpl service = new DashboardServiceImpl();
|
||||||
|
BotService botService = mock(BotService.class);
|
||||||
|
ChatDashboardQueryService chatDashboardQueryService = mock(ChatDashboardQueryService.class);
|
||||||
|
CategoryPermissionService categoryPermissionService = mock(CategoryPermissionService.class);
|
||||||
|
|
||||||
|
Bot bot = new Bot();
|
||||||
|
bot.setId(BigInteger.TEN);
|
||||||
|
bot.setStatus(1);
|
||||||
|
bot.setCreatedBy(BigInteger.ONE);
|
||||||
|
bot.setCategoryId(BigInteger.valueOf(8));
|
||||||
|
|
||||||
|
when(botService.getById(BigInteger.TEN)).thenReturn(bot);
|
||||||
|
when(chatDashboardQueryService.available()).thenReturn(true);
|
||||||
|
when(categoryPermissionService.canAccessCategory(any(LoginAccount.class), eq("BOT"), eq(BigInteger.ONE), eq(BigInteger.valueOf(8))))
|
||||||
|
.thenReturn(false);
|
||||||
|
|
||||||
|
setFieldSilently(service, "botService", botService);
|
||||||
|
setFieldSilently(service, "chatDashboardQueryService", chatDashboardQueryService);
|
||||||
|
setFieldSilently(service, "categoryPermissionService", categoryPermissionService);
|
||||||
|
setFieldSilently(service, "sysAccountService", mock(SysAccountService.class));
|
||||||
|
|
||||||
|
DashboardUserRankQuery query = new DashboardUserRankQuery();
|
||||||
|
query.setRange("7d");
|
||||||
|
query.setAssistantId(BigInteger.TEN);
|
||||||
|
service.getUserRanks(new LoginAccount(), query);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证导出结果包含表头与账号昵称列。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void shouldExportDashboardUserRanksWithSeparatedIdentityColumns() throws Exception {
|
||||||
|
DashboardServiceImpl service = new DashboardServiceImpl();
|
||||||
|
ChatDashboardQueryService chatDashboardQueryService = mock(ChatDashboardQueryService.class);
|
||||||
|
SysAccountService sysAccountService = mock(SysAccountService.class);
|
||||||
|
|
||||||
|
when(chatDashboardQueryService.available()).thenReturn(true);
|
||||||
|
when(chatDashboardQueryService.queryActiveUserRanks(any(), any(), any(), isNull(), isNull()))
|
||||||
|
.thenReturn(List.of(new ChatActiveUserRank(BigInteger.valueOf(7), "export-user", 3L, 6L, 1L)));
|
||||||
|
when(sysAccountService.list(any(QueryWrapper.class))).thenReturn(List.of(buildSysAccount(7L, "export-user", "导出演示")));
|
||||||
|
|
||||||
|
setField(service, "chatDashboardQueryService", chatDashboardQueryService);
|
||||||
|
setField(service, "sysAccountService", sysAccountService);
|
||||||
|
|
||||||
|
DashboardUserRankQuery query = new DashboardUserRankQuery();
|
||||||
|
query.setRange("7d");
|
||||||
|
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||||
|
service.exportUserRanks(new LoginAccount(), query, outputStream);
|
||||||
|
|
||||||
|
try (org.apache.poi.ss.usermodel.Workbook workbook =
|
||||||
|
WorkbookFactory.create(new ByteArrayInputStream(outputStream.toByteArray()))) {
|
||||||
|
org.apache.poi.ss.usermodel.Sheet sheet = workbook.getSheetAt(0);
|
||||||
|
Assert.assertEquals(sheet.getRow(0).getCell(0).getStringCellValue(), "登录账号");
|
||||||
|
Assert.assertEquals(sheet.getRow(0).getCell(1).getStringCellValue(), "昵称");
|
||||||
|
Assert.assertEquals(sheet.getRow(0).getCell(2).getStringCellValue(), "会话数");
|
||||||
|
Assert.assertEquals(sheet.getRow(0).getCell(3).getStringCellValue(), "消息数");
|
||||||
|
Assert.assertEquals(sheet.getRow(1).getCell(0).getStringCellValue(), "export-user");
|
||||||
|
Assert.assertEquals(sheet.getRow(1).getCell(1).getStringCellValue(), "导出演示");
|
||||||
|
Assert.assertEquals(sheet.getRow(1).getCell(2).getStringCellValue(), "3");
|
||||||
|
Assert.assertEquals(sheet.getRow(1).getCell(3).getStringCellValue(), "6");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造查询上下文。
|
||||||
|
*
|
||||||
|
* @param range 时间范围
|
||||||
|
* @param tenantId 租户 ID
|
||||||
|
* @return 查询上下文实例
|
||||||
|
* @throws Exception 反射失败
|
||||||
|
*/
|
||||||
|
private Object newContext(String range, BigInteger tenantId) throws Exception {
|
||||||
|
return newContext(
|
||||||
|
range,
|
||||||
|
tenantId,
|
||||||
|
LocalDateTime.of(LocalDate.now(), java.time.LocalTime.MIN),
|
||||||
|
LocalDateTime.of(LocalDate.now().plusDays(1), java.time.LocalTime.MIN)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造查询上下文。
|
||||||
|
*
|
||||||
|
* @param range 时间范围
|
||||||
|
* @param tenantId 租户 ID
|
||||||
|
* @param startTime 开始时间
|
||||||
|
* @param endTime 结束时间
|
||||||
|
* @return 查询上下文实例
|
||||||
|
* @throws Exception 反射失败
|
||||||
|
*/
|
||||||
|
private Object newContext(String range,
|
||||||
|
BigInteger tenantId,
|
||||||
|
LocalDateTime startTime,
|
||||||
|
LocalDateTime endTime) throws Exception {
|
||||||
|
Class<?> contextClass = Class.forName(
|
||||||
|
"tech.easyflow.admin.service.dashboard.impl.DashboardServiceImpl$DashboardQueryContext"
|
||||||
|
);
|
||||||
|
Constructor<?> constructor = contextClass.getDeclaredConstructor();
|
||||||
|
constructor.setAccessible(true);
|
||||||
|
Object context = constructor.newInstance();
|
||||||
|
setField(context, "range", range);
|
||||||
|
setField(context, "tenantFilterId", tenantId);
|
||||||
|
setField(context, "startTime", startTime);
|
||||||
|
setField(context, "endTime", endTime);
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调用私有上下文构建方法。
|
||||||
|
*
|
||||||
|
* @param service service
|
||||||
|
* @param query 查询参数
|
||||||
|
* @return 上下文
|
||||||
|
* @throws Exception 反射失败
|
||||||
|
*/
|
||||||
|
private Object invokeBuildContext(DashboardServiceImpl service, DashboardOverviewQuery query) throws Exception {
|
||||||
|
Method method = DashboardServiceImpl.class.getDeclaredMethod(
|
||||||
|
"buildContext",
|
||||||
|
LoginAccount.class,
|
||||||
|
DashboardOverviewQuery.class
|
||||||
|
);
|
||||||
|
method.setAccessible(true);
|
||||||
|
return method.invoke(service, null, query);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调用私有聊天载荷组装方法。
|
||||||
|
*
|
||||||
|
* @param service service
|
||||||
|
* @param context 上下文
|
||||||
|
* @param summary 汇总对象
|
||||||
|
* @return 载荷
|
||||||
|
* @throws Exception 反射失败
|
||||||
|
*/
|
||||||
|
private Object invokeBuildChatPayload(DashboardServiceImpl service, Object context, DashboardSummaryVo summary)
|
||||||
|
throws Exception {
|
||||||
|
Method method = DashboardServiceImpl.class.getDeclaredMethod(
|
||||||
|
"buildChatPayload",
|
||||||
|
context.getClass(),
|
||||||
|
DashboardSummaryVo.class
|
||||||
|
);
|
||||||
|
method.setAccessible(true);
|
||||||
|
return method.invoke(service, context, summary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取对象字段。
|
||||||
|
*
|
||||||
|
* @param target 目标对象
|
||||||
|
* @param fieldName 字段名
|
||||||
|
* @return 字段值
|
||||||
|
* @throws Exception 反射失败
|
||||||
|
*/
|
||||||
|
private Object readField(Object target, String fieldName) throws Exception {
|
||||||
|
Field field = target.getClass().getDeclaredField(fieldName);
|
||||||
|
field.setAccessible(true);
|
||||||
|
return field.get(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调用 getter。
|
||||||
|
*
|
||||||
|
* @param target 目标对象
|
||||||
|
* @param methodName 方法名
|
||||||
|
* @return 返回值
|
||||||
|
* @throws Exception 反射失败
|
||||||
|
*/
|
||||||
|
private Object invokeGetter(Object target, String methodName) throws Exception {
|
||||||
|
Method method = target.getClass().getMethod(methodName);
|
||||||
|
return method.invoke(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过反射设置字段值。
|
||||||
|
*
|
||||||
|
* @param target 目标对象
|
||||||
|
* @param fieldName 字段名
|
||||||
|
* @param value 字段值
|
||||||
|
* @throws Exception 反射失败
|
||||||
|
*/
|
||||||
|
private void setField(Object target, String fieldName, Object value) throws Exception {
|
||||||
|
Class<?> current = target.getClass();
|
||||||
|
while (current != null) {
|
||||||
|
try {
|
||||||
|
Field field = current.getDeclaredField(fieldName);
|
||||||
|
field.setAccessible(true);
|
||||||
|
field.set(target, value);
|
||||||
|
return;
|
||||||
|
} catch (NoSuchFieldException ignored) {
|
||||||
|
current = current.getSuperclass();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException("未找到字段: " + fieldName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setFieldSilently(Object target, String fieldName, Object value) {
|
||||||
|
try {
|
||||||
|
setField(target, fieldName, value);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
throw new IllegalStateException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private SysAccount buildSysAccount(long id, String loginName, String nickname) {
|
||||||
|
SysAccount account = new SysAccount();
|
||||||
|
account.setId(BigInteger.valueOf(id));
|
||||||
|
account.setLoginName(loginName);
|
||||||
|
account.setNickname(nickname);
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,16 +7,20 @@ import jakarta.validation.constraints.NotBlank;
|
|||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
|
import tech.easyflow.approval.annotation.RequirePublishedAccess;
|
||||||
import tech.easyflow.ai.entity.Bot;
|
import tech.easyflow.ai.entity.Bot;
|
||||||
import tech.easyflow.ai.entity.ChatRequestParams;
|
import tech.easyflow.ai.entity.ChatRequestParams;
|
||||||
import tech.easyflow.ai.service.BotService;
|
import tech.easyflow.ai.service.BotService;
|
||||||
import tech.easyflow.ai.service.impl.BotServiceImpl;
|
import tech.easyflow.ai.service.impl.BotServiceImpl;
|
||||||
import tech.easyflow.common.domain.Result;
|
import tech.easyflow.common.domain.Result;
|
||||||
import tech.easyflow.core.chat.protocol.sse.ChatSseUtil;
|
import tech.easyflow.core.chat.protocol.sse.ChatSseUtil;
|
||||||
|
import tech.easyflow.core.runtime.ChatChannel;
|
||||||
|
import tech.easyflow.core.runtime.ChatRuntimeContext;
|
||||||
import tech.easyflow.system.entity.SysApiKey;
|
import tech.easyflow.system.entity.SysApiKey;
|
||||||
import tech.easyflow.system.service.SysApiKeyService;
|
import tech.easyflow.system.service.SysApiKeyService;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* bot 接口
|
* bot 接口
|
||||||
@@ -34,8 +38,9 @@ public class PublicBotController {
|
|||||||
* 根据id或别名获取bot详情
|
* 根据id或别名获取bot详情
|
||||||
*/
|
*/
|
||||||
@GetMapping("/getByIdOrAlias")
|
@GetMapping("/getByIdOrAlias")
|
||||||
|
@RequirePublishedAccess(resourceType = "BOT", idExpr = "#key", denyMessage = "聊天助手尚未发布")
|
||||||
public Result<Bot> getByIdOrAlias(@NotBlank(message = "key不能为空") String key) {
|
public Result<Bot> getByIdOrAlias(@NotBlank(message = "key不能为空") String key) {
|
||||||
return Result.ok(botService.getDetail(key));
|
return Result.ok(botService.getPublishedDetail(key));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -44,6 +49,7 @@ public class PublicBotController {
|
|||||||
* @return 返回SseEmitter对象,用于服务器向客户端推送聊天响应数据
|
* @return 返回SseEmitter对象,用于服务器向客户端推送聊天响应数据
|
||||||
*/
|
*/
|
||||||
@PostMapping("chat")
|
@PostMapping("chat")
|
||||||
|
@RequirePublishedAccess(resourceType = "BOT", idExpr = "#chatRequestParams.botId", denyMessage = "聊天助手尚未发布")
|
||||||
public SseEmitter chat(@RequestBody ChatRequestParams chatRequestParams, HttpServletRequest request) {
|
public SseEmitter chat(@RequestBody ChatRequestParams chatRequestParams, HttpServletRequest request) {
|
||||||
String apikey = request.getHeader(SysApiKey.KEY_Apikey);
|
String apikey = request.getHeader(SysApiKey.KEY_Apikey);
|
||||||
String requestURI = request.getRequestURI();
|
String requestURI = request.getRequestURI();
|
||||||
@@ -51,6 +57,7 @@ public class PublicBotController {
|
|||||||
return ChatSseUtil.sendSystemError(null, "Apikey不能为空!");
|
return ChatSseUtil.sendSystemError(null, "Apikey不能为空!");
|
||||||
}
|
}
|
||||||
sysApiKeyService.checkApikeyPermission(apikey, requestURI);
|
sysApiKeyService.checkApikeyPermission(apikey, requestURI);
|
||||||
|
SysApiKey sysApiKey = sysApiKeyService.getSysApiKey(apikey);
|
||||||
BotServiceImpl.ChatCheckResult chatCheckResult = new BotServiceImpl.ChatCheckResult();
|
BotServiceImpl.ChatCheckResult chatCheckResult = new BotServiceImpl.ChatCheckResult();
|
||||||
int size = chatRequestParams.getMessages().size();
|
int size = chatRequestParams.getMessages().size();
|
||||||
String prompt = null;
|
String prompt = null;
|
||||||
@@ -62,7 +69,30 @@ public class PublicBotController {
|
|||||||
if (errorEmitter != null) {
|
if (errorEmitter != null) {
|
||||||
return errorEmitter;
|
return errorEmitter;
|
||||||
}
|
}
|
||||||
return botService.startPublicChat(chatRequestParams.getBotId(), prompt, chatRequestParams.getMessages(), chatCheckResult);
|
return botService.startPublicChat(
|
||||||
|
chatRequestParams.getBotId(),
|
||||||
|
prompt,
|
||||||
|
chatRequestParams.getMessages(),
|
||||||
|
chatCheckResult,
|
||||||
|
buildRuntimeContext(chatCheckResult.getAiBot(), chatRequestParams.getConversationId(), prompt, sysApiKey)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ChatRuntimeContext buildRuntimeContext(Bot bot, String conversationId, String prompt, SysApiKey sysApiKey) {
|
||||||
|
ChatRuntimeContext context = new ChatRuntimeContext();
|
||||||
|
context.setChannel(ChatChannel.PUBLIC_API);
|
||||||
|
context.setSessionId(new BigInteger(conversationId));
|
||||||
|
context.setTenantId(BigInteger.ZERO);
|
||||||
|
context.setDeptId(BigInteger.ZERO);
|
||||||
|
context.setUserId(BigInteger.ZERO);
|
||||||
|
context.setUserAccount("apikey:" + sysApiKey.getId());
|
||||||
|
context.setUserName("API 调用方");
|
||||||
|
context.setAssistantId(bot == null ? BigInteger.ZERO : bot.getId());
|
||||||
|
context.setAssistantCode(bot == null ? null : bot.getAlias());
|
||||||
|
context.setAssistantName(bot == null ? null : bot.getTitle());
|
||||||
|
context.setSessionTitle(prompt != null && prompt.length() > 200 ? prompt.substring(0, 200) : prompt);
|
||||||
|
context.setAnonymous(true);
|
||||||
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,673 @@
|
|||||||
|
package tech.easyflow.publicapi.controller;
|
||||||
|
|
||||||
|
import cn.hutool.core.io.IoUtil;
|
||||||
|
import com.easyagents.core.model.embedding.EmbeddingModel;
|
||||||
|
import com.easyagents.core.store.DocumentStore;
|
||||||
|
import com.easyagents.core.store.StoreOptions;
|
||||||
|
import com.easyagents.core.store.StoreResult;
|
||||||
|
import com.mybatisflex.core.paginate.Page;
|
||||||
|
import com.mybatisflex.core.query.QueryWrapper;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestHeader;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
import tech.easyflow.ai.documentimport.DocumentImportDtos;
|
||||||
|
import tech.easyflow.ai.dto.KnowledgeSearchResultItem;
|
||||||
|
import tech.easyflow.ai.entity.Document;
|
||||||
|
import tech.easyflow.ai.entity.DocumentChunk;
|
||||||
|
import tech.easyflow.ai.entity.DocumentCollection;
|
||||||
|
import tech.easyflow.ai.entity.FaqItem;
|
||||||
|
import tech.easyflow.ai.entity.Model;
|
||||||
|
import tech.easyflow.ai.enums.KnowledgeShareActionScope;
|
||||||
|
import tech.easyflow.ai.rag.KnowledgeRetrievalModes;
|
||||||
|
import tech.easyflow.ai.rag.KnowledgeRetrievalRequest;
|
||||||
|
import tech.easyflow.ai.service.DocumentChunkService;
|
||||||
|
import tech.easyflow.ai.service.DocumentCollectionService;
|
||||||
|
import tech.easyflow.ai.service.DocumentService;
|
||||||
|
import tech.easyflow.ai.service.FaqCategoryService;
|
||||||
|
import tech.easyflow.ai.service.FaqItemService;
|
||||||
|
import tech.easyflow.ai.service.KnowledgeShareAuditService;
|
||||||
|
import tech.easyflow.ai.service.KnowledgeSharePermissionService;
|
||||||
|
import tech.easyflow.ai.service.ModelService;
|
||||||
|
import tech.easyflow.ai.service.impl.KnowledgeSharePermissionServiceImpl;
|
||||||
|
import tech.easyflow.ai.support.DocumentStoreLifecycleSupport;
|
||||||
|
import tech.easyflow.ai.vo.FaqImportResultVo;
|
||||||
|
import tech.easyflow.common.domain.Result;
|
||||||
|
import tech.easyflow.common.filestorage.FileStorageService;
|
||||||
|
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||||
|
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||||
|
import tech.easyflow.system.entity.SysApiKey;
|
||||||
|
import tech.easyflow.system.service.SysApiKeyService;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 知识库 API 分享接口。
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping(value = "/public-api/knowledge-share", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||||
|
public class PublicKnowledgeShareController {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private SysApiKeyService sysApiKeyService;
|
||||||
|
@Resource
|
||||||
|
private KnowledgeSharePermissionService knowledgeSharePermissionService;
|
||||||
|
@Resource
|
||||||
|
private KnowledgeShareAuditService knowledgeShareAuditService;
|
||||||
|
@Resource
|
||||||
|
private DocumentCollectionService documentCollectionService;
|
||||||
|
@Resource
|
||||||
|
private DocumentService documentService;
|
||||||
|
@Resource
|
||||||
|
private DocumentChunkService documentChunkService;
|
||||||
|
@Resource
|
||||||
|
private FaqItemService faqItemService;
|
||||||
|
@Resource
|
||||||
|
private FaqCategoryService faqCategoryService;
|
||||||
|
@Resource
|
||||||
|
private ModelService modelService;
|
||||||
|
@Resource(name = "default")
|
||||||
|
private FileStorageService fileStorageService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取知识库详情。
|
||||||
|
*/
|
||||||
|
@GetMapping("/detail")
|
||||||
|
public Result<DocumentCollection> detail(
|
||||||
|
@RequestHeader("ApiKey") String apiKey,
|
||||||
|
@RequestParam BigInteger knowledgeId,
|
||||||
|
HttpServletRequest request
|
||||||
|
) {
|
||||||
|
assertApiShare(apiKey, request.getRequestURI(), knowledgeId, KnowledgeShareActionScope.VIEW.name());
|
||||||
|
audit(apiKey, "API读取知识库详情", "KNOWLEDGE_API_SHARE_ACCESS", request.getRequestURI(), Map.of("knowledgeId", knowledgeId));
|
||||||
|
return Result.ok(documentCollectionService.getDetail(knowledgeId.toString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检索知识库。
|
||||||
|
*/
|
||||||
|
@GetMapping("/search")
|
||||||
|
public Result<List<KnowledgeSearchResultItem>> search(
|
||||||
|
@RequestHeader("ApiKey") String apiKey,
|
||||||
|
@RequestParam BigInteger knowledgeId,
|
||||||
|
@RequestParam String keyword,
|
||||||
|
@RequestParam(required = false) String retrievalMode,
|
||||||
|
HttpServletRequest request
|
||||||
|
) {
|
||||||
|
assertApiShare(apiKey, request.getRequestURI(), knowledgeId, KnowledgeShareActionScope.SEARCH.name());
|
||||||
|
KnowledgeRetrievalRequest retrievalRequest = new KnowledgeRetrievalRequest();
|
||||||
|
retrievalRequest.setKnowledgeId(knowledgeId);
|
||||||
|
retrievalRequest.setQuery(keyword);
|
||||||
|
retrievalRequest.setRetrievalMode(KnowledgeRetrievalModes.parse(retrievalMode));
|
||||||
|
retrievalRequest.setCallerType("PUBLIC_API");
|
||||||
|
retrievalRequest.setCallerId(String.valueOf(knowledgeId));
|
||||||
|
audit(apiKey, "API检索知识库", "KNOWLEDGE_API_SHARE_ACCESS", request.getRequestURI(), Map.of("knowledgeId", knowledgeId));
|
||||||
|
return Result.ok(toKnowledgeSearchResult(documentCollectionService.search(retrievalRequest)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文档分页。
|
||||||
|
*/
|
||||||
|
@GetMapping("/document/page")
|
||||||
|
public Result<Page<Document>> documentPage(
|
||||||
|
@RequestHeader("ApiKey") String apiKey,
|
||||||
|
@RequestParam BigInteger knowledgeId,
|
||||||
|
@RequestParam(required = false) String title,
|
||||||
|
@RequestParam(defaultValue = "10") int pageSize,
|
||||||
|
@RequestParam(defaultValue = "1") int pageNumber,
|
||||||
|
HttpServletRequest request
|
||||||
|
) {
|
||||||
|
assertApiShare(apiKey, request.getRequestURI(), knowledgeId, KnowledgeShareActionScope.VIEW.name());
|
||||||
|
requireDocumentKnowledge(knowledgeId);
|
||||||
|
return Result.ok(documentService.getDocumentList(knowledgeId.toString(), pageSize, pageNumber, title));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载文档。
|
||||||
|
*/
|
||||||
|
@GetMapping(value = "/document/download", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
|
||||||
|
public void documentDownload(
|
||||||
|
@RequestHeader("ApiKey") String apiKey,
|
||||||
|
@RequestParam BigInteger knowledgeId,
|
||||||
|
@RequestParam BigInteger documentId,
|
||||||
|
HttpServletRequest request,
|
||||||
|
HttpServletResponse response
|
||||||
|
) throws Exception {
|
||||||
|
assertApiShare(apiKey, request.getRequestURI(), knowledgeId, KnowledgeShareActionScope.VIEW.name());
|
||||||
|
requireDocumentKnowledge(knowledgeId);
|
||||||
|
Document document = requireDocument(documentId, knowledgeId);
|
||||||
|
response.setContentType("application/octet-stream");
|
||||||
|
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
|
||||||
|
String fileName = URLEncoder.encode(document.getTitle(), StandardCharsets.UTF_8).replaceAll("\\+", "%20");
|
||||||
|
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName);
|
||||||
|
try (InputStream inputStream = fileStorageService.readStream(document.getDocumentPath())) {
|
||||||
|
IoUtil.copy(inputStream, response.getOutputStream());
|
||||||
|
response.flushBuffer();
|
||||||
|
}
|
||||||
|
audit(apiKey, "API下载文档", "KNOWLEDGE_API_SHARE_ACCESS", request.getRequestURI(), Map.of("knowledgeId", knowledgeId, "documentId", documentId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除文档。
|
||||||
|
*/
|
||||||
|
@PostMapping("/document/remove")
|
||||||
|
public Result<?> removeDocument(
|
||||||
|
@RequestHeader("ApiKey") String apiKey,
|
||||||
|
@RequestParam BigInteger knowledgeId,
|
||||||
|
@JsonBody("id") String id,
|
||||||
|
HttpServletRequest request
|
||||||
|
) {
|
||||||
|
assertApiShare(apiKey, request.getRequestURI(), knowledgeId, KnowledgeShareActionScope.CONTENT_DELETE.name());
|
||||||
|
requireDocumentKnowledge(knowledgeId);
|
||||||
|
requireDocument(new BigInteger(id), knowledgeId);
|
||||||
|
audit(apiKey, "API删除文档", "KNOWLEDGE_API_SHARE_WRITE", request.getRequestURI(), Map.of("knowledgeId", knowledgeId, "documentId", id));
|
||||||
|
return Result.ok(documentService.removeDoc(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文档导入分析。
|
||||||
|
*/
|
||||||
|
@PostMapping("/document/import/analyze")
|
||||||
|
public Result<DocumentImportDtos.AnalyzeResponse> analyzeImport(
|
||||||
|
@RequestHeader("ApiKey") String apiKey,
|
||||||
|
@JsonBody DocumentImportDtos.AnalyzeRequest request,
|
||||||
|
HttpServletRequest servletRequest
|
||||||
|
) {
|
||||||
|
assertApiShare(apiKey, servletRequest.getRequestURI(), request.getKnowledgeId(), KnowledgeShareActionScope.CONTENT_CREATE.name());
|
||||||
|
requireDocumentKnowledge(request.getKnowledgeId());
|
||||||
|
audit(apiKey, "API分析文档导入", "KNOWLEDGE_API_SHARE_WRITE", servletRequest.getRequestURI(), Map.of("knowledgeId", request.getKnowledgeId()));
|
||||||
|
return documentService.analyzeImport(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文档导入预览。
|
||||||
|
*/
|
||||||
|
@PostMapping("/document/import/preview")
|
||||||
|
public Result<DocumentImportDtos.PreviewResponse> previewImport(
|
||||||
|
@RequestHeader("ApiKey") String apiKey,
|
||||||
|
@JsonBody DocumentImportDtos.PreviewRequest request,
|
||||||
|
HttpServletRequest servletRequest
|
||||||
|
) {
|
||||||
|
assertApiShare(apiKey, servletRequest.getRequestURI(), request.getKnowledgeId(), KnowledgeShareActionScope.CONTENT_CREATE.name());
|
||||||
|
requireDocumentKnowledge(request.getKnowledgeId());
|
||||||
|
audit(apiKey, "API预览文档导入", "KNOWLEDGE_API_SHARE_WRITE", servletRequest.getRequestURI(), Map.of("knowledgeId", request.getKnowledgeId()));
|
||||||
|
return documentService.previewImport(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文档导入提交。
|
||||||
|
*/
|
||||||
|
@PostMapping("/document/import/commit")
|
||||||
|
public Result<DocumentImportDtos.CommitResponse> commitImport(
|
||||||
|
@RequestHeader("ApiKey") String apiKey,
|
||||||
|
@JsonBody DocumentImportDtos.CommitRequest request,
|
||||||
|
HttpServletRequest servletRequest
|
||||||
|
) {
|
||||||
|
assertApiShare(apiKey, servletRequest.getRequestURI(), request.getKnowledgeId(), KnowledgeShareActionScope.CONTENT_CREATE.name());
|
||||||
|
requireDocumentKnowledge(request.getKnowledgeId());
|
||||||
|
audit(apiKey, "API提交文档导入", "KNOWLEDGE_API_SHARE_WRITE", servletRequest.getRequestURI(), Map.of("knowledgeId", request.getKnowledgeId()));
|
||||||
|
return documentService.commitImport(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/document/import/task/create")
|
||||||
|
public Result<DocumentImportDtos.TaskCreateResponse> createImportTask(
|
||||||
|
@RequestHeader("ApiKey") String apiKey,
|
||||||
|
@JsonBody DocumentImportDtos.TaskCreateRequest request,
|
||||||
|
HttpServletRequest servletRequest
|
||||||
|
) {
|
||||||
|
assertApiShare(apiKey, servletRequest.getRequestURI(), request.getKnowledgeId(), KnowledgeShareActionScope.CONTENT_CREATE.name());
|
||||||
|
requireDocumentKnowledge(request.getKnowledgeId());
|
||||||
|
audit(apiKey, "API创建文档导入任务", "KNOWLEDGE_API_SHARE_WRITE", servletRequest.getRequestURI(), Map.of("knowledgeId", request.getKnowledgeId()));
|
||||||
|
return documentService.createImportTask(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/document/import/task/detail")
|
||||||
|
public Result<DocumentImportDtos.TaskDetailResponse> getImportTaskDetail(
|
||||||
|
@RequestHeader("ApiKey") String apiKey,
|
||||||
|
@RequestParam BigInteger knowledgeId,
|
||||||
|
@RequestParam BigInteger taskId,
|
||||||
|
HttpServletRequest request
|
||||||
|
) {
|
||||||
|
assertApiShare(apiKey, request.getRequestURI(), knowledgeId, KnowledgeShareActionScope.VIEW.name());
|
||||||
|
requireDocumentKnowledge(knowledgeId);
|
||||||
|
Result<DocumentImportDtos.TaskDetailResponse> result = documentService.getImportTaskDetail(taskId);
|
||||||
|
if (result.getData() == null || result.getData().getKnowledgeId() == null
|
||||||
|
|| result.getData().getKnowledgeId().compareTo(knowledgeId) != 0) {
|
||||||
|
throw new BusinessException("任务不存在");
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/document/import/task/preview")
|
||||||
|
public Result<DocumentImportDtos.PreviewResponse> previewImportTask(
|
||||||
|
@RequestHeader("ApiKey") String apiKey,
|
||||||
|
@JsonBody DocumentImportDtos.PreviewRequest request,
|
||||||
|
HttpServletRequest servletRequest
|
||||||
|
) {
|
||||||
|
assertApiShare(apiKey, servletRequest.getRequestURI(), request.getKnowledgeId(), KnowledgeShareActionScope.CONTENT_CREATE.name());
|
||||||
|
requireDocumentKnowledge(request.getKnowledgeId());
|
||||||
|
audit(apiKey, "API预览文档分块", "KNOWLEDGE_API_SHARE_WRITE", servletRequest.getRequestURI(), Map.of("knowledgeId", request.getKnowledgeId()));
|
||||||
|
return documentService.previewImportTask(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/document/import/task/startIndex")
|
||||||
|
public Result<DocumentImportDtos.TaskStartIndexResponse> startIndexTask(
|
||||||
|
@RequestHeader("ApiKey") String apiKey,
|
||||||
|
@JsonBody DocumentImportDtos.TaskStartIndexRequest request,
|
||||||
|
HttpServletRequest servletRequest
|
||||||
|
) {
|
||||||
|
assertApiShare(apiKey, servletRequest.getRequestURI(), request.getKnowledgeId(), KnowledgeShareActionScope.CONTENT_CREATE.name());
|
||||||
|
requireDocumentKnowledge(request.getKnowledgeId());
|
||||||
|
audit(apiKey, "API启动文档向量化", "KNOWLEDGE_API_SHARE_WRITE", servletRequest.getRequestURI(), Map.of("knowledgeId", request.getKnowledgeId()));
|
||||||
|
return documentService.startIndexTask(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/document/import/task/retryParse")
|
||||||
|
public Result<DocumentImportDtos.TaskStartIndexResponse> retryParseTask(
|
||||||
|
@RequestHeader("ApiKey") String apiKey,
|
||||||
|
@JsonBody DocumentImportDtos.TaskRetryRequest request,
|
||||||
|
HttpServletRequest servletRequest
|
||||||
|
) {
|
||||||
|
assertApiShare(apiKey, servletRequest.getRequestURI(), request.getKnowledgeId(), KnowledgeShareActionScope.CONTENT_CREATE.name());
|
||||||
|
requireDocumentKnowledge(request.getKnowledgeId());
|
||||||
|
audit(apiKey, "API重试文档解析", "KNOWLEDGE_API_SHARE_WRITE", servletRequest.getRequestURI(), Map.of("knowledgeId", request.getKnowledgeId()));
|
||||||
|
return documentService.retryParseTask(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/document/import/task/retryIndex")
|
||||||
|
public Result<DocumentImportDtos.TaskStartIndexResponse> retryIndexTask(
|
||||||
|
@RequestHeader("ApiKey") String apiKey,
|
||||||
|
@JsonBody DocumentImportDtos.TaskRetryRequest request,
|
||||||
|
HttpServletRequest servletRequest
|
||||||
|
) {
|
||||||
|
assertApiShare(apiKey, servletRequest.getRequestURI(), request.getKnowledgeId(), KnowledgeShareActionScope.CONTENT_CREATE.name());
|
||||||
|
requireDocumentKnowledge(request.getKnowledgeId());
|
||||||
|
audit(apiKey, "API重试文档向量化", "KNOWLEDGE_API_SHARE_WRITE", servletRequest.getRequestURI(), Map.of("knowledgeId", request.getKnowledgeId()));
|
||||||
|
return documentService.retryIndexTask(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chunk 分页。
|
||||||
|
*/
|
||||||
|
@GetMapping("/documentChunk/page")
|
||||||
|
public Result<Page<DocumentChunk>> documentChunkPage(
|
||||||
|
@RequestHeader("ApiKey") String apiKey,
|
||||||
|
@RequestParam BigInteger knowledgeId,
|
||||||
|
@RequestParam BigInteger documentId,
|
||||||
|
@RequestParam(defaultValue = "1") long pageNumber,
|
||||||
|
@RequestParam(defaultValue = "10") long pageSize,
|
||||||
|
HttpServletRequest request
|
||||||
|
) {
|
||||||
|
assertApiShare(apiKey, request.getRequestURI(), knowledgeId, KnowledgeShareActionScope.VIEW.name());
|
||||||
|
requireDocumentKnowledge(knowledgeId);
|
||||||
|
requireDocument(documentId, knowledgeId);
|
||||||
|
QueryWrapper wrapper = QueryWrapper.create()
|
||||||
|
.eq(DocumentChunk::getDocumentId, documentId)
|
||||||
|
.orderBy("sorting asc");
|
||||||
|
return Result.ok(documentChunkService.page(new Page<>(pageNumber, pageSize), wrapper));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新 Chunk。
|
||||||
|
*/
|
||||||
|
@PostMapping("/documentChunk/update")
|
||||||
|
public Result<?> updateDocumentChunk(
|
||||||
|
@RequestHeader("ApiKey") String apiKey,
|
||||||
|
@RequestParam BigInteger knowledgeId,
|
||||||
|
@JsonBody DocumentChunk documentChunk,
|
||||||
|
HttpServletRequest request
|
||||||
|
) {
|
||||||
|
assertApiShare(apiKey, request.getRequestURI(), knowledgeId, KnowledgeShareActionScope.CONTENT_UPDATE.name());
|
||||||
|
requireDocumentKnowledge(knowledgeId);
|
||||||
|
DocumentChunk current = requireDocumentChunk(documentChunk.getId(), knowledgeId);
|
||||||
|
boolean success = documentChunkService.updateById(documentChunk);
|
||||||
|
if (success) {
|
||||||
|
DocumentCollection knowledge = documentCollectionService.getById(knowledgeId);
|
||||||
|
DocumentStore documentStore = knowledge.toDocumentStore();
|
||||||
|
if (documentStore == null) {
|
||||||
|
return Result.fail(2, "知识库没有配置向量库");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
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());
|
||||||
|
com.easyagents.core.document.Document doc = com.easyagents.core.document.Document.of(documentChunk.getContent());
|
||||||
|
doc.setId(current.getId());
|
||||||
|
StoreResult result = documentStore.update(doc, options);
|
||||||
|
audit(apiKey, "API更新文档 Chunk", "KNOWLEDGE_API_SHARE_WRITE", request.getRequestURI(), Map.of("knowledgeId", knowledgeId, "chunkId", documentChunk.getId()));
|
||||||
|
return Result.ok(result);
|
||||||
|
} finally {
|
||||||
|
DocumentStoreLifecycleSupport.closeQuietly(documentStore);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Result.ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除 Chunk。
|
||||||
|
*/
|
||||||
|
@PostMapping("/documentChunk/remove")
|
||||||
|
public Result<?> removeDocumentChunk(
|
||||||
|
@RequestHeader("ApiKey") String apiKey,
|
||||||
|
@RequestParam BigInteger knowledgeId,
|
||||||
|
@JsonBody("id") BigInteger chunkId,
|
||||||
|
HttpServletRequest request
|
||||||
|
) {
|
||||||
|
assertApiShare(apiKey, request.getRequestURI(), knowledgeId, KnowledgeShareActionScope.CONTENT_DELETE.name());
|
||||||
|
requireDocumentKnowledge(knowledgeId);
|
||||||
|
requireDocumentChunk(chunkId, knowledgeId);
|
||||||
|
DocumentCollection knowledge = documentCollectionService.getById(knowledgeId);
|
||||||
|
DocumentStore documentStore = knowledge.toDocumentStore();
|
||||||
|
if (documentStore == null) {
|
||||||
|
return Result.fail(2, "知识库没有配置向量库");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Model model = modelService.getModelInstance(knowledge.getVectorEmbedModelId());
|
||||||
|
if (model == null) {
|
||||||
|
return Result.fail(3, "知识库没有配置向量模型");
|
||||||
|
}
|
||||||
|
documentStore.setEmbeddingModel(model.toEmbeddingModel());
|
||||||
|
StoreOptions options = StoreOptions.ofCollectionName(knowledge.getVectorStoreCollection());
|
||||||
|
documentStore.delete(Collections.singletonList(chunkId), options);
|
||||||
|
documentChunkService.removeById(chunkId);
|
||||||
|
audit(apiKey, "API删除文档 Chunk", "KNOWLEDGE_API_SHARE_WRITE", request.getRequestURI(), Map.of("knowledgeId", knowledgeId, "chunkId", chunkId));
|
||||||
|
return Result.ok(true);
|
||||||
|
} finally {
|
||||||
|
DocumentStoreLifecycleSupport.closeQuietly(documentStore);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FAQ 分页。
|
||||||
|
*/
|
||||||
|
@GetMapping("/faq/page")
|
||||||
|
public Result<Page<FaqItem>> faqPage(
|
||||||
|
@RequestHeader("ApiKey") String apiKey,
|
||||||
|
@RequestParam BigInteger knowledgeId,
|
||||||
|
@RequestParam(required = false) String question,
|
||||||
|
@RequestParam(required = false) String categoryId,
|
||||||
|
@RequestParam(defaultValue = "1") long pageNumber,
|
||||||
|
@RequestParam(defaultValue = "10") long pageSize,
|
||||||
|
HttpServletRequest request
|
||||||
|
) {
|
||||||
|
assertApiShare(apiKey, request.getRequestURI(), knowledgeId, KnowledgeShareActionScope.VIEW.name());
|
||||||
|
requireFaqKnowledge(knowledgeId);
|
||||||
|
faqCategoryService.ensureDefaultCategory(knowledgeId);
|
||||||
|
QueryWrapper queryWrapper = QueryWrapper.create()
|
||||||
|
.eq(FaqItem::getCollectionId, knowledgeId);
|
||||||
|
if (question != null && !question.isBlank()) {
|
||||||
|
queryWrapper.like(FaqItem::getQuestion, question.trim());
|
||||||
|
}
|
||||||
|
if (categoryId != null && !categoryId.isBlank()) {
|
||||||
|
List<BigInteger> descendantIds = faqCategoryService.findDescendantIds(knowledgeId, new BigInteger(categoryId));
|
||||||
|
if (descendantIds.isEmpty()) {
|
||||||
|
queryWrapper.eq(FaqItem::getId, BigInteger.ZERO);
|
||||||
|
} else {
|
||||||
|
queryWrapper.in(FaqItem::getCategoryId, descendantIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
queryWrapper.orderBy("order_no asc");
|
||||||
|
Page<FaqItem> page = faqItemService.page(new Page<>(pageNumber, pageSize), queryWrapper);
|
||||||
|
Map<BigInteger, String> pathMap = faqCategoryService.buildPathMap(knowledgeId);
|
||||||
|
if (page.getRecords() != null) {
|
||||||
|
for (FaqItem record : page.getRecords()) {
|
||||||
|
record.setCategoryPath(pathMap.get(record.getCategoryId()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Result.ok(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FAQ 详情。
|
||||||
|
*/
|
||||||
|
@GetMapping("/faq/detail")
|
||||||
|
public Result<FaqItem> faqDetail(
|
||||||
|
@RequestHeader("ApiKey") String apiKey,
|
||||||
|
@RequestParam BigInteger knowledgeId,
|
||||||
|
@RequestParam String id,
|
||||||
|
HttpServletRequest request
|
||||||
|
) {
|
||||||
|
assertApiShare(apiKey, request.getRequestURI(), knowledgeId, KnowledgeShareActionScope.VIEW.name());
|
||||||
|
requireFaqKnowledge(knowledgeId);
|
||||||
|
FaqItem faqItem = requireFaq(new BigInteger(id), knowledgeId);
|
||||||
|
return Result.ok(faqItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增 FAQ。
|
||||||
|
*/
|
||||||
|
@PostMapping("/faq/save")
|
||||||
|
public Result<?> saveFaq(
|
||||||
|
@RequestHeader("ApiKey") String apiKey,
|
||||||
|
@JsonBody FaqItem entity,
|
||||||
|
HttpServletRequest request
|
||||||
|
) {
|
||||||
|
assertApiShare(apiKey, request.getRequestURI(), entity.getCollectionId(), KnowledgeShareActionScope.CONTENT_CREATE.name());
|
||||||
|
requireFaqKnowledge(entity.getCollectionId());
|
||||||
|
audit(apiKey, "API新增FAQ", "KNOWLEDGE_API_SHARE_WRITE", request.getRequestURI(), Map.of("knowledgeId", entity.getCollectionId()));
|
||||||
|
return Result.ok(faqItemService.saveFaqItem(entity));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新 FAQ。
|
||||||
|
*/
|
||||||
|
@PostMapping("/faq/update")
|
||||||
|
public Result<?> updateFaq(
|
||||||
|
@RequestHeader("ApiKey") String apiKey,
|
||||||
|
@RequestParam BigInteger knowledgeId,
|
||||||
|
@JsonBody FaqItem entity,
|
||||||
|
HttpServletRequest request
|
||||||
|
) {
|
||||||
|
assertApiShare(apiKey, request.getRequestURI(), knowledgeId, KnowledgeShareActionScope.CONTENT_UPDATE.name());
|
||||||
|
requireFaqKnowledge(knowledgeId);
|
||||||
|
requireFaq(entity.getId(), knowledgeId);
|
||||||
|
audit(apiKey, "API更新FAQ", "KNOWLEDGE_API_SHARE_WRITE", request.getRequestURI(), Map.of("knowledgeId", knowledgeId, "faqId", entity.getId()));
|
||||||
|
return Result.ok(faqItemService.updateFaqItem(entity));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除 FAQ。
|
||||||
|
*/
|
||||||
|
@PostMapping("/faq/remove")
|
||||||
|
public Result<?> removeFaq(
|
||||||
|
@RequestHeader("ApiKey") String apiKey,
|
||||||
|
@RequestParam BigInteger knowledgeId,
|
||||||
|
@JsonBody("id") BigInteger id,
|
||||||
|
HttpServletRequest request
|
||||||
|
) {
|
||||||
|
assertApiShare(apiKey, request.getRequestURI(), knowledgeId, KnowledgeShareActionScope.CONTENT_DELETE.name());
|
||||||
|
requireFaqKnowledge(knowledgeId);
|
||||||
|
requireFaq(id, knowledgeId);
|
||||||
|
audit(apiKey, "API删除FAQ", "KNOWLEDGE_API_SHARE_WRITE", request.getRequestURI(), Map.of("knowledgeId", knowledgeId, "faqId", id));
|
||||||
|
return Result.ok(faqItemService.removeFaqItem(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导入 FAQ Excel。
|
||||||
|
*/
|
||||||
|
@PostMapping(value = "/faq/importExcel", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||||
|
public Result<FaqImportResultVo> importFaqExcel(
|
||||||
|
@RequestHeader("ApiKey") String apiKey,
|
||||||
|
MultipartFile file,
|
||||||
|
BigInteger collectionId,
|
||||||
|
HttpServletRequest request
|
||||||
|
) {
|
||||||
|
assertApiShare(apiKey, request.getRequestURI(), collectionId, KnowledgeShareActionScope.IMPORT_EXPORT.name());
|
||||||
|
requireFaqKnowledge(collectionId);
|
||||||
|
audit(apiKey, "API导入FAQ Excel", "KNOWLEDGE_API_SHARE_WRITE", request.getRequestURI(), Map.of("knowledgeId", collectionId));
|
||||||
|
return Result.ok(faqItemService.importFromExcel(collectionId, file));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载 FAQ 导入模板。
|
||||||
|
*/
|
||||||
|
@GetMapping(value = "/faq/downloadImportTemplate", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
|
||||||
|
public void downloadFaqImportTemplate(
|
||||||
|
@RequestHeader("ApiKey") String apiKey,
|
||||||
|
@RequestParam BigInteger knowledgeId,
|
||||||
|
HttpServletRequest request,
|
||||||
|
HttpServletResponse response
|
||||||
|
) throws Exception {
|
||||||
|
assertApiShare(apiKey, request.getRequestURI(), knowledgeId, KnowledgeShareActionScope.IMPORT_EXPORT.name());
|
||||||
|
requireFaqKnowledge(knowledgeId);
|
||||||
|
response.setContentType("application/octet-stream");
|
||||||
|
response.setHeader(
|
||||||
|
"Content-disposition",
|
||||||
|
"attachment;filename*=utf-8''" + URLEncoder.encode("faq_import_template.xlsx", StandardCharsets.UTF_8)
|
||||||
|
);
|
||||||
|
faqItemService.writeImportTemplate(response.getOutputStream());
|
||||||
|
response.flushBuffer();
|
||||||
|
audit(apiKey, "API下载FAQ导入模板", "KNOWLEDGE_API_SHARE_ACCESS", request.getRequestURI(), Map.of("knowledgeId", knowledgeId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出 FAQ Excel。
|
||||||
|
*/
|
||||||
|
@GetMapping(value = "/faq/exportExcel", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
|
||||||
|
public void exportFaqExcel(
|
||||||
|
@RequestHeader("ApiKey") String apiKey,
|
||||||
|
@RequestParam BigInteger knowledgeId,
|
||||||
|
HttpServletRequest request,
|
||||||
|
HttpServletResponse response
|
||||||
|
) throws Exception {
|
||||||
|
assertApiShare(apiKey, request.getRequestURI(), knowledgeId, KnowledgeShareActionScope.IMPORT_EXPORT.name());
|
||||||
|
requireFaqKnowledge(knowledgeId);
|
||||||
|
String fileName = "faq_export_" + new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()) + ".xlsx";
|
||||||
|
response.setContentType("application/octet-stream");
|
||||||
|
response.setHeader(
|
||||||
|
"Content-disposition",
|
||||||
|
"attachment;filename*=utf-8''" + URLEncoder.encode(fileName, StandardCharsets.UTF_8)
|
||||||
|
);
|
||||||
|
faqItemService.exportToExcel(knowledgeId, response.getOutputStream());
|
||||||
|
response.flushBuffer();
|
||||||
|
audit(apiKey, "API导出FAQ Excel", "KNOWLEDGE_API_SHARE_ACCESS", request.getRequestURI(), Map.of("knowledgeId", knowledgeId));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertApiShare(String apiKey, String requestUri, BigInteger knowledgeId, String actionScope) {
|
||||||
|
SysApiKey sysApiKey = sysApiKeyService.getSysApiKey(apiKey);
|
||||||
|
knowledgeSharePermissionService.assertApiShare(sysApiKey.getId(), requestUri, knowledgeId, actionScope);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 断言知识库为文档类型。
|
||||||
|
*
|
||||||
|
* @param knowledgeId 知识库ID
|
||||||
|
* @return 知识库实体
|
||||||
|
*/
|
||||||
|
private DocumentCollection requireDocumentKnowledge(BigInteger knowledgeId) {
|
||||||
|
DocumentCollection knowledge = requireKnowledge(knowledgeId);
|
||||||
|
if (!knowledge.isDocumentCollection()) {
|
||||||
|
throw new BusinessException("当前知识库类型不支持文档接口");
|
||||||
|
}
|
||||||
|
return knowledge;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 断言知识库为 FAQ 类型。
|
||||||
|
*
|
||||||
|
* @param knowledgeId 知识库ID
|
||||||
|
* @return 知识库实体
|
||||||
|
*/
|
||||||
|
private DocumentCollection requireFaqKnowledge(BigInteger knowledgeId) {
|
||||||
|
DocumentCollection knowledge = requireKnowledge(knowledgeId);
|
||||||
|
if (!knowledge.isFaqCollection()) {
|
||||||
|
throw new BusinessException("当前知识库类型不支持FAQ接口");
|
||||||
|
}
|
||||||
|
return knowledge;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取知识库并保证存在。
|
||||||
|
*
|
||||||
|
* @param knowledgeId 知识库ID
|
||||||
|
* @return 知识库实体
|
||||||
|
*/
|
||||||
|
private DocumentCollection requireKnowledge(BigInteger knowledgeId) {
|
||||||
|
DocumentCollection knowledge = documentCollectionService.getById(knowledgeId);
|
||||||
|
if (knowledge == null) {
|
||||||
|
throw new BusinessException("知识库不存在");
|
||||||
|
}
|
||||||
|
return knowledge;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Document requireDocument(BigInteger documentId, BigInteger knowledgeId) {
|
||||||
|
Document document = documentService.getById(documentId);
|
||||||
|
if (document == null || document.getCollectionId() == null || document.getCollectionId().compareTo(knowledgeId) != 0) {
|
||||||
|
throw new BusinessException("文档不存在");
|
||||||
|
}
|
||||||
|
return document;
|
||||||
|
}
|
||||||
|
|
||||||
|
private DocumentChunk requireDocumentChunk(BigInteger chunkId, BigInteger knowledgeId) {
|
||||||
|
DocumentChunk chunk = documentChunkService.getById(chunkId);
|
||||||
|
if (chunk == null || chunk.getDocumentCollectionId() == null || chunk.getDocumentCollectionId().compareTo(knowledgeId) != 0) {
|
||||||
|
throw new BusinessException("记录不存在");
|
||||||
|
}
|
||||||
|
return chunk;
|
||||||
|
}
|
||||||
|
|
||||||
|
private FaqItem requireFaq(BigInteger faqId, BigInteger knowledgeId) {
|
||||||
|
FaqItem faqItem = faqItemService.getById(faqId);
|
||||||
|
if (faqItem == null || faqItem.getCollectionId() == null || faqItem.getCollectionId().compareTo(knowledgeId) != 0) {
|
||||||
|
throw new BusinessException("FAQ不存在");
|
||||||
|
}
|
||||||
|
return faqItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void audit(String apiKey, String actionName, String actionType, String actionUrl, Map<String, Object> detail) {
|
||||||
|
SysApiKey sysApiKey = sysApiKeyService.getSysApiKey(apiKey);
|
||||||
|
Map<String, Object> payload = new HashMap<>(detail);
|
||||||
|
payload.put("apiKeyId", sysApiKey.getId());
|
||||||
|
payload.put("channel", "API");
|
||||||
|
knowledgeShareAuditService.log(null, actionName, actionType, actionUrl, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<KnowledgeSearchResultItem> toKnowledgeSearchResult(List<com.easyagents.core.document.Document> documents) {
|
||||||
|
List<KnowledgeSearchResultItem> result = new java.util.ArrayList<>();
|
||||||
|
for (com.easyagents.core.document.Document document : documents) {
|
||||||
|
KnowledgeSearchResultItem item = new KnowledgeSearchResultItem();
|
||||||
|
item.setContent(document.getContent());
|
||||||
|
Object renderMarkdown = document.getMetadata("renderMarkdown");
|
||||||
|
item.setRenderMarkdown(renderMarkdown == null ? null : String.valueOf(renderMarkdown));
|
||||||
|
Object sourceFileName = document.getMetadata("sourceFileName");
|
||||||
|
item.setSourceFileName(sourceFileName == null ? null : String.valueOf(sourceFileName));
|
||||||
|
item.setScore(document.getScore());
|
||||||
|
Object hitSource = document.getMetadata("hitSource");
|
||||||
|
item.setHitSource(hitSource == null ? null : String.valueOf(hitSource));
|
||||||
|
item.setVectorScore(asDouble(document.getMetadata("vectorScore")));
|
||||||
|
item.setKeywordScore(asDouble(document.getMetadata("keywordScore")));
|
||||||
|
result.add(item);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Double asDouble(Object value) {
|
||||||
|
if (value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (value instanceof Number number) {
|
||||||
|
return number.doubleValue();
|
||||||
|
}
|
||||||
|
return Double.parseDouble(String.valueOf(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
package tech.easyflow.publicapi.controller;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.annotation.SaIgnore;
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
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 org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
|
import tech.easyflow.ai.invoke.exception.ModelInvokeException;
|
||||||
|
import tech.easyflow.ai.invoke.mapper.OpenAiProtocolMapper;
|
||||||
|
import tech.easyflow.ai.invoke.model.UnifiedChatChunk;
|
||||||
|
import tech.easyflow.ai.invoke.model.UnifiedChatRequest;
|
||||||
|
import tech.easyflow.ai.invoke.model.UnifiedChatResponse;
|
||||||
|
import tech.easyflow.ai.invoke.protocol.openai.OpenAiChatCompletionRequest;
|
||||||
|
import tech.easyflow.ai.invoke.protocol.openai.OpenAiErrorResponse;
|
||||||
|
import tech.easyflow.ai.invoke.provider.UnifiedChatChunkObserver;
|
||||||
|
import tech.easyflow.ai.invoke.service.UnifiedModelInvokeService;
|
||||||
|
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||||
|
import tech.easyflow.system.service.SysApiKeyService;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
|
@SaIgnore
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/v1")
|
||||||
|
public class PublicModelChatController {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(PublicModelChatController.class);
|
||||||
|
private static final long SSE_TIMEOUT = Duration.ofMinutes(10).toMillis();
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private SysApiKeyService sysApiKeyService;
|
||||||
|
@Resource
|
||||||
|
private UnifiedModelInvokeService unifiedModelInvokeService;
|
||||||
|
@Resource
|
||||||
|
private OpenAiProtocolMapper openAiProtocolMapper;
|
||||||
|
@Resource
|
||||||
|
private ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统一模型调用(OpenAI Chat Completions)
|
||||||
|
*/
|
||||||
|
@PostMapping(
|
||||||
|
value = "/chat/completions",
|
||||||
|
produces = {
|
||||||
|
MediaType.APPLICATION_JSON_VALUE,
|
||||||
|
MediaType.TEXT_EVENT_STREAM_VALUE
|
||||||
|
}
|
||||||
|
)
|
||||||
|
public Object chatCompletions(@RequestBody String rawBody, HttpServletRequest request) {
|
||||||
|
try {
|
||||||
|
String apiKey = resolveApiKey(request.getHeader(HttpHeaders.AUTHORIZATION));
|
||||||
|
sysApiKeyService.checkApikeyPermission(apiKey, request.getRequestURI());
|
||||||
|
|
||||||
|
OpenAiChatCompletionRequest openAiRequest = openAiProtocolMapper.readRequest(rawBody);
|
||||||
|
UnifiedChatRequest unifiedRequest = openAiProtocolMapper.toUnifiedRequest(openAiRequest);
|
||||||
|
if (Boolean.TRUE.equals(unifiedRequest.getStream())) {
|
||||||
|
return createStreamEmitter(unifiedRequest);
|
||||||
|
}
|
||||||
|
UnifiedChatResponse response = unifiedModelInvokeService.chat(unifiedRequest);
|
||||||
|
return buildJsonResponse(openAiProtocolMapper.toOpenAiResponse(response));
|
||||||
|
} catch (ModelInvokeException e) {
|
||||||
|
return buildErrorResponse(e);
|
||||||
|
} catch (BusinessException e) {
|
||||||
|
return buildErrorResponse(mapBusinessException(e));
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("chatCompletions unexpected error: {}", e.getMessage(), e);
|
||||||
|
return buildErrorResponse(new ModelInvokeException(
|
||||||
|
500,
|
||||||
|
"统一模型调用失败: " + e.getMessage(),
|
||||||
|
"api_error",
|
||||||
|
null,
|
||||||
|
"internal_error"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private SseEmitter createStreamEmitter(UnifiedChatRequest unifiedRequest) {
|
||||||
|
SseEmitter emitter = new SseEmitter(SSE_TIMEOUT);
|
||||||
|
unifiedModelInvokeService.chatStream(unifiedRequest, new UnifiedChatChunkObserver() {
|
||||||
|
@Override
|
||||||
|
public void onChunk(UnifiedChatChunk chunk) {
|
||||||
|
try {
|
||||||
|
String payload = objectMapper.writeValueAsString(openAiProtocolMapper.toOpenAiChunk(chunk));
|
||||||
|
emitter.send(SseEmitter.event().data(payload));
|
||||||
|
} catch (IOException e) {
|
||||||
|
emitter.completeWithError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onComplete() {
|
||||||
|
try {
|
||||||
|
emitter.send(SseEmitter.event().data("[DONE]"));
|
||||||
|
emitter.complete();
|
||||||
|
} catch (IOException e) {
|
||||||
|
emitter.completeWithError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(Throwable throwable) {
|
||||||
|
log.error("chatCompletions stream error: {}", throwable.getMessage(), throwable);
|
||||||
|
emitter.completeWithError(throwable);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return emitter;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ResponseEntity<String> buildJsonResponse(Object body) {
|
||||||
|
try {
|
||||||
|
return ResponseEntity.ok()
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.body(objectMapper.writeValueAsString(body));
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("序列化响应失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ResponseEntity<String> buildErrorResponse(ModelInvokeException e) {
|
||||||
|
OpenAiErrorResponse response = new OpenAiErrorResponse();
|
||||||
|
OpenAiErrorResponse.Error error = new OpenAiErrorResponse.Error();
|
||||||
|
error.setMessage(e.getMessage());
|
||||||
|
error.setType(e.getType());
|
||||||
|
error.setParam(e.getParam());
|
||||||
|
error.setCode(e.getCode());
|
||||||
|
response.setError(error);
|
||||||
|
return ResponseEntity.status(e.getStatus())
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.body(writeJson(response));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String writeJson(Object body) {
|
||||||
|
try {
|
||||||
|
return objectMapper.writeValueAsString(body);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.error("writeJson error: {}", ex.getMessage(), ex);
|
||||||
|
return "{\"error\":{\"message\":\"响应序列化失败\",\"type\":\"api_error\",\"code\":\"serialization_error\"}}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ModelInvokeException mapBusinessException(BusinessException e) {
|
||||||
|
String message = StrUtil.blankToDefault(e.getMessage(), "访问令牌校验失败");
|
||||||
|
if (StrUtil.containsAnyIgnoreCase(message, "apikey 不存在", "apikey 已过期", "已禁用")) {
|
||||||
|
return ModelInvokeException.unauthorized(message);
|
||||||
|
}
|
||||||
|
if (StrUtil.containsAnyIgnoreCase(message, "无权限", "接口不存在")) {
|
||||||
|
return ModelInvokeException.forbidden(message);
|
||||||
|
}
|
||||||
|
return ModelInvokeException.badRequest(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveApiKey(String authorizationHeader) {
|
||||||
|
if (StrUtil.isBlank(authorizationHeader)) {
|
||||||
|
throw ModelInvokeException.unauthorized("Authorization 不能为空");
|
||||||
|
}
|
||||||
|
String trimmed = authorizationHeader.trim();
|
||||||
|
if (StrUtil.startWithIgnoreCase(trimmed, "Bearer ")) {
|
||||||
|
trimmed = trimmed.substring(7).trim();
|
||||||
|
}
|
||||||
|
if (StrUtil.isBlank(trimmed)) {
|
||||||
|
throw ModelInvokeException.unauthorized("Authorization 无效");
|
||||||
|
}
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,22 +1,34 @@
|
|||||||
package tech.easyflow.publicapi.controller;
|
package tech.easyflow.publicapi.controller;
|
||||||
|
|
||||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
import com.easyagents.flow.core.chain.ChainDefinition;
|
import cn.dev33.satoken.stp.StpUtil;
|
||||||
import com.easyagents.flow.core.chain.Parameter;
|
|
||||||
import com.easyagents.flow.core.chain.runtime.ChainExecutor;
|
import com.easyagents.flow.core.chain.runtime.ChainExecutor;
|
||||||
import com.easyagents.flow.core.parser.ChainParser;
|
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import tech.easyflow.approval.annotation.RequirePublishedAccess;
|
||||||
import tech.easyflow.ai.easyagentsflow.entity.ChainInfo;
|
import tech.easyflow.ai.easyagentsflow.entity.ChainInfo;
|
||||||
import tech.easyflow.ai.easyagentsflow.entity.NodeInfo;
|
import tech.easyflow.ai.easyagentsflow.entity.NodeInfo;
|
||||||
import tech.easyflow.ai.easyagentsflow.entity.WorkflowCheckStage;
|
import tech.easyflow.ai.easyagentsflow.entity.WorkflowCheckStage;
|
||||||
import tech.easyflow.ai.easyagentsflow.service.TinyFlowService;
|
import tech.easyflow.ai.easyagentsflow.service.TinyFlowService;
|
||||||
import tech.easyflow.ai.easyagentsflow.service.WorkflowCheckService;
|
import tech.easyflow.ai.easyagentsflow.service.WorkflowCheckService;
|
||||||
|
import tech.easyflow.ai.easyagentsflow.service.WorkflowRunningParameterResolver;
|
||||||
|
import tech.easyflow.ai.easyagentsflow.support.PublishedWorkflowDefinitionIds;
|
||||||
import tech.easyflow.ai.entity.Workflow;
|
import tech.easyflow.ai.entity.Workflow;
|
||||||
|
import tech.easyflow.ai.entity.WorkflowExecResult;
|
||||||
|
import tech.easyflow.ai.enums.PublishStatus;
|
||||||
|
import tech.easyflow.ai.service.WorkflowExecResultService;
|
||||||
import tech.easyflow.ai.service.WorkflowService;
|
import tech.easyflow.ai.service.WorkflowService;
|
||||||
|
import tech.easyflow.ai.service.WorkflowApiPermissionService;
|
||||||
|
import tech.easyflow.ai.utils.WorkFlowUtil;
|
||||||
|
import tech.easyflow.common.constant.Constants;
|
||||||
import tech.easyflow.common.domain.Result;
|
import tech.easyflow.common.domain.Result;
|
||||||
|
import tech.easyflow.common.entity.LoginAccount;
|
||||||
|
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||||
|
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||||
|
import tech.easyflow.system.entity.SysApiKey;
|
||||||
|
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@@ -35,11 +47,15 @@ public class PublicWorkflowController {
|
|||||||
@Resource
|
@Resource
|
||||||
private ChainExecutor chainExecutor;
|
private ChainExecutor chainExecutor;
|
||||||
@Resource
|
@Resource
|
||||||
private ChainParser chainParser;
|
|
||||||
@Resource
|
|
||||||
private TinyFlowService tinyFlowService;
|
private TinyFlowService tinyFlowService;
|
||||||
@Resource
|
@Resource
|
||||||
private WorkflowCheckService workflowCheckService;
|
private WorkflowCheckService workflowCheckService;
|
||||||
|
@Resource
|
||||||
|
private WorkflowRunningParameterResolver workflowRunningParameterResolver;
|
||||||
|
@Resource
|
||||||
|
private WorkflowApiPermissionService workflowApiPermissionService;
|
||||||
|
@Resource
|
||||||
|
private WorkflowExecResultService workflowExecResultService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通过id或别名获取工作流详情
|
* 通过id或别名获取工作流详情
|
||||||
@@ -48,10 +64,14 @@ public class PublicWorkflowController {
|
|||||||
* @return 工作流详情
|
* @return 工作流详情
|
||||||
*/
|
*/
|
||||||
@GetMapping(value = "/getByIdOrAlias")
|
@GetMapping(value = "/getByIdOrAlias")
|
||||||
|
@RequirePublishedAccess(resourceType = "WORKFLOW", idExpr = "#key", denyMessage = "工作流尚未发布")
|
||||||
public Result<Workflow> getByIdOrAlias(
|
public Result<Workflow> getByIdOrAlias(
|
||||||
@RequestParam
|
@RequestParam
|
||||||
@NotBlank(message = "key不能为空") String key) {
|
@NotBlank(message = "key不能为空") String key,
|
||||||
Workflow workflow = workflowService.getDetail(key);
|
HttpServletRequest request) {
|
||||||
|
workflowApiPermissionService.assertWorkflowApi(request.getHeader("ApiKey"), request.getRequestURI());
|
||||||
|
Workflow workflow = workflowService.getPublishedDetail(key);
|
||||||
|
assertStrictPublishedWorkflow(workflow);
|
||||||
return Result.ok(workflow);
|
return Result.ok(workflow);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,6 +89,13 @@ public class PublicWorkflowController {
|
|||||||
if (workflow == null) {
|
if (workflow == null) {
|
||||||
return Result.fail(1, "工作流不存在");
|
return Result.fail(1, "工作流不存在");
|
||||||
}
|
}
|
||||||
|
if (variables == null) {
|
||||||
|
variables = new HashMap<>();
|
||||||
|
}
|
||||||
|
variables = workflowRunningParameterResolver.normalizeRuntimeVariables(workflow.getContent(), variables);
|
||||||
|
if (StpUtil.isLogin()) {
|
||||||
|
variables.put(Constants.LOGIN_USER_KEY, SaTokenUtil.getLoginAccount());
|
||||||
|
}
|
||||||
Map<String, Object> res = chainExecutor.executeNode(workflowId.toString(), nodeId, variables);
|
Map<String, Object> res = chainExecutor.executeNode(workflowId.toString(), nodeId, variables);
|
||||||
return Result.ok(res);
|
return Result.ok(res);
|
||||||
}
|
}
|
||||||
@@ -77,18 +104,21 @@ public class PublicWorkflowController {
|
|||||||
* 运行工作流 - v2
|
* 运行工作流 - v2
|
||||||
*/
|
*/
|
||||||
@PostMapping("/runAsync")
|
@PostMapping("/runAsync")
|
||||||
@SaCheckPermission("/api/v1/workflow/save")
|
@RequirePublishedAccess(resourceType = "WORKFLOW", idExpr = "#id", denyMessage = "工作流尚未发布")
|
||||||
public Result<String> runAsync(@JsonBody(value = "id", required = true) BigInteger id,
|
public Result<String> runAsync(@JsonBody(value = "id", required = true) BigInteger id,
|
||||||
@JsonBody("variables") Map<String, Object> variables) {
|
@JsonBody("variables") Map<String, Object> variables,
|
||||||
|
HttpServletRequest request) {
|
||||||
|
SysApiKey apiKey = workflowApiPermissionService.assertWorkflowApi(request.getHeader("ApiKey"), request.getRequestURI());
|
||||||
if (variables == null) {
|
if (variables == null) {
|
||||||
variables = new HashMap<>();
|
variables = new HashMap<>();
|
||||||
}
|
}
|
||||||
Workflow workflow = workflowService.getById(id);
|
Workflow workflow = workflowService.getPublishedById(id);
|
||||||
if (workflow == null) {
|
assertStrictPublishedWorkflow(workflow);
|
||||||
throw new RuntimeException("工作流不存在");
|
|
||||||
}
|
|
||||||
workflowCheckService.checkOrThrow(workflow.getContent(), WorkflowCheckStage.PRE_EXECUTE, workflow.getId());
|
workflowCheckService.checkOrThrow(workflow.getContent(), WorkflowCheckStage.PRE_EXECUTE, workflow.getId());
|
||||||
String executeId = chainExecutor.executeAsync(id.toString(), variables);
|
variables = workflowRunningParameterResolver.normalizeRuntimeVariables(workflow.getContent(), variables);
|
||||||
|
variables.put(Constants.LOGIN_USER_KEY, buildApiKeyLoginAccount(apiKey));
|
||||||
|
variables.put(WorkFlowUtil.CREATED_KEY_MEMORY_KEY, WorkFlowUtil.API_KEY);
|
||||||
|
String executeId = chainExecutor.executeAsync(PublishedWorkflowDefinitionIds.published(id.toString()), variables);
|
||||||
return Result.ok(executeId);
|
return Result.ok(executeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,7 +127,10 @@ public class PublicWorkflowController {
|
|||||||
*/
|
*/
|
||||||
@PostMapping("/getChainStatus")
|
@PostMapping("/getChainStatus")
|
||||||
public Result<ChainInfo> getChainStatus(@JsonBody(value = "executeId") String executeId,
|
public Result<ChainInfo> getChainStatus(@JsonBody(value = "executeId") String executeId,
|
||||||
@JsonBody("nodes") List<NodeInfo> nodes) {
|
@JsonBody("nodes") List<NodeInfo> nodes,
|
||||||
|
HttpServletRequest request) {
|
||||||
|
SysApiKey apiKey = workflowApiPermissionService.assertWorkflowApi(request.getHeader("ApiKey"), request.getRequestURI());
|
||||||
|
assertApiKeyExecutionOwnership(apiKey, executeId);
|
||||||
ChainInfo res = tinyFlowService.getChainStatus(executeId, nodes);
|
ChainInfo res = tinyFlowService.getChainStatus(executeId, nodes);
|
||||||
return Result.ok(res);
|
return Result.ok(res);
|
||||||
}
|
}
|
||||||
@@ -106,33 +139,96 @@ public class PublicWorkflowController {
|
|||||||
* 恢复工作流运行 - v2
|
* 恢复工作流运行 - v2
|
||||||
*/
|
*/
|
||||||
@PostMapping("/resume")
|
@PostMapping("/resume")
|
||||||
@SaCheckPermission("/api/v1/workflow/save")
|
|
||||||
public Result<Void> resume(@JsonBody(value = "executeId", required = true) String executeId,
|
public Result<Void> resume(@JsonBody(value = "executeId", required = true) String executeId,
|
||||||
@JsonBody("confirmParams") Map<String, Object> confirmParams) {
|
@JsonBody("confirmParams") Map<String, Object> confirmParams,
|
||||||
|
HttpServletRequest request) {
|
||||||
|
SysApiKey apiKey = workflowApiPermissionService.assertWorkflowApi(request.getHeader("ApiKey"), request.getRequestURI());
|
||||||
|
WorkflowExecResult execResult = assertApiKeyExecutionOwnership(apiKey, executeId);
|
||||||
|
assertWorkflowExecutionResumable(execResult);
|
||||||
chainExecutor.resumeAsync(executeId, confirmParams);
|
chainExecutor.resumeAsync(executeId, confirmParams);
|
||||||
return Result.ok();
|
return Result.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("getRunningParameters")
|
@GetMapping("getRunningParameters")
|
||||||
@SaCheckPermission("/api/v1/workflow/query")
|
@RequirePublishedAccess(resourceType = "WORKFLOW", idExpr = "#id", denyMessage = "工作流尚未发布")
|
||||||
public Result<?> getRunningParameters(@RequestParam BigInteger id) {
|
public Result<?> getRunningParameters(@RequestParam BigInteger id, HttpServletRequest request) {
|
||||||
Workflow workflow = workflowService.getById(id);
|
workflowApiPermissionService.assertWorkflowApi(request.getHeader("ApiKey"), request.getRequestURI());
|
||||||
|
Workflow workflow = workflowService.getPublishedById(id);
|
||||||
|
|
||||||
if (workflow == null) {
|
assertStrictPublishedWorkflow(workflow);
|
||||||
return Result.fail(1, "can not find the workflow by id: " + id);
|
|
||||||
}
|
|
||||||
workflowCheckService.checkOrThrow(workflow.getContent(), WorkflowCheckStage.PRE_EXECUTE, workflow.getId());
|
workflowCheckService.checkOrThrow(workflow.getContent(), WorkflowCheckStage.PRE_EXECUTE, workflow.getId());
|
||||||
|
Map<String, Object> res = workflowRunningParameterResolver.buildRunningParametersView(workflow);
|
||||||
ChainDefinition definition = chainParser.parse(workflow.getContent());
|
if (res == null) {
|
||||||
if (definition == null) {
|
|
||||||
return Result.fail(2, "节点配置错误,请检查! ");
|
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);
|
return Result.ok(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建 API Key 调用方的运行身份。
|
||||||
|
*
|
||||||
|
* @param apiKey 访问令牌
|
||||||
|
* @return 工作流运行身份
|
||||||
|
*/
|
||||||
|
private LoginAccount buildApiKeyLoginAccount(SysApiKey apiKey) {
|
||||||
|
LoginAccount account = new LoginAccount();
|
||||||
|
account.setId(apiKey.getId());
|
||||||
|
account.setDeptId(apiKey.getDeptId() == null ? BigInteger.ZERO : apiKey.getDeptId());
|
||||||
|
account.setTenantId(apiKey.getTenantId() == null ? BigInteger.ZERO : apiKey.getTenantId());
|
||||||
|
account.setLoginName("apikey:" + apiKey.getId());
|
||||||
|
account.setNickname("API 调用方");
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验工作流 Public API 只能访问严格已发布且存在发布快照的工作流。
|
||||||
|
*
|
||||||
|
* @param workflow 工作流发布视图
|
||||||
|
*/
|
||||||
|
private void assertStrictPublishedWorkflow(Workflow workflow) {
|
||||||
|
if (workflow == null || !PublishStatus.PUBLISHED.getCode().equals(workflow.getPublishStatus())
|
||||||
|
|| workflow.getPublishedSnapshotJson() == null || workflow.getPublishedSnapshotJson().isEmpty()) {
|
||||||
|
throw new BusinessException("工作流尚未发布");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验 Public Workflow API 后续操作只能作用于当前 API Key 发起的执行实例。
|
||||||
|
*
|
||||||
|
* @param apiKey 当前 API Key
|
||||||
|
* @param executeId 执行 ID
|
||||||
|
* @return 已通过归属校验的执行记录
|
||||||
|
*/
|
||||||
|
private WorkflowExecResult assertApiKeyExecutionOwnership(SysApiKey apiKey, String executeId) {
|
||||||
|
if (executeId == null || executeId.isBlank()) {
|
||||||
|
throw new BusinessException("执行ID不能为空");
|
||||||
|
}
|
||||||
|
WorkflowExecResult execResult = workflowExecResultService.getByExecKey(executeId);
|
||||||
|
if (execResult == null) {
|
||||||
|
throw new BusinessException("工作流执行记录不存在,请稍后重试");
|
||||||
|
}
|
||||||
|
if (!WorkFlowUtil.API_KEY.equals(execResult.getCreatedKey())
|
||||||
|
|| apiKey == null
|
||||||
|
|| apiKey.getId() == null
|
||||||
|
|| !String.valueOf(apiKey.getId()).equals(execResult.getCreatedBy())) {
|
||||||
|
throw new BusinessException("无权限访问当前工作流执行记录");
|
||||||
|
}
|
||||||
|
return execResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验当前执行实例是否仍允许恢复。
|
||||||
|
*
|
||||||
|
* @param execResult 执行记录
|
||||||
|
*/
|
||||||
|
private void assertWorkflowExecutionResumable(WorkflowExecResult execResult) {
|
||||||
|
if (execResult == null || execResult.getWorkflowId() == null) {
|
||||||
|
throw new BusinessException("工作流执行记录不存在,请稍后重试");
|
||||||
|
}
|
||||||
|
Workflow workflow = workflowService.getById(execResult.getWorkflowId());
|
||||||
|
if (workflow == null || !PublishStatus.PUBLISHED.getCode().equals(workflow.getPublishStatus())
|
||||||
|
|| workflow.getPublishedSnapshotJson() == null || workflow.getPublishedSnapshotJson().isEmpty()) {
|
||||||
|
throw new BusinessException("工作流已下线或不可恢复执行");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,10 +20,14 @@
|
|||||||
<groupId>tech.easyflow</groupId>
|
<groupId>tech.easyflow</groupId>
|
||||||
<artifactId>easyflow-module-ai</artifactId>
|
<artifactId>easyflow-module-ai</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>tech.easyflow</groupId>
|
||||||
|
<artifactId>easyflow-module-chatlog</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>tech.easyflow</groupId>
|
<groupId>tech.easyflow</groupId>
|
||||||
<artifactId>easyflow-common-captcha</artifactId>
|
<artifactId>easyflow-common-captcha</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
|
|||||||
@@ -1,11 +1,20 @@
|
|||||||
package tech.easyflow.usercenter.controller.ai;
|
package tech.easyflow.usercenter.controller.ai;
|
||||||
|
|
||||||
|
import com.mybatisflex.core.query.QueryWrapper;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import tech.easyflow.ai.entity.BotCategory;
|
import tech.easyflow.ai.entity.BotCategory;
|
||||||
import tech.easyflow.ai.service.BotCategoryService;
|
import tech.easyflow.ai.service.BotCategoryService;
|
||||||
import tech.easyflow.common.annotation.UsePermission;
|
import tech.easyflow.common.annotation.UsePermission;
|
||||||
|
import tech.easyflow.common.domain.Result;
|
||||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||||
|
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
|
||||||
|
import tech.easyflow.system.service.CategoryPermissionService;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* bot分类 控制层。
|
* bot分类 控制层。
|
||||||
@@ -17,7 +26,24 @@ import tech.easyflow.common.web.controller.BaseCurdController;
|
|||||||
@RequestMapping("/userCenter/botCategory")
|
@RequestMapping("/userCenter/botCategory")
|
||||||
@UsePermission(moduleName = "/api/v1/bot")
|
@UsePermission(moduleName = "/api/v1/bot")
|
||||||
public class UcBotCategoryController extends BaseCurdController<BotCategoryService, BotCategory> {
|
public class UcBotCategoryController extends BaseCurdController<BotCategoryService, BotCategory> {
|
||||||
|
@Resource
|
||||||
|
private CategoryPermissionService categoryPermissionService;
|
||||||
|
|
||||||
public UcBotCategoryController(BotCategoryService service) {
|
public UcBotCategoryController(BotCategoryService service) {
|
||||||
super(service);
|
super(service);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
@GetMapping("visibleList")
|
||||||
|
public Result<List<BotCategory>> visibleList(BotCategory entity, Boolean asTree, String sortKey, String sortType) {
|
||||||
|
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
|
||||||
|
RoleCategoryAccessSnapshot access = categoryPermissionService.getCurrentAccess("BOT");
|
||||||
|
if (access.isRestricted()) {
|
||||||
|
if (access.getCategoryIds().isEmpty()) {
|
||||||
|
return Result.ok(Collections.emptyList());
|
||||||
|
}
|
||||||
|
queryWrapper.in(BotCategory::getId, access.getCategoryIds());
|
||||||
|
}
|
||||||
|
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||||
|
return Result.ok(service.list(queryWrapper));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ package tech.easyflow.usercenter.controller.ai;
|
|||||||
|
|
||||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
import cn.dev33.satoken.annotation.SaIgnore;
|
import cn.dev33.satoken.annotation.SaIgnore;
|
||||||
|
import cn.dev33.satoken.stp.StpUtil;
|
||||||
import com.alicp.jetcache.Cache;
|
import com.alicp.jetcache.Cache;
|
||||||
|
import com.mybatisflex.core.paginate.Page;
|
||||||
import com.mybatisflex.core.keygen.impl.SnowFlakeIDKeyGenerator;
|
import com.mybatisflex.core.keygen.impl.SnowFlakeIDKeyGenerator;
|
||||||
import com.mybatisflex.core.query.QueryWrapper;
|
import com.mybatisflex.core.query.QueryWrapper;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@@ -15,16 +17,24 @@ import org.springframework.util.StringUtils;
|
|||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
|
import tech.easyflow.ai.chattime.availability.ChatTimeToolAvailabilityContext;
|
||||||
import tech.easyflow.ai.entity.*;
|
import tech.easyflow.ai.entity.*;
|
||||||
import tech.easyflow.ai.service.*;
|
import tech.easyflow.ai.service.*;
|
||||||
import tech.easyflow.ai.service.impl.BotServiceImpl;
|
import tech.easyflow.ai.service.impl.BotServiceImpl;
|
||||||
import tech.easyflow.common.annotation.UsePermission;
|
import tech.easyflow.common.annotation.UsePermission;
|
||||||
import tech.easyflow.common.audio.core.AudioServiceManager;
|
import tech.easyflow.common.audio.core.AudioServiceManager;
|
||||||
import tech.easyflow.common.domain.Result;
|
import tech.easyflow.common.domain.Result;
|
||||||
|
import tech.easyflow.common.entity.LoginAccount;
|
||||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||||
|
import tech.easyflow.chatlog.service.ChatRoundOperateService;
|
||||||
|
import tech.easyflow.core.runtime.ChatChannel;
|
||||||
|
import tech.easyflow.core.runtime.ChatRuntimeExtKeys;
|
||||||
|
import tech.easyflow.core.runtime.ChatRuntimeContext;
|
||||||
|
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
|
||||||
|
import tech.easyflow.system.service.CategoryPermissionService;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
@@ -34,6 +44,8 @@ import java.util.HashMap;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static tech.easyflow.ai.entity.table.BotTableDef.BOT;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 控制层。
|
* 控制层。
|
||||||
*
|
*
|
||||||
@@ -57,6 +69,8 @@ public class UcBotController extends BaseCurdController<BotService, Bot> {
|
|||||||
private Cache<String, Object> cache;
|
private Cache<String, Object> cache;
|
||||||
@Resource
|
@Resource
|
||||||
private AudioServiceManager audioServiceManager;
|
private AudioServiceManager audioServiceManager;
|
||||||
|
@Resource
|
||||||
|
private ChatRoundOperateService chatRoundOperateService;
|
||||||
|
|
||||||
public UcBotController(BotService service, ModelService modelService, BotWorkflowService botWorkflowService,
|
public UcBotController(BotService service, ModelService modelService, BotWorkflowService botWorkflowService,
|
||||||
BotDocumentCollectionService botDocumentCollectionService) {
|
BotDocumentCollectionService botDocumentCollectionService) {
|
||||||
@@ -69,7 +83,7 @@ public class UcBotController extends BaseCurdController<BotService, Bot> {
|
|||||||
@Resource
|
@Resource
|
||||||
private BotPluginService botPluginService;
|
private BotPluginService botPluginService;
|
||||||
@Resource
|
@Resource
|
||||||
private BotConversationService conversationMessageService;
|
private CategoryPermissionService categoryPermissionService;
|
||||||
|
|
||||||
@GetMapping("/generateConversationId")
|
@GetMapping("/generateConversationId")
|
||||||
public Result<Long> generateConversationId() {
|
public Result<Long> generateConversationId() {
|
||||||
@@ -143,37 +157,30 @@ public class UcBotController extends BaseCurdController<BotService, Bot> {
|
|||||||
@JsonBody(value = "botId", required = true) BigInteger botId,
|
@JsonBody(value = "botId", required = true) BigInteger botId,
|
||||||
@JsonBody(value = "conversationId", required = true) BigInteger conversationId,
|
@JsonBody(value = "conversationId", required = true) BigInteger conversationId,
|
||||||
@JsonBody(value = "messages") List<Map<String, String>> messages,
|
@JsonBody(value = "messages") List<Map<String, String>> messages,
|
||||||
@JsonBody(value = "attachments") List<String> attachments
|
@JsonBody(value = "attachments") List<String> attachments,
|
||||||
|
@JsonBody(value = "regenerateRoundId") BigInteger regenerateRoundId
|
||||||
|
|
||||||
) {
|
) {
|
||||||
BotServiceImpl.ChatCheckResult chatCheckResult = new BotServiceImpl.ChatCheckResult();
|
BotServiceImpl.ChatCheckResult chatCheckResult = new BotServiceImpl.ChatCheckResult();
|
||||||
|
if (regenerateRoundId != null) {
|
||||||
|
chatRoundOperateService.requireRegeneratableRound(conversationId, regenerateRoundId);
|
||||||
|
}
|
||||||
|
|
||||||
// 前置校验:失败则直接返回错误SseEmitter
|
// 前置校验:失败则直接返回错误SseEmitter
|
||||||
SseEmitter errorEmitter = botService.checkChatBeforeStart(botId, prompt, conversationId.toString(), chatCheckResult);
|
SseEmitter errorEmitter = botService.checkChatBeforeStart(botId, prompt, conversationId.toString(), chatCheckResult, regenerateRoundId);
|
||||||
if (errorEmitter != null) {
|
if (errorEmitter != null) {
|
||||||
return errorEmitter;
|
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());
|
|
||||||
try {
|
|
||||||
conversationMessageService.save(conversation);
|
|
||||||
} catch (DuplicateKeyException e) {
|
|
||||||
// 并发重试场景下允许重复创建请求,唯一主键冲突按已创建处理。
|
|
||||||
log.debug("conversation already exists, conversationId={}", conversationId, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return botService.startChat(botId, prompt, conversationId, messages, chatCheckResult, attachments);
|
return botService.startChat(
|
||||||
|
botId,
|
||||||
|
prompt,
|
||||||
|
conversationId,
|
||||||
|
messages,
|
||||||
|
chatCheckResult,
|
||||||
|
attachments,
|
||||||
|
buildRuntimeContext(chatCheckResult.getAiBot(), conversationId, prompt, attachments, regenerateRoundId)
|
||||||
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,7 +195,11 @@ public class UcBotController extends BaseCurdController<BotService, Bot> {
|
|||||||
@GetMapping("getDetail")
|
@GetMapping("getDetail")
|
||||||
@SaIgnore
|
@SaIgnore
|
||||||
public Result<Bot> getDetail(String id) {
|
public Result<Bot> getDetail(String id) {
|
||||||
return Result.ok(botService.getDetail(id));
|
Bot bot = botService.getDetail(id);
|
||||||
|
if (bot != null && StpUtil.isLogin()) {
|
||||||
|
categoryPermissionService.assertCategoryResourceVisible("BOT", bot.getCreatedBy(), bot.getCategoryId(), "无权限访问聊天助手");
|
||||||
|
}
|
||||||
|
return Result.ok(bot);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -198,6 +209,9 @@ public class UcBotController extends BaseCurdController<BotService, Bot> {
|
|||||||
if (data == null) {
|
if (data == null) {
|
||||||
return Result.ok(data);
|
return Result.ok(data);
|
||||||
}
|
}
|
||||||
|
if (StpUtil.isLogin()) {
|
||||||
|
categoryPermissionService.assertCategoryResourceVisible("BOT", data.getCreatedBy(), data.getCategoryId(), "无权限访问聊天助手");
|
||||||
|
}
|
||||||
|
|
||||||
Map<String, Object> llmOptions = data.getModelOptions();
|
Map<String, Object> llmOptions = data.getModelOptions();
|
||||||
if (llmOptions == null) {
|
if (llmOptions == null) {
|
||||||
@@ -229,6 +243,32 @@ public class UcBotController extends BaseCurdController<BotService, Bot> {
|
|||||||
return Result.ok(data);
|
return Result.ok(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Result<List<Bot>> list(Bot entity, Boolean asTree, String sortKey, String sortType) {
|
||||||
|
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
|
||||||
|
applyCategoryPermission(queryWrapper);
|
||||||
|
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||||
|
return Result.ok(service.list(queryWrapper));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Page<Bot> queryPage(Page<Bot> page, QueryWrapper queryWrapper) {
|
||||||
|
applyCategoryPermission(queryWrapper);
|
||||||
|
return super.queryPage(page, queryWrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyCategoryPermission(QueryWrapper queryWrapper) {
|
||||||
|
RoleCategoryAccessSnapshot access = categoryPermissionService.getCurrentAccess("BOT");
|
||||||
|
if (!access.isRestricted()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (access.getCategoryIds().isEmpty()) {
|
||||||
|
queryWrapper.eq(Bot::getCreatedBy, access.getAccountId());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
queryWrapper.and(BOT.CREATED_BY.eq(access.getAccountId()).or(BOT.CATEGORY_ID.in(access.getCategoryIds())));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Result<?> onSaveOrUpdateBefore(Bot entity, boolean isSave) {
|
protected Result<?> onSaveOrUpdateBefore(Bot entity, boolean isSave) {
|
||||||
|
|
||||||
@@ -255,6 +295,38 @@ public class UcBotController extends BaseCurdController<BotService, Bot> {
|
|||||||
return super.onSaveOrUpdateBefore(entity, isSave);
|
return super.onSaveOrUpdateBefore(entity, isSave);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ChatRuntimeContext buildRuntimeContext(Bot bot, BigInteger conversationId, String prompt, List<String> attachments,
|
||||||
|
BigInteger regenerateRoundId) {
|
||||||
|
LoginAccount account = requireCurrentLoginAccount();
|
||||||
|
ChatRuntimeContext context = new ChatRuntimeContext();
|
||||||
|
context.setChannel(ChatChannel.USER_CENTER);
|
||||||
|
context.setSessionId(conversationId);
|
||||||
|
context.setTenantId(account.getTenantId());
|
||||||
|
context.setDeptId(account.getDeptId());
|
||||||
|
context.setUserId(account.getId());
|
||||||
|
context.setUserAccount(account.getLoginName());
|
||||||
|
context.setUserName(StringUtils.hasText(account.getNickname()) ? account.getNickname() : account.getLoginName());
|
||||||
|
context.setAssistantId(bot == null ? BigInteger.ZERO : bot.getId());
|
||||||
|
context.setAssistantCode(bot == null ? null : bot.getAlias());
|
||||||
|
context.setAssistantName(bot == null ? null : bot.getTitle());
|
||||||
|
context.setSessionTitle(prompt.length() > 200 ? prompt.substring(0, 200) : prompt);
|
||||||
|
context.setAnonymous(false);
|
||||||
|
context.setAttachments(attachments);
|
||||||
|
if (regenerateRoundId != null) {
|
||||||
|
context.getExt().put(ChatRuntimeExtKeys.REGENERATE_ROUND_ID, regenerateRoundId);
|
||||||
|
}
|
||||||
|
ChatTimeToolAvailabilityContext.bindLoggedInSnapshot(context, account, bot);
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
private LoginAccount requireCurrentLoginAccount() {
|
||||||
|
try {
|
||||||
|
return SaTokenUtil.getLoginAccount();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new BusinessException("当前登录状态失效,请重新登录后再试");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private Map<String, Object> getDefaultLlmOptions() {
|
private Map<String, Object> getDefaultLlmOptions() {
|
||||||
Map<String, Object> defaultLlmOptions = new HashMap<>();
|
Map<String, Object> defaultLlmOptions = new HashMap<>();
|
||||||
defaultLlmOptions.put("temperature", 0.7);
|
defaultLlmOptions.put("temperature", 0.7);
|
||||||
|
|||||||
@@ -1,108 +0,0 @@
|
|||||||
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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package tech.easyflow.usercenter.controller.ai;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.annotation.SaIgnore;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import tech.easyflow.chatlog.domain.dto.ChatHistoryPage;
|
||||||
|
import tech.easyflow.chatlog.domain.dto.ChatMessageRecord;
|
||||||
|
import tech.easyflow.chatlog.domain.dto.ChatSessionPage;
|
||||||
|
import tech.easyflow.chatlog.domain.dto.ChatSessionSummary;
|
||||||
|
import tech.easyflow.chatlog.domain.query.ChatPageQuery;
|
||||||
|
import tech.easyflow.chatlog.service.ChatHistoryManageService;
|
||||||
|
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 java.math.BigInteger;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/userCenter/chatHistory")
|
||||||
|
@SaIgnore
|
||||||
|
public class UcChatHistoryController {
|
||||||
|
|
||||||
|
private final ChatHistoryManageService chatHistoryManageService;
|
||||||
|
|
||||||
|
public UcChatHistoryController(ChatHistoryManageService chatHistoryManageService) {
|
||||||
|
this.chatHistoryManageService = chatHistoryManageService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/sessions")
|
||||||
|
public Result<ChatSessionPage> listSessions(BigInteger assistantId, ChatPageQuery query) {
|
||||||
|
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||||
|
return Result.ok(chatHistoryManageService.queryUserSessions(account.getId(), assistantId, query));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/sessions/{sessionId}")
|
||||||
|
public Result<ChatSessionSummary> getSession(@PathVariable BigInteger sessionId) {
|
||||||
|
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||||
|
return Result.ok(chatHistoryManageService.getUserSession(account.getId(), sessionId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/sessions/{sessionId}/messages")
|
||||||
|
public Result<ChatHistoryPage> queryMessages(@PathVariable BigInteger sessionId, ChatPageQuery query) {
|
||||||
|
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||||
|
return Result.ok(chatHistoryManageService.queryUserMessages(account.getId(), sessionId, query));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/sessions/{sessionId}/rename")
|
||||||
|
public Result<Void> renameSession(@PathVariable BigInteger sessionId,
|
||||||
|
@JsonBody(value = "title", required = true) String title) {
|
||||||
|
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||||
|
chatHistoryManageService.renameUserSession(account.getId(), sessionId, title, account.getId());
|
||||||
|
return Result.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/sessions/{sessionId}/delete")
|
||||||
|
public Result<Void> deleteSession(@PathVariable BigInteger sessionId) {
|
||||||
|
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||||
|
chatHistoryManageService.deleteUserSession(account.getId(), sessionId, account.getId());
|
||||||
|
return Result.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/sessions/{sessionId}/rounds/{roundId}/variants")
|
||||||
|
public Result<List<ChatMessageRecord>> listRoundVariants(@PathVariable BigInteger sessionId,
|
||||||
|
@PathVariable BigInteger roundId) {
|
||||||
|
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||||
|
return Result.ok(chatHistoryManageService.listUserRoundVariants(account.getId(), sessionId, roundId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/sessions/{sessionId}/rounds/{roundId}/selectVariant")
|
||||||
|
public Result<ChatMessageRecord> selectRoundVariant(@PathVariable BigInteger sessionId,
|
||||||
|
@PathVariable BigInteger roundId,
|
||||||
|
@JsonBody(value = "variantIndex", required = true) Integer variantIndex) {
|
||||||
|
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||||
|
return Result.ok(chatHistoryManageService.selectUserRoundVariant(account.getId(), sessionId, roundId, variantIndex, account.getId()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,10 +13,15 @@ import tech.easyflow.common.domain.Result;
|
|||||||
import tech.easyflow.common.entity.LoginAccount;
|
import tech.easyflow.common.entity.LoginAccount;
|
||||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||||
|
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
|
||||||
|
import tech.easyflow.system.service.CategoryPermissionService;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static tech.easyflow.ai.entity.table.ResourceTableDef.RESOURCE;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 素材库
|
* 素材库
|
||||||
@@ -28,6 +33,9 @@ import java.util.Date;
|
|||||||
@RequestMapping("/userCenter/resource")
|
@RequestMapping("/userCenter/resource")
|
||||||
@UsePermission(moduleName = "/api/v1/resource")
|
@UsePermission(moduleName = "/api/v1/resource")
|
||||||
public class UcResourceController extends BaseCurdController<ResourceService, Resource> {
|
public class UcResourceController extends BaseCurdController<ResourceService, Resource> {
|
||||||
|
@javax.annotation.Resource
|
||||||
|
private CategoryPermissionService categoryPermissionService;
|
||||||
|
|
||||||
public UcResourceController(ResourceService service) {
|
public UcResourceController(ResourceService service) {
|
||||||
super(service);
|
super(service);
|
||||||
}
|
}
|
||||||
@@ -52,7 +60,36 @@ public class UcResourceController extends BaseCurdController<ResourceService, Re
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Page<Resource> queryPage(Page<Resource> page, QueryWrapper queryWrapper) {
|
protected Page<Resource> queryPage(Page<Resource> page, QueryWrapper queryWrapper) {
|
||||||
queryWrapper.eq(Resource::getCreatedBy, SaTokenUtil.getLoginAccount().getId().toString());
|
applyCategoryPermission(queryWrapper);
|
||||||
return super.queryPage(page, queryWrapper);
|
return super.queryPage(page, queryWrapper);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
@Override
|
||||||
|
public Result<List<Resource>> list(Resource entity, Boolean asTree, String sortKey, String sortType) {
|
||||||
|
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
|
||||||
|
applyCategoryPermission(queryWrapper);
|
||||||
|
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||||
|
return Result.ok(service.list(queryWrapper));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Result<Resource> detail(String id) {
|
||||||
|
Resource resource = service.getById(id);
|
||||||
|
if (resource != null) {
|
||||||
|
categoryPermissionService.assertCategoryResourceVisible("RESOURCE", resource.getCreatedBy(), resource.getCategoryId(), "无权限访问素材");
|
||||||
|
}
|
||||||
|
return Result.ok(resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyCategoryPermission(QueryWrapper queryWrapper) {
|
||||||
|
RoleCategoryAccessSnapshot access = categoryPermissionService.getCurrentAccess("RESOURCE");
|
||||||
|
if (!access.isRestricted()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (access.getCategoryIds().isEmpty()) {
|
||||||
|
queryWrapper.eq(Resource::getCreatedBy, access.getAccountId());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
queryWrapper.and(RESOURCE.CREATED_BY.eq(access.getAccountId()).or(RESOURCE.CATEGORY_ID.in(access.getCategoryIds())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,15 +1,22 @@
|
|||||||
package tech.easyflow.usercenter.controller.ai;
|
package tech.easyflow.usercenter.controller.ai;
|
||||||
|
|
||||||
|
import com.mybatisflex.core.query.QueryWrapper;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import tech.easyflow.ai.entity.WorkflowCategory;
|
import tech.easyflow.ai.entity.WorkflowCategory;
|
||||||
import tech.easyflow.ai.service.WorkflowCategoryService;
|
import tech.easyflow.ai.service.WorkflowCategoryService;
|
||||||
import tech.easyflow.common.annotation.UsePermission;
|
import tech.easyflow.common.annotation.UsePermission;
|
||||||
import tech.easyflow.common.domain.Result;
|
import tech.easyflow.common.domain.Result;
|
||||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||||
|
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
|
||||||
|
import tech.easyflow.system.service.CategoryPermissionService;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 工作流分类
|
* 工作流分类
|
||||||
@@ -21,6 +28,8 @@ import java.util.Collection;
|
|||||||
@RequestMapping("/userCenter/workflowCategory")
|
@RequestMapping("/userCenter/workflowCategory")
|
||||||
@UsePermission(moduleName = "/api/v1/workflow")
|
@UsePermission(moduleName = "/api/v1/workflow")
|
||||||
public class UcWorkflowCategoryController extends BaseCurdController<WorkflowCategoryService, WorkflowCategory> {
|
public class UcWorkflowCategoryController extends BaseCurdController<WorkflowCategoryService, WorkflowCategory> {
|
||||||
|
@Resource
|
||||||
|
private CategoryPermissionService categoryPermissionService;
|
||||||
|
|
||||||
public UcWorkflowCategoryController(WorkflowCategoryService service) {
|
public UcWorkflowCategoryController(WorkflowCategoryService service) {
|
||||||
super(service);
|
super(service);
|
||||||
@@ -35,4 +44,18 @@ public class UcWorkflowCategoryController extends BaseCurdController<WorkflowCat
|
|||||||
protected Result<?> onRemoveBefore(Collection<Serializable> ids) {
|
protected Result<?> onRemoveBefore(Collection<Serializable> ids) {
|
||||||
return Result.fail("-");
|
return Result.fail("-");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
@GetMapping("visibleList")
|
||||||
|
public Result<List<WorkflowCategory>> visibleList(WorkflowCategory entity, Boolean asTree, String sortKey, String sortType) {
|
||||||
|
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
|
||||||
|
RoleCategoryAccessSnapshot access = categoryPermissionService.getCurrentAccess("WORKFLOW");
|
||||||
|
if (access.isRestricted()) {
|
||||||
|
if (access.getCategoryIds().isEmpty()) {
|
||||||
|
return Result.ok(Collections.emptyList());
|
||||||
|
}
|
||||||
|
queryWrapper.in(WorkflowCategory::getId, access.getCategoryIds());
|
||||||
|
}
|
||||||
|
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||||
|
return Result.ok(service.list(queryWrapper));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,16 +2,17 @@ package tech.easyflow.usercenter.controller.ai;
|
|||||||
|
|
||||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
import cn.dev33.satoken.stp.StpUtil;
|
import cn.dev33.satoken.stp.StpUtil;
|
||||||
import com.easyagents.flow.core.chain.ChainDefinition;
|
import com.mybatisflex.core.paginate.Page;
|
||||||
import com.easyagents.flow.core.chain.Parameter;
|
import com.mybatisflex.core.query.QueryWrapper;
|
||||||
import com.easyagents.flow.core.chain.runtime.ChainExecutor;
|
import com.easyagents.flow.core.chain.runtime.ChainExecutor;
|
||||||
import com.easyagents.flow.core.parser.ChainParser;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import tech.easyflow.ai.permission.WorkflowVisibilityQueryHelper;
|
||||||
import tech.easyflow.ai.easyagentsflow.entity.ChainInfo;
|
import tech.easyflow.ai.easyagentsflow.entity.ChainInfo;
|
||||||
import tech.easyflow.ai.easyagentsflow.entity.NodeInfo;
|
import tech.easyflow.ai.easyagentsflow.entity.NodeInfo;
|
||||||
import tech.easyflow.ai.easyagentsflow.entity.WorkflowCheckStage;
|
import tech.easyflow.ai.easyagentsflow.entity.WorkflowCheckStage;
|
||||||
import tech.easyflow.ai.easyagentsflow.service.TinyFlowService;
|
import tech.easyflow.ai.easyagentsflow.service.TinyFlowService;
|
||||||
import tech.easyflow.ai.easyagentsflow.service.WorkflowCheckService;
|
import tech.easyflow.ai.easyagentsflow.service.WorkflowCheckService;
|
||||||
|
import tech.easyflow.ai.easyagentsflow.service.WorkflowRunningParameterResolver;
|
||||||
import tech.easyflow.ai.entity.Workflow;
|
import tech.easyflow.ai.entity.Workflow;
|
||||||
import tech.easyflow.ai.service.WorkflowService;
|
import tech.easyflow.ai.service.WorkflowService;
|
||||||
import tech.easyflow.common.annotation.UsePermission;
|
import tech.easyflow.common.annotation.UsePermission;
|
||||||
@@ -20,6 +21,10 @@ import tech.easyflow.common.domain.Result;
|
|||||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||||
|
import tech.easyflow.system.enums.CategoryResourceType;
|
||||||
|
import tech.easyflow.system.enums.ResourceAction;
|
||||||
|
import tech.easyflow.system.enums.ResourceLookup;
|
||||||
|
import tech.easyflow.system.permission.resource.RequireResourceAccess;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
@@ -40,11 +45,13 @@ public class UcWorkflowController extends BaseCurdController<WorkflowService, Wo
|
|||||||
@Resource
|
@Resource
|
||||||
private ChainExecutor chainExecutor;
|
private ChainExecutor chainExecutor;
|
||||||
@Resource
|
@Resource
|
||||||
private ChainParser chainParser;
|
|
||||||
@Resource
|
|
||||||
private TinyFlowService tinyFlowService;
|
private TinyFlowService tinyFlowService;
|
||||||
@Resource
|
@Resource
|
||||||
private WorkflowCheckService workflowCheckService;
|
private WorkflowCheckService workflowCheckService;
|
||||||
|
@Resource
|
||||||
|
private WorkflowRunningParameterResolver workflowRunningParameterResolver;
|
||||||
|
@Resource
|
||||||
|
private WorkflowVisibilityQueryHelper workflowVisibilityQueryHelper;
|
||||||
|
|
||||||
public UcWorkflowController(WorkflowService service) {
|
public UcWorkflowController(WorkflowService service) {
|
||||||
super(service);
|
super(service);
|
||||||
@@ -55,6 +62,13 @@ public class UcWorkflowController extends BaseCurdController<WorkflowService, Wo
|
|||||||
*/
|
*/
|
||||||
@PostMapping("/singleRun")
|
@PostMapping("/singleRun")
|
||||||
@SaCheckPermission("/api/v1/workflow/save")
|
@SaCheckPermission("/api/v1/workflow/save")
|
||||||
|
@RequireResourceAccess(
|
||||||
|
resource = CategoryResourceType.WORKFLOW,
|
||||||
|
action = ResourceAction.USE,
|
||||||
|
lookup = ResourceLookup.WORKFLOW_ID,
|
||||||
|
idExpr = "#workflowId",
|
||||||
|
denyMessage = "无权限运行工作流"
|
||||||
|
)
|
||||||
public Result<?> singleRun(
|
public Result<?> singleRun(
|
||||||
@JsonBody(value = "workflowId", required = true) BigInteger workflowId,
|
@JsonBody(value = "workflowId", required = true) BigInteger workflowId,
|
||||||
@JsonBody(value = "nodeId", required = true) String nodeId,
|
@JsonBody(value = "nodeId", required = true) String nodeId,
|
||||||
@@ -64,6 +78,13 @@ public class UcWorkflowController extends BaseCurdController<WorkflowService, Wo
|
|||||||
if (workflow == null) {
|
if (workflow == null) {
|
||||||
return Result.fail(1, "工作流不存在");
|
return Result.fail(1, "工作流不存在");
|
||||||
}
|
}
|
||||||
|
if (variables == null) {
|
||||||
|
variables = new HashMap<>();
|
||||||
|
}
|
||||||
|
variables = workflowRunningParameterResolver.normalizeRuntimeVariables(workflow.getContent(), variables);
|
||||||
|
if (StpUtil.isLogin()) {
|
||||||
|
variables.put(Constants.LOGIN_USER_KEY, SaTokenUtil.getLoginAccount());
|
||||||
|
}
|
||||||
Map<String, Object> res = chainExecutor.executeNode(workflowId.toString(), nodeId, variables);
|
Map<String, Object> res = chainExecutor.executeNode(workflowId.toString(), nodeId, variables);
|
||||||
return Result.ok(res);
|
return Result.ok(res);
|
||||||
}
|
}
|
||||||
@@ -73,6 +94,13 @@ public class UcWorkflowController extends BaseCurdController<WorkflowService, Wo
|
|||||||
*/
|
*/
|
||||||
@PostMapping("/runAsync")
|
@PostMapping("/runAsync")
|
||||||
@SaCheckPermission("/api/v1/workflow/save")
|
@SaCheckPermission("/api/v1/workflow/save")
|
||||||
|
@RequireResourceAccess(
|
||||||
|
resource = CategoryResourceType.WORKFLOW,
|
||||||
|
action = ResourceAction.USE,
|
||||||
|
lookup = ResourceLookup.WORKFLOW_ID,
|
||||||
|
idExpr = "#id",
|
||||||
|
denyMessage = "无权限运行工作流"
|
||||||
|
)
|
||||||
public Result<String> runAsync(@JsonBody(value = "id", required = true) BigInteger id,
|
public Result<String> runAsync(@JsonBody(value = "id", required = true) BigInteger id,
|
||||||
@JsonBody("variables") Map<String, Object> variables) {
|
@JsonBody("variables") Map<String, Object> variables) {
|
||||||
if (variables == null) {
|
if (variables == null) {
|
||||||
@@ -83,6 +111,7 @@ public class UcWorkflowController extends BaseCurdController<WorkflowService, Wo
|
|||||||
throw new RuntimeException("工作流不存在");
|
throw new RuntimeException("工作流不存在");
|
||||||
}
|
}
|
||||||
workflowCheckService.checkOrThrow(workflow.getContent(), WorkflowCheckStage.PRE_EXECUTE, workflow.getId());
|
workflowCheckService.checkOrThrow(workflow.getContent(), WorkflowCheckStage.PRE_EXECUTE, workflow.getId());
|
||||||
|
variables = workflowRunningParameterResolver.normalizeRuntimeVariables(workflow.getContent(), variables);
|
||||||
if (StpUtil.isLogin()) {
|
if (StpUtil.isLogin()) {
|
||||||
variables.put(Constants.LOGIN_USER_KEY, SaTokenUtil.getLoginAccount());
|
variables.put(Constants.LOGIN_USER_KEY, SaTokenUtil.getLoginAccount());
|
||||||
}
|
}
|
||||||
@@ -94,6 +123,13 @@ public class UcWorkflowController extends BaseCurdController<WorkflowService, Wo
|
|||||||
* 获取工作流运行状态 - v2
|
* 获取工作流运行状态 - v2
|
||||||
*/
|
*/
|
||||||
@PostMapping("/getChainStatus")
|
@PostMapping("/getChainStatus")
|
||||||
|
@RequireResourceAccess(
|
||||||
|
resource = CategoryResourceType.WORKFLOW,
|
||||||
|
action = ResourceAction.USE,
|
||||||
|
lookup = ResourceLookup.EXEC_KEY,
|
||||||
|
idExpr = "#executeId",
|
||||||
|
denyMessage = "无权限访问该执行记录"
|
||||||
|
)
|
||||||
public Result<ChainInfo> getChainStatus(@JsonBody(value = "executeId") String executeId,
|
public Result<ChainInfo> getChainStatus(@JsonBody(value = "executeId") String executeId,
|
||||||
@JsonBody("nodes") List<NodeInfo> nodes) {
|
@JsonBody("nodes") List<NodeInfo> nodes) {
|
||||||
ChainInfo res = tinyFlowService.getChainStatus(executeId, nodes);
|
ChainInfo res = tinyFlowService.getChainStatus(executeId, nodes);
|
||||||
@@ -105,6 +141,13 @@ public class UcWorkflowController extends BaseCurdController<WorkflowService, Wo
|
|||||||
*/
|
*/
|
||||||
@PostMapping("/resume")
|
@PostMapping("/resume")
|
||||||
@SaCheckPermission("/api/v1/workflow/save")
|
@SaCheckPermission("/api/v1/workflow/save")
|
||||||
|
@RequireResourceAccess(
|
||||||
|
resource = CategoryResourceType.WORKFLOW,
|
||||||
|
action = ResourceAction.USE,
|
||||||
|
lookup = ResourceLookup.EXEC_KEY,
|
||||||
|
idExpr = "#executeId",
|
||||||
|
denyMessage = "无权限恢复工作流执行"
|
||||||
|
)
|
||||||
public Result<Void> resume(@JsonBody(value = "executeId", required = true) String executeId,
|
public Result<Void> resume(@JsonBody(value = "executeId", required = true) String executeId,
|
||||||
@JsonBody("confirmParams") Map<String, Object> confirmParams) {
|
@JsonBody("confirmParams") Map<String, Object> confirmParams) {
|
||||||
chainExecutor.resumeAsync(executeId, confirmParams);
|
chainExecutor.resumeAsync(executeId, confirmParams);
|
||||||
@@ -116,6 +159,13 @@ public class UcWorkflowController extends BaseCurdController<WorkflowService, Wo
|
|||||||
*/
|
*/
|
||||||
@GetMapping("getRunningParameters")
|
@GetMapping("getRunningParameters")
|
||||||
@SaCheckPermission("/api/v1/workflow/query")
|
@SaCheckPermission("/api/v1/workflow/query")
|
||||||
|
@RequireResourceAccess(
|
||||||
|
resource = CategoryResourceType.WORKFLOW,
|
||||||
|
action = ResourceAction.READ,
|
||||||
|
lookup = ResourceLookup.WORKFLOW_ID,
|
||||||
|
idExpr = "#id",
|
||||||
|
denyMessage = "无权限访问工作流"
|
||||||
|
)
|
||||||
public Result<?> getRunningParameters(@RequestParam BigInteger id) {
|
public Result<?> getRunningParameters(@RequestParam BigInteger id) {
|
||||||
Workflow workflow = service.getById(id);
|
Workflow workflow = service.getById(id);
|
||||||
|
|
||||||
@@ -123,17 +173,10 @@ public class UcWorkflowController extends BaseCurdController<WorkflowService, Wo
|
|||||||
return Result.fail(1, "can not find the workflow by id: " + id);
|
return Result.fail(1, "can not find the workflow by id: " + id);
|
||||||
}
|
}
|
||||||
workflowCheckService.checkOrThrow(workflow.getContent(), WorkflowCheckStage.PRE_EXECUTE, workflow.getId());
|
workflowCheckService.checkOrThrow(workflow.getContent(), WorkflowCheckStage.PRE_EXECUTE, workflow.getId());
|
||||||
|
Map<String, Object> res = workflowRunningParameterResolver.buildRunningParametersView(workflow);
|
||||||
ChainDefinition definition = chainParser.parse(workflow.getContent());
|
if (res == null) {
|
||||||
if (definition == null) {
|
|
||||||
return Result.fail(2, "节点配置错误,请检查! ");
|
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);
|
return Result.ok(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,4 +189,32 @@ public class UcWorkflowController extends BaseCurdController<WorkflowService, Wo
|
|||||||
protected Result<?> onRemoveBefore(Collection<Serializable> ids) {
|
protected Result<?> onRemoveBefore(Collection<Serializable> ids) {
|
||||||
return Result.fail("-");
|
return Result.fail("-");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Result<List<Workflow>> list(Workflow entity, Boolean asTree, String sortKey, String sortType) {
|
||||||
|
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
|
||||||
|
workflowVisibilityQueryHelper.applyReadableAccess(queryWrapper);
|
||||||
|
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||||
|
return Result.ok(service.list(queryWrapper));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Page<Workflow> queryPage(Page<Workflow> page, QueryWrapper queryWrapper) {
|
||||||
|
workflowVisibilityQueryHelper.applyReadableAccess(queryWrapper);
|
||||||
|
return super.queryPage(page, queryWrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@GetMapping("detail")
|
||||||
|
@RequireResourceAccess(
|
||||||
|
resource = CategoryResourceType.WORKFLOW,
|
||||||
|
action = ResourceAction.READ,
|
||||||
|
lookup = ResourceLookup.WORKFLOW_ID,
|
||||||
|
idExpr = "#id",
|
||||||
|
denyMessage = "无权限访问工作流"
|
||||||
|
)
|
||||||
|
public Result<Workflow> detail(String id) {
|
||||||
|
Workflow workflow = service.getDetail(id);
|
||||||
|
return Result.ok(workflow);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import tech.easyflow.common.satoken.util.SaTokenUtil;
|
|||||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||||
import tech.easyflow.system.entity.SysAccount;
|
import tech.easyflow.system.entity.SysAccount;
|
||||||
import tech.easyflow.system.service.SysAccountService;
|
import tech.easyflow.system.service.SysAccountService;
|
||||||
|
import tech.easyflow.system.util.SysPasswordPolicy;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
@@ -80,12 +81,14 @@ public class UcSysAccountController {
|
|||||||
if (!newPassword.equals(confirmPassword)) {
|
if (!newPassword.equals(confirmPassword)) {
|
||||||
return Result.fail(2, "两次密码不一致");
|
return Result.fail(2, "两次密码不一致");
|
||||||
}
|
}
|
||||||
|
SysPasswordPolicy.validateStrongPassword(newPassword);
|
||||||
SysAccount update = new SysAccount();
|
SysAccount update = new SysAccount();
|
||||||
update.setId(loginAccountId);
|
update.setId(loginAccountId);
|
||||||
update.setPassword(BCrypt.hashpw(newPassword));
|
update.setPassword(BCrypt.hashpw(newPassword));
|
||||||
|
update.setPasswordResetRequired(false);
|
||||||
update.setModified(new Date());
|
update.setModified(new Date());
|
||||||
update.setModifiedBy(loginAccountId);
|
update.setModifiedBy(loginAccountId);
|
||||||
service.updateById(update);
|
service.updateById(update);
|
||||||
return Result.ok();
|
return Result.ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,418 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
# 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 判断业务,不依赖协议本身,支持其他协议的扩展
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -11,6 +11,10 @@
|
|||||||
<artifactId>easyflow-common-all</artifactId>
|
<artifactId>easyflow-common-all</artifactId>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>tech.easyflow</groupId>
|
||||||
|
<artifactId>easyflow-common-analytical-db</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>tech.easyflow</groupId>
|
<groupId>tech.easyflow</groupId>
|
||||||
<artifactId>easyflow-common-ai</artifactId>
|
<artifactId>easyflow-common-ai</artifactId>
|
||||||
@@ -23,6 +27,10 @@
|
|||||||
<groupId>tech.easyflow</groupId>
|
<groupId>tech.easyflow</groupId>
|
||||||
<artifactId>easyflow-common-cache</artifactId>
|
<artifactId>easyflow-common-cache</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>tech.easyflow</groupId>
|
||||||
|
<artifactId>easyflow-common-mq</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>tech.easyflow</groupId>
|
<groupId>tech.easyflow</groupId>
|
||||||
<artifactId>easyflow-common-file-storage</artifactId>
|
<artifactId>easyflow-common-file-storage</artifactId>
|
||||||
|
|||||||
53
easyflow-commons/easyflow-common-analytical-db/pom.xml
Normal file
53
easyflow-commons/easyflow-common-analytical-db/pom.xml
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<?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-analytical-db</name>
|
||||||
|
<artifactId>easyflow-common-analytical-db</artifactId>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>tech.easyflow</groupId>
|
||||||
|
<artifactId>easyflow-common-base</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-jdbc</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-autoconfigure</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-actuator</artifactId>
|
||||||
|
<version>${spring-boot.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.clickhouse</groupId>
|
||||||
|
<artifactId>clickhouse-jdbc</artifactId>
|
||||||
|
<classifier>all</classifier>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.flywaydb</groupId>
|
||||||
|
<artifactId>flyway-core</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.flywaydb</groupId>
|
||||||
|
<artifactId>flyway-database-clickhouse</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>junit</groupId>
|
||||||
|
<artifactId>junit</artifactId>
|
||||||
|
<version>${junit.version}</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package tech.easyflow.common.analyticaldb.config;
|
||||||
|
|
||||||
|
import com.zaxxer.hikari.HikariConfig;
|
||||||
|
import com.zaxxer.hikari.HikariDataSource;
|
||||||
|
import org.flywaydb.core.Flyway;
|
||||||
|
import org.springframework.beans.factory.InitializingBean;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
import tech.easyflow.common.analyticaldb.core.AnalyticalDBOperations;
|
||||||
|
import tech.easyflow.common.analyticaldb.core.DefaultAnalyticalDBOperations;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class AnalyticalDBConfiguration {
|
||||||
|
|
||||||
|
@Bean(destroyMethod = "close")
|
||||||
|
@ConditionalOnProperty(prefix = "easyflow.analytical-db", name = "enabled", havingValue = "true")
|
||||||
|
public AnalyticalDBResources analyticalDBResources(AnalyticalDBProperties properties) {
|
||||||
|
HikariConfig config = new HikariConfig();
|
||||||
|
config.setPoolName("easyflow-analytical-db");
|
||||||
|
config.setDriverClassName(properties.getDriverClassName());
|
||||||
|
config.setJdbcUrl(properties.getUrl());
|
||||||
|
config.setUsername(properties.getUsername());
|
||||||
|
config.setPassword(properties.getPassword());
|
||||||
|
config.setMaximumPoolSize(properties.getPool().getMaxPoolSize());
|
||||||
|
config.setMinimumIdle(properties.getPool().getMinIdle());
|
||||||
|
config.setConnectionTimeout(properties.getPool().getConnectionTimeout());
|
||||||
|
config.setValidationTimeout(properties.getPool().getValidationTimeout());
|
||||||
|
config.setIdleTimeout(properties.getPool().getIdleTimeout());
|
||||||
|
config.setMaxLifetime(properties.getPool().getMaxLifetime());
|
||||||
|
config.setInitializationFailTimeout(-1L);
|
||||||
|
HikariDataSource dataSource = new HikariDataSource(config);
|
||||||
|
return new AnalyticalDBResources(dataSource, new JdbcTemplate(dataSource));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnProperty(prefix = "easyflow.analytical-db", name = "enabled", havingValue = "true")
|
||||||
|
public AnalyticalDBOperations analyticalDBOperations(AnalyticalDBResources resources) {
|
||||||
|
return new DefaultAnalyticalDBOperations(resources.jdbcTemplate());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnProperty(prefix = "easyflow.analytical-db", name = "enabled", havingValue = "true")
|
||||||
|
public InitializingBean analyticalDBFlywayInitializer(AnalyticalDBResources resources,
|
||||||
|
AnalyticalDBFlywayProperties properties) {
|
||||||
|
return () -> Flyway.configure()
|
||||||
|
.dataSource(resources.dataSource())
|
||||||
|
.locations(splitLocations(properties.getLocations()))
|
||||||
|
.table(properties.getTable())
|
||||||
|
.baselineOnMigrate(properties.isBaselineOnMigrate())
|
||||||
|
.validateOnMigrate(properties.isValidateOnMigrate())
|
||||||
|
.load()
|
||||||
|
.migrate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String[] splitLocations(String locations) {
|
||||||
|
if (!StringUtils.hasText(locations)) {
|
||||||
|
return new String[]{"classpath:db/migration/analyticaldb"};
|
||||||
|
}
|
||||||
|
return StringUtils.commaDelimitedListToStringArray(locations);
|
||||||
|
}
|
||||||
|
|
||||||
|
public record AnalyticalDBResources(HikariDataSource dataSource, JdbcTemplate jdbcTemplate) implements AutoCloseable {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
dataSource.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user