Compare commits
235 Commits
81125ce55c
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
| e938ecda80 | |||
| 431fe8e707 | |||
| 0968e3bfa5 | |||
| 65c85180c2 | |||
| 0f126ad489 | |||
| 1e59063c37 | |||
| 788c8e5459 | |||
| c3673ece46 | |||
| 36acf37976 | |||
| 1dd0ac167c | |||
| 6498f1049a | |||
| 1f37b0a8ae | |||
| c00369f6b9 | |||
| 3e34b65bcb | |||
| 263f5f4b8b | |||
| 6daf805cd0 | |||
| 4386b2a1a6 | |||
| f28e3919ac | |||
| 0cedf85729 | |||
| 155af9989c | |||
| 8c174e5c02 | |||
| 17ef189862 | |||
| e6b2e2798f | |||
| 4823b0741f | |||
| 1c68e3582c | |||
| 619b60600d | |||
| 240b84063a | |||
| ec6e03587a | |||
| 1ccdafdb47 | |||
| e38821e48a | |||
| 3e79e99925 | |||
| 98b34bd4bb | |||
| c27e97bcc2 | |||
| 9068d42f4d | |||
| fd64073148 | |||
| f63cd9be4d | |||
| 407b85c8a9 | |||
| 9078ca163e | |||
| 7f083a9433 | |||
| 7d654c3302 | |||
| fa07134cf8 | |||
| 310fc1fb58 | |||
| c96f772f01 | |||
| 30b2cc36fd | |||
| 7a5298c3fc | |||
| 9579619384 | |||
| 4e8640dcaf | |||
| 91d66e636d | |||
| 5de3b209c1 | |||
| 77d66e1b42 | |||
| 9be9bd7665 | |||
| 64a85c6a5b | |||
| 765006747a | |||
| fe41d62b8a | |||
| ac9e200a15 | |||
| 6c491bd893 | |||
| 742a4b1647 | |||
| 402c0f16b8 | |||
| 1c0fbfa5ff | |||
| 6bfd440214 | |||
| 9d2fa39a2d | |||
| bae7b18977 | |||
| 54d85ae460 | |||
| 0d14f1c165 | |||
| 13dec6c216 | |||
| d244a0404d | |||
| 6ad004da9b | |||
| 4b52d85512 | |||
| 7a64cfcaa4 | |||
| 7082041e6e | |||
| 170b315459 | |||
| 6544cdcff0 | |||
| 7bcaf69fc0 | |||
| a75ae9a843 | |||
| 46398d1365 | |||
| 08fe3ba1f1 | |||
| 3a77149087 | |||
| 2a930a1c97 | |||
| c757eeed50 | |||
| 2eaf2eace7 | |||
| 0860492446 | |||
| de0ccf03aa | |||
| 89824b5b9b | |||
| 8c334be65d | |||
| 93db17b384 | |||
| 866688b92f | |||
| 219e4f7eff | |||
| 19dac5146c | |||
| e3228837f4 | |||
| dc99d1aa52 | |||
| 1bf755f6c4 | |||
| 51dbfd41b6 | |||
| 6df3dd9981 | |||
| 527336bfc9 | |||
| 12080ceedb | |||
| 5c29ca9407 | |||
| 4a0efe8879 | |||
| fb08424cef | |||
| 0754ad0792 | |||
| f92707dac8 | |||
| a051dbacba | |||
| f872eac1f9 | |||
| f0aba1eddd | |||
| 41b056b7e3 | |||
| 1cbee6b018 | |||
| c2ed5a24a3 | |||
| 615092f4f7 | |||
| 048aa9bc1e | |||
| 0b764b79de | |||
| 2267517fd8 | |||
| 100d744c25 | |||
| 2abff304ed | |||
| 9959f95b94 | |||
| 5dbb46ef8f | |||
| 57bd7b5d06 | |||
| 864cea6135 | |||
| ff5f90121b | |||
| 1b40829135 | |||
| 03f45212ef | |||
| c78074a969 | |||
| 5f11219226 | |||
| 766554bf63 | |||
| 19c7b60a65 | |||
| 9c3badc55c | |||
| d45c67a317 | |||
| fceedd02cd | |||
| 1ae8a22afe | |||
| 5ee6065017 | |||
| f392c896f9 | |||
| dc35ddc3e4 | |||
| 1404c2ddc5 | |||
| 1630d6194a | |||
| a22ca24906 | |||
| 24b1142738 | |||
| 908eb5583d | |||
| a904f63ec8 | |||
| b0ed512e97 | |||
| b08eb009bb | |||
| 5497931abd | |||
| 2892a7eddc | |||
| 567fd12706 | |||
| 0dc5c3ca55 | |||
| 2a9e882ac6 | |||
| aedefe6b5e | |||
| dc7e46260b | |||
| ba4253e13e | |||
| ac07899dc4 | |||
| cb06599dca | |||
| da59c713f9 | |||
| a75f7baf1b | |||
| 63eb55e24c | |||
| 526e16163b | |||
| 41545fcef0 | |||
| 417e846cbd | |||
| 8b4ba65e1c | |||
| 4a8e633083 | |||
| df9fe4c2fe | |||
| 3e8ee66ab6 | |||
| ebfd91eeab | |||
| 5a42826d44 | |||
| caa1f07b66 | |||
| 19059fde96 | |||
| 9f06c238d3 | |||
| 5a3d4788da | |||
| 53fb63802b | |||
| 9436cc5397 | |||
| f658f120e6 | |||
| 4c28eaf393 | |||
| 791649c7d5 | |||
| ba21f861f4 | |||
| 1e6158be77 | |||
| 62d763199f | |||
| 5a91323a2e | |||
| 27e50a7624 | |||
| 705e0faab6 | |||
| ce8b4fb420 | |||
| 1a91e16ac9 | |||
| b1706e9589 | |||
| 7cf6c31da3 | |||
| e56f043483 | |||
| 03ad011f64 | |||
| 9c1de1657d | |||
| 999a21e2d9 | |||
| 28797a83cc | |||
| 72c0115b19 | |||
| 7591eb8cda | |||
| ef4528a441 | |||
| e9ddeae33f | |||
| 950148b3f7 | |||
| 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 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -35,6 +35,8 @@ build/
|
||||
### Mac OS ###
|
||||
.DS_Store
|
||||
/.logs/
|
||||
/logs/
|
||||
/agent-workspaces/
|
||||
/.idea/
|
||||
.logs
|
||||
.idea
|
||||
@@ -42,6 +44,7 @@ build/
|
||||
.jlsp/
|
||||
.arts/
|
||||
luceneKnowledge
|
||||
**/*.lic
|
||||
|
||||
# v1
|
||||
/easyflow-ui-react
|
||||
120
Dockerfile
120
Dockerfile
@@ -1,4 +1,5 @@
|
||||
FROM --platform=linux/amd64 swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/eclipse-temurin:17-jre
|
||||
# 后端构建脚本
|
||||
FROM swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/eclipse-temurin:17-jre
|
||||
|
||||
ENV LANG=C.UTF-8
|
||||
ENV LC_ALL=C.UTF-8
|
||||
@@ -8,18 +9,129 @@ ENV EASYFLOW_JAR_PATH=/app/artifacts/easyflow.jar
|
||||
ENV EASYFLOW_CONFIG_PATH=file:/app/application.yml
|
||||
ENV EASYFLOW_LOG_FILE=/app/logs/app.log
|
||||
ENV EASYFLOW_JAR_RESTART_GRACE_SECONDS=30
|
||||
ENV NPM_CONFIG_REGISTRY=https://registry.npmjs.org
|
||||
ENV PIP_INDEX_URL=https://pypi.org/simple
|
||||
ENV PYTHONPATH=/opt/easyflow/python-packages
|
||||
ENV NODE_PATH=/app/node_modules
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN useradd --system --create-home easyflow && \
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends python3 inotify-tools tini && \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
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 \
|
||||
coreutils \
|
||||
diffutils \
|
||||
file \
|
||||
findutils \
|
||||
fontconfig \
|
||||
fonts-liberation2 \
|
||||
fonts-noto-cjk \
|
||||
gawk \
|
||||
grep \
|
||||
gzip \
|
||||
inotify-tools \
|
||||
jq \
|
||||
libdigest-sha-perl \
|
||||
libreoffice-calc \
|
||||
libreoffice-impress \
|
||||
libreoffice-writer \
|
||||
nodejs \
|
||||
pandoc \
|
||||
poppler-utils \
|
||||
procps \
|
||||
python3 \
|
||||
python3-pip \
|
||||
python3-venv \
|
||||
qpdf \
|
||||
ripgrep \
|
||||
sed \
|
||||
tar \
|
||||
tree \
|
||||
unzip \
|
||||
util-linux \
|
||||
zip \
|
||||
tini && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN mkdir -p /etc/pip "${PYTHONPATH}" /opt/easyflow/node-runtime && \
|
||||
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}" && \
|
||||
printf "[global]\nindex-url = %s\n" "${PIP_INDEX_URL}" > /etc/pip.conf
|
||||
|
||||
RUN python3 -m pip install --no-cache-dir --target "${PYTHONPATH}" \
|
||||
python-docx==1.2.0 \
|
||||
python-pptx==1.0.2 \
|
||||
openpyxl==3.1.5 \
|
||||
xlsxwriter==3.2.9 \
|
||||
lxml==6.1.1 \
|
||||
defusedxml==0.7.1 \
|
||||
pillow==12.3.0 \
|
||||
pypdf==6.16.1 \
|
||||
pdfplumber==0.11.10 \
|
||||
pdf2image==1.17.0 \
|
||||
reportlab==5.0.0 \
|
||||
numpy==2.5.2 \
|
||||
pandas==3.0.5 \
|
||||
matplotlib==3.11.1 \
|
||||
seaborn==0.13.2 \
|
||||
pyyaml==6.0.3 \
|
||||
jsonschema==4.26.0 \
|
||||
jinja2==3.1.6 \
|
||||
beautifulsoup4==4.15.0 \
|
||||
pydantic==2.13.4 \
|
||||
python-dateutil==2.9.0.post0 \
|
||||
tabulate==0.10.0 \
|
||||
markdown==3.10.3 \
|
||||
charset-normalizer==3.5.1 \
|
||||
tenacity==9.1.4 && \
|
||||
PYTHONPATH="${PYTHONPATH}" python3 -c "import bs4, defusedxml, docx, jsonschema, lxml, matplotlib, numpy, openpyxl, pandas, pdfplumber, PIL, pptx, pydantic, pypdf, reportlab, seaborn, yaml"
|
||||
|
||||
RUN npm install --prefix /opt/easyflow/node-runtime --omit=dev --no-audit --no-fund --save-exact \
|
||||
docx@9.7.1 \
|
||||
pptxgenjs@4.0.1 \
|
||||
sharp@0.35.3 \
|
||||
pdf-lib@1.17.1 \
|
||||
pdfjs-dist@6.2.108 \
|
||||
zod@4.4.3 \
|
||||
ajv@8.20.0 \
|
||||
yaml@2.9.0 \
|
||||
csv-parse@7.0.2 \
|
||||
csv-stringify@6.8.3 \
|
||||
fast-xml-parser@5.10.1 \
|
||||
marked@18.0.9 \
|
||||
sanitize-html@2.17.7 \
|
||||
cheerio@1.2.0 \
|
||||
dayjs@1.11.21 \
|
||||
handlebars@4.7.9 \
|
||||
jszip@3.10.1 && \
|
||||
ln -s /opt/easyflow/node-runtime/node_modules /app/node_modules && \
|
||||
node -e "for (const name of ['docx','pptxgenjs','sharp','pdf-lib','pdfjs-dist/package.json','zod','ajv','yaml','csv-parse','csv-stringify','fast-xml-parser','marked','sanitize-html','cheerio','dayjs','handlebars','jszip']) require.resolve(name)" && \
|
||||
npm cache clean --force
|
||||
|
||||
RUN fc-cache -f && \
|
||||
mkdir -p /app/logs /app/artifacts /app/data && \
|
||||
chown -R easyflow:easyflow /app
|
||||
|
||||
COPY docker-soffice-wrapper.sh /usr/local/bin/soffice
|
||||
COPY docker-entrypoint.sh /usr/local/bin/easyflow-entrypoint.sh
|
||||
RUN chmod 755 /usr/local/bin/easyflow-entrypoint.sh
|
||||
RUN chmod 755 /usr/local/bin/soffice /usr/local/bin/easyflow-entrypoint.sh
|
||||
|
||||
VOLUME ["/app/logs", "/app/data"]
|
||||
EXPOSE 8111
|
||||
|
||||
15
README.md
15
README.md
@@ -58,6 +58,14 @@ pnpm install
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
管理端开发环境与生产环境统一使用 `/flow/` 基路径和 Hash 路由,默认访问地址:
|
||||
|
||||
```text
|
||||
http://127.0.0.1:5090/flow/#/
|
||||
```
|
||||
|
||||
直接访问 `http://127.0.0.1:5090` 或 `/flow` 时,开发服务器会自动补齐 `/flow/`。
|
||||
|
||||
用户中心:
|
||||
|
||||
```bash
|
||||
@@ -68,6 +76,13 @@ pnpm dev
|
||||
|
||||
默认测试账号:`admin / Easy@2026`
|
||||
|
||||
管理端发布前可执行以下命令,验证环境契约、生产镜像和 Nginx 路由:
|
||||
|
||||
```bash
|
||||
cd easyflow-ui-admin
|
||||
pnpm verify:deployment
|
||||
```
|
||||
|
||||
## 后端 Jar 包构建与部署
|
||||
|
||||
### 构建 Jar
|
||||
|
||||
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.** { *; }
|
||||
@@ -72,6 +72,8 @@ services:
|
||||
TZ: Asia/Shanghai
|
||||
MINIO_ROOT_USER: easyflowadmin
|
||||
MINIO_ROOT_PASSWORD: easyflowadmin123
|
||||
MINIO_API_STALE_UPLOADS_EXPIRY: 24h
|
||||
MINIO_API_STALE_UPLOADS_CLEANUP_INTERVAL: 6h
|
||||
ports:
|
||||
- "9000:9000"
|
||||
- "9001:9001"
|
||||
@@ -88,7 +90,7 @@ services:
|
||||
MINIO_ROOT_USER: easyflowadmin
|
||||
MINIO_ROOT_PASSWORD: easyflowadmin123
|
||||
MINIO_ENDPOINT: http://minio:9000
|
||||
MINIO_BUCKETS: easyflow,milvus
|
||||
MINIO_BUCKETS: easyflow,milvus,easyflow-agent-media
|
||||
MINIO_PUBLIC_BUCKETS: easyflow
|
||||
MINIO_ALIAS: local
|
||||
volumes:
|
||||
|
||||
@@ -22,6 +22,7 @@ services:
|
||||
- easyflow-net
|
||||
volumes:
|
||||
- ./attachment:/www/easyflow/attachment
|
||||
- ./logs:/app/logs
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
|
||||
21
docker-soffice-wrapper.sh
Normal file
21
docker-soffice-wrapper.sh
Normal file
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
profile_parent="${TMPDIR:-/tmp}"
|
||||
profile_dir="$(mktemp -d "${profile_parent%/}/easyflow-soffice-XXXXXX")"
|
||||
|
||||
cleanup() {
|
||||
rm -rf -- "$profile_dir"
|
||||
}
|
||||
|
||||
trap cleanup EXIT HUP INT TERM
|
||||
|
||||
/usr/bin/soffice \
|
||||
-env:UserInstallation="file://${profile_dir}" \
|
||||
--headless \
|
||||
--safe-mode \
|
||||
--nologo \
|
||||
--nodefault \
|
||||
--nolockcheck \
|
||||
--norestore \
|
||||
"$@"
|
||||
@@ -20,6 +20,14 @@
|
||||
<groupId>tech.easyflow</groupId>
|
||||
<artifactId>easyflow-module-ai</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>tech.easyflow</groupId>
|
||||
<artifactId>easyflow-module-agent</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>tech.easyflow</groupId>
|
||||
<artifactId>easyflow-module-skill</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>tech.easyflow</groupId>
|
||||
<artifactId>easyflow-module-chatlog</artifactId>
|
||||
@@ -32,9 +40,25 @@
|
||||
<groupId>tech.easyflow</groupId>
|
||||
<artifactId>easyflow-module-job</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>tech.easyflow</groupId>
|
||||
<artifactId>easyflow-module-dataspace</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>tech.easyflow</groupId>
|
||||
<artifactId>easyflow-common-captcha</artifactId>
|
||||
</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>
|
||||
</project>
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
package tech.easyflow.admin.controller.agent;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import cn.dev33.satoken.annotation.SaMode;
|
||||
import org.springframework.http.CacheControl;
|
||||
import org.springframework.http.ContentDisposition;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
|
||||
import tech.easyflow.agent.entity.AgentArtifact;
|
||||
import tech.easyflow.agent.runtime.artifact.AgentArtifactService;
|
||||
import tech.easyflow.agent.runtime.artifact.AgentArtifactView;
|
||||
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 java.io.InputStream;
|
||||
import java.math.BigInteger;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* Agent Artifact 安全元数据与鉴权下载控制器。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/agent/artifacts")
|
||||
public class AgentArtifactController {
|
||||
|
||||
private final AgentArtifactService artifactService;
|
||||
|
||||
/**
|
||||
* 创建 Artifact 控制器。
|
||||
*
|
||||
* @param artifactService Artifact 服务
|
||||
*/
|
||||
public AgentArtifactController(AgentArtifactService artifactService) {
|
||||
this.artifactService = artifactService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询一个已鉴权 Artifact 的安全元数据。
|
||||
*
|
||||
* @param artifactId 稳定 Artifact ID
|
||||
* @return 安全元数据
|
||||
*/
|
||||
@GetMapping("/{artifactId}")
|
||||
@SaCheckPermission(value = {"/api/v1/agent/session/query", "/api/v1/agent/save"}, mode = SaMode.OR)
|
||||
public Result<AgentArtifactView> metadata(@PathVariable String artifactId,
|
||||
@RequestParam BigInteger agentId,
|
||||
@RequestParam String mode,
|
||||
@RequestParam(required = false) BigInteger sessionId,
|
||||
@RequestParam(required = false) String runtimeSessionId) {
|
||||
AgentArtifact artifact = artifactService.requireDownload(
|
||||
artifactId, requireAccount(), agentId, mode, sessionId, runtimeSessionId);
|
||||
return Result.ok(artifactService.toView(artifact));
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过后端鉴权代理流式下载私有 Artifact。
|
||||
*
|
||||
* @param artifactId 稳定 Artifact ID
|
||||
* @return 私有流式响应
|
||||
*/
|
||||
@GetMapping("/{artifactId}/content")
|
||||
@SaCheckPermission(value = {"/api/v1/agent/session/query", "/api/v1/agent/save"}, mode = SaMode.OR)
|
||||
public ResponseEntity<StreamingResponseBody> content(@PathVariable String artifactId,
|
||||
@RequestParam BigInteger agentId,
|
||||
@RequestParam String mode,
|
||||
@RequestParam(required = false) BigInteger sessionId,
|
||||
@RequestParam(required = false) String runtimeSessionId) {
|
||||
AgentArtifact artifact = artifactService.requireDownload(
|
||||
artifactId, requireAccount(), agentId, mode, sessionId, runtimeSessionId);
|
||||
StreamingResponseBody body = output -> {
|
||||
try (InputStream input = artifactService.openDownload(artifact)) {
|
||||
input.transferTo(output);
|
||||
}
|
||||
};
|
||||
String mimeType = artifact.getMimeType() == null
|
||||
? MediaType.APPLICATION_OCTET_STREAM_VALUE : artifact.getMimeType();
|
||||
return ResponseEntity.ok()
|
||||
.cacheControl(CacheControl.noStore())
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, ContentDisposition.attachment()
|
||||
.filename(artifact.getFileName(), StandardCharsets.UTF_8).build().toString())
|
||||
.header("X-Content-Type-Options", "nosniff")
|
||||
.contentType(MediaType.parseMediaType(mimeType))
|
||||
.contentLength(artifact.getSizeBytes() == null ? 0L : artifact.getSizeBytes())
|
||||
.body(body);
|
||||
}
|
||||
|
||||
private LoginAccount requireAccount() {
|
||||
try {
|
||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||
if (account == null || account.getId() == null || account.getTenantId() == null) {
|
||||
throw new BusinessException("当前登录状态失效,请重新登录后再试");
|
||||
}
|
||||
return account;
|
||||
} catch (BusinessException error) {
|
||||
throw error;
|
||||
} catch (Exception error) {
|
||||
throw new BusinessException("当前登录状态失效,请重新登录后再试");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,848 @@
|
||||
package tech.easyflow.admin.controller.agent;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import cn.dev33.satoken.annotation.SaMode;
|
||||
import com.mybatisflex.core.paginate.Page;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import io.agentscope.core.agui.model.RunAgentInput;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.http.ContentDisposition;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
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.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
|
||||
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.runtime.agui.AgentAguiHitlResolveRequest;
|
||||
import tech.easyflow.agent.runtime.agui.AgentAguiRunStatusView;
|
||||
import tech.easyflow.agent.runtime.composer.AgentComposerDraft;
|
||||
import tech.easyflow.agent.runtime.composer.AgentComposerDraftService;
|
||||
import tech.easyflow.agent.runtime.composer.AgentComposerSession;
|
||||
import tech.easyflow.agent.runtime.document.AgentDocumentResource;
|
||||
import tech.easyflow.agent.runtime.document.AgentDocumentService;
|
||||
import tech.easyflow.agent.runtime.document.AgentDocumentUploadView;
|
||||
import tech.easyflow.agent.runtime.media.AgentMediaService;
|
||||
import tech.easyflow.agent.runtime.media.AgentMediaUploadView;
|
||||
import com.easyagents.agent.runtime.media.AgentMediaResource;
|
||||
import tech.easyflow.agent.security.AgentVisibilityQueryHelper;
|
||||
import tech.easyflow.agent.service.AgentApprovalStateService;
|
||||
import tech.easyflow.agent.service.AgentKnowledgeBindingService;
|
||||
import tech.easyflow.agent.service.AgentOptionQueryService;
|
||||
import tech.easyflow.agent.service.AgentService;
|
||||
import tech.easyflow.agent.service.AgentSkillBindingService;
|
||||
import tech.easyflow.agent.service.AgentToolBindingService;
|
||||
import tech.easyflow.agent.vo.AgentOptionView;
|
||||
import tech.easyflow.agent.vo.AgentResourceOptionsView;
|
||||
import tech.easyflow.ai.enums.PublishStatus;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalActionResult;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
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.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.log.annotation.LogReporterDisabled;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigInteger;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Agent 管理端控制器。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/agent")
|
||||
public class AgentController extends BaseCurdController<AgentService, Agent> {
|
||||
|
||||
@Resource
|
||||
private AgentToolBindingService agentToolBindingService;
|
||||
@Resource
|
||||
private AgentKnowledgeBindingService agentKnowledgeBindingService;
|
||||
@Resource
|
||||
private AgentSkillBindingService agentSkillBindingService;
|
||||
@Resource
|
||||
private AgentRunService agentRunService;
|
||||
@Resource
|
||||
private AgentPublishAppService agentPublishAppService;
|
||||
@Resource
|
||||
private AgentApprovalStateService agentApprovalStateService;
|
||||
@Resource
|
||||
private AiResourceCreatorNameSupport aiResourceCreatorNameSupport;
|
||||
@Resource
|
||||
private AgentMediaService agentMediaService;
|
||||
@Resource
|
||||
private AgentDocumentService agentDocumentService;
|
||||
@Resource
|
||||
private AgentComposerDraftService agentComposerDraftService;
|
||||
@Resource
|
||||
private AgentOptionQueryService agentOptionQueryService;
|
||||
@Resource
|
||||
private AgentVisibilityQueryHelper agentVisibilityQueryHelper;
|
||||
|
||||
/**
|
||||
* 创建 Agent 控制器。
|
||||
*
|
||||
* @param service Agent 服务
|
||||
*/
|
||||
public AgentController(AgentService service) {
|
||||
super(service);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Agent 列表关键字搜索字段。
|
||||
*
|
||||
* @return Agent 名称和描述属性
|
||||
*/
|
||||
@Override
|
||||
protected String[] getKeywordSearchProperties() {
|
||||
return new String[]{"name", "description"};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Agent 详情。
|
||||
*
|
||||
* @param id Agent ID
|
||||
* @return Agent 详情
|
||||
*/
|
||||
@GetMapping("/getDetail")
|
||||
public Result<AgentDetailView> getDetail(BigInteger id) {
|
||||
Agent agent = service.getDetail(id);
|
||||
agentApprovalStateService.fillAgentApprovalState(agent);
|
||||
aiResourceCreatorNameSupport.fillAgentCreatorNames(List.of(agent));
|
||||
return Result.ok(AgentDetailView.from(agent));
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存 Agent 草稿。
|
||||
*
|
||||
* @param agent Agent 草稿
|
||||
* @return Agent 详情
|
||||
*/
|
||||
@Override
|
||||
@PostMapping("save")
|
||||
public Result<?> save(@JsonBody Agent agent) {
|
||||
Agent saved = service.saveDraft(agent);
|
||||
return Result.ok(AgentDetailView.from(service.getDetail(saved.getId())));
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 Agent 草稿。
|
||||
*
|
||||
* @param agent Agent 草稿
|
||||
* @return Agent 详情
|
||||
*/
|
||||
@Override
|
||||
@PostMapping("update")
|
||||
public Result<?> update(@JsonBody Agent agent) {
|
||||
Agent saved = service.updateDraft(agent);
|
||||
return Result.ok(AgentDetailView.from(service.getDetail(saved.getId())));
|
||||
}
|
||||
|
||||
/**
|
||||
* 原子保存 Agent 草稿及本次发生变化的绑定组。
|
||||
*
|
||||
* @param request 设计器保存请求
|
||||
* @return 保存后的 Agent 与本次替换的绑定
|
||||
*/
|
||||
@PostMapping("/draft/save")
|
||||
@SaCheckPermission("/api/v1/agent/save")
|
||||
public Result<AgentDetailView> saveDraft(@JsonBody(required = true, skipConvertError = false)
|
||||
AgentDraftSaveRequest request) {
|
||||
if (request == null || request.getAgent() == null) {
|
||||
throw new BusinessException("Agent 草稿不能为空");
|
||||
}
|
||||
Agent saved = service.saveDraftGraph(
|
||||
request.getAgent(),
|
||||
request.getToolBindings(),
|
||||
request.isReplaceToolBindings(),
|
||||
request.getKnowledgeBindings(),
|
||||
request.isReplaceKnowledgeBindings(),
|
||||
request.toSkillBindings(),
|
||||
request.isReplaceSkillBindings());
|
||||
return Result.ok(AgentDetailView.from(saved));
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 Agent 可见范围。
|
||||
*
|
||||
* @param agent 包含 Agent ID 和可见范围的请求数据
|
||||
* @return 更新后的 Agent
|
||||
*/
|
||||
@PostMapping("visibilityScope/update")
|
||||
@SaCheckPermission("/api/v1/agent/save")
|
||||
public Result<AgentDetailView> updateVisibilityScope(@JsonBody Agent agent) {
|
||||
return Result.ok(AgentDetailView.from(
|
||||
service.updateVisibilityScope(agent.getId(), agent.getVisibilityScope())));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询 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);
|
||||
agentVisibilityQueryHelper.applyReadableAccess(queryWrapper);
|
||||
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();
|
||||
}
|
||||
agents.forEach(this::sanitizeListItem);
|
||||
agentApprovalStateService.fillAgentApprovalState(agents);
|
||||
aiResourceCreatorNameSupport.fillAgentCreatorNames(agents);
|
||||
return Result.ok(agents);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询 Agent 安全选择项。
|
||||
*
|
||||
* @param publishedOnly 是否仅返回已发布 Agent
|
||||
* @return Agent 安全选择项
|
||||
*/
|
||||
@GetMapping("/options")
|
||||
@SaCheckPermission("/api/v1/agent/query")
|
||||
public Result<List<AgentOptionView>> options(
|
||||
@RequestParam(value = "publishedOnly", defaultValue = "false") boolean publishedOnly) {
|
||||
return Result.ok(agentOptionQueryService.listAgentOptions(publishedOnly));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询 Agent 设计器的安全资源选项。
|
||||
*
|
||||
* @return 设计器资源选项
|
||||
*/
|
||||
@GetMapping("/resourceOptions")
|
||||
@SaCheckPermission("/api/v1/agent/save")
|
||||
public Result<AgentResourceOptionsView> resourceOptions() {
|
||||
return Result.ok(agentOptionQueryService.listDesignerResourceOptions());
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询 Agent 会话可使用的知识库安全选项。
|
||||
*
|
||||
* @return 知识库选项
|
||||
*/
|
||||
@GetMapping("/knowledgeOptions")
|
||||
@SaCheckPermission("/api/v1/agent/query")
|
||||
public Result<List<AgentResourceOptionsView.ResourceOption>> knowledgeOptions() {
|
||||
return Result.ok(agentOptionQueryService.listKnowledgeOptions());
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询指定 MCP 的安全工具列表。
|
||||
*
|
||||
* @param id MCP ID
|
||||
* @return MCP 工具列表
|
||||
*/
|
||||
@GetMapping("/mcpToolOptions")
|
||||
@SaCheckPermission("/api/v1/agent/save")
|
||||
public Result<List<AgentResourceOptionsView.McpToolOption>> mcpToolOptions(
|
||||
@RequestParam BigInteger id) {
|
||||
return Result.ok(agentOptionQueryService.listMcpTools(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行 Agent 纯文本聊天。
|
||||
*
|
||||
* @param request 聊天请求
|
||||
* @return SSE Emitter
|
||||
*/
|
||||
@PostMapping("chat")
|
||||
@SaCheckPermission("/api/v1/agent/session/query")
|
||||
public SseEmitter chat(@JsonBody AgentChatRequest request) {
|
||||
return agentRunService.chat(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行 Agent 草稿态纯文本试用。
|
||||
*
|
||||
* @param request 草稿试用请求
|
||||
* @return SSE Emitter
|
||||
*/
|
||||
@PostMapping("/chat/draft")
|
||||
@SaCheckPermission("/api/v1/agent/save")
|
||||
public SseEmitter chatDraft(@JsonBody AgentDraftChatRequest request) {
|
||||
return agentRunService.chatDraft(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 AG-UI 协议运行正式 Agent 聊天。
|
||||
*
|
||||
* @param agentId URL 中的 Agent ID
|
||||
* @param input AG-UI 运行输入
|
||||
* @return 原生 AG-UI SSE
|
||||
*/
|
||||
@PostMapping("/{agentId}/agui/run")
|
||||
@SaCheckPermission("/api/v1/agent/session/query")
|
||||
public SseEmitter chatAgui(@PathVariable BigInteger agentId,
|
||||
@RequestBody RunAgentInput input) {
|
||||
return agentRunService.chatAgui(agentId, input);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 AG-UI 协议运行草稿 Agent 试用。
|
||||
*
|
||||
* @param input AG-UI 运行输入
|
||||
* @return 原生 AG-UI SSE
|
||||
*/
|
||||
@PostMapping("/agui/run/draft")
|
||||
@SaCheckPermission("/api/v1/agent/save")
|
||||
public SseEmitter chatDraftAgui(@RequestBody RunAgentInput input) {
|
||||
return agentRunService.chatDraftAgui(input);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询可重连 AG-UI 运行状态。
|
||||
*
|
||||
* @param runId 客户端运行 ID
|
||||
* @return 运行状态
|
||||
*/
|
||||
@GetMapping("/agui/run/{runId}/status")
|
||||
@SaCheckPermission(value = {
|
||||
"/api/v1/agent/session/query", "/api/v1/agent/save"
|
||||
}, mode = SaMode.OR)
|
||||
public Result<AgentAguiRunStatusView> getAguiRunStatus(@PathVariable String runId) {
|
||||
return Result.ok(agentRunService.getAguiRunStatus(runId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 从指定游标继续订阅 AG-UI 运行事件。
|
||||
*
|
||||
* @param runId 客户端运行 ID
|
||||
* @param after 已消费的最后事件游标
|
||||
* @return 增量重放 SSE
|
||||
*/
|
||||
@GetMapping("/agui/run/{runId}/events")
|
||||
@SaCheckPermission(value = {
|
||||
"/api/v1/agent/session/query", "/api/v1/agent/save"
|
||||
}, mode = SaMode.OR)
|
||||
public SseEmitter subscribeAguiRun(@PathVariable String runId,
|
||||
@RequestParam(defaultValue = "0") long after) {
|
||||
return agentRunService.subscribeAguiRun(runId, after);
|
||||
}
|
||||
|
||||
/**
|
||||
* 显式取消单次 AG-UI 运行。
|
||||
*
|
||||
* @param runId 客户端运行 ID
|
||||
* @return 操作结果
|
||||
*/
|
||||
@PostMapping("/agui/run/{runId}/cancel")
|
||||
@SaCheckPermission(value = {
|
||||
"/api/v1/agent/session/query", "/api/v1/agent/save"
|
||||
}, mode = SaMode.OR)
|
||||
public Result<Void> cancelAguiRun(@PathVariable String runId) {
|
||||
agentRunService.cancelAguiRun(runId);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 AG-UI 自定义 HITL 兼容桥审批。
|
||||
*
|
||||
* @param request 审批请求
|
||||
* @return 操作结果
|
||||
*/
|
||||
@PostMapping("/agui/hitl/resolve")
|
||||
@SaCheckPermission(value = {
|
||||
"/api/v1/agent/session/query", "/api/v1/agent/save"
|
||||
}, mode = SaMode.OR)
|
||||
public Result<Void> resolveAguiApproval(@RequestBody AgentAguiHitlResolveRequest request) {
|
||||
agentRunService.resolveAguiApproval(request);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传一张 Agent 聊天临时图片。
|
||||
*
|
||||
* @param file 图片文件
|
||||
* @param mode 聊天模式
|
||||
* @param agentId Agent ID
|
||||
* @param sessionId 会话 ID
|
||||
* @return 上传结果
|
||||
*/
|
||||
@PostMapping(value = "/media/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@SaCheckPermission(value = {
|
||||
"/api/v1/agent/session/query", "/api/v1/agent/save"
|
||||
}, mode = SaMode.OR)
|
||||
public Result<AgentMediaUploadView> uploadMedia(@RequestParam("file") MultipartFile file,
|
||||
@RequestParam("mode") String mode,
|
||||
@RequestParam("agentId") String agentId,
|
||||
@RequestParam("sessionId") String sessionId) {
|
||||
return Result.ok(agentMediaService.upload(file, mode, agentId, sessionId, SaTokenUtil.getLoginAccount()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除当前账号尚未发送的临时图片。
|
||||
*
|
||||
* @param uploadId 上传 ID
|
||||
* @return 操作结果
|
||||
*/
|
||||
@PostMapping("/media/delete")
|
||||
@SaCheckPermission(value = {
|
||||
"/api/v1/agent/session/query", "/api/v1/agent/save"
|
||||
}, mode = SaMode.OR)
|
||||
public Result<Void> deleteMedia(@JsonBody(value = "uploadId", required = true) String uploadId) {
|
||||
agentMediaService.deleteUpload(uploadId, SaTokenUtil.getLoginAccount());
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过鉴权代理读取 Agent 私有聊天图片。
|
||||
*
|
||||
* @param reference 稳定图片引用
|
||||
* @return 图片响应
|
||||
*/
|
||||
@GetMapping("/media/content")
|
||||
@SaCheckPermission(value = {
|
||||
"/api/v1/agent/session/query", "/api/v1/agent/save"
|
||||
}, mode = SaMode.OR)
|
||||
@LogReporterDisabled
|
||||
public ResponseEntity<byte[]> mediaContent(@RequestParam("reference") String reference) {
|
||||
AgentMediaResource resource = agentMediaService.load(reference, SaTokenUtil.getLoginAccount());
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.CACHE_CONTROL, "private, no-store")
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, "inline")
|
||||
.contentType(MediaType.parseMediaType(resource.mimeType()))
|
||||
.contentLength(resource.bytes().length)
|
||||
.body(resource.bytes());
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传一份 Agent 聊天文档并异步触发轻量读取。
|
||||
*
|
||||
* @param file 文档文件
|
||||
* @param mode 聊天模式
|
||||
* @param agentId Agent ID
|
||||
* @param sessionId 会话 ID
|
||||
* @param uploadId 客户端生成的幂等上传 ID
|
||||
* @return 上传与读取状态
|
||||
*/
|
||||
@PostMapping(value = "/media/document/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@SaCheckPermission(value = {
|
||||
"/api/v1/agent/session/query", "/api/v1/agent/save"
|
||||
}, mode = SaMode.OR)
|
||||
public Result<AgentDocumentUploadView> uploadDocument(@RequestParam("file") MultipartFile file,
|
||||
@RequestParam("mode") String mode,
|
||||
@RequestParam("agentId") String agentId,
|
||||
@RequestParam("sessionId") String sessionId,
|
||||
@RequestParam(value = "uploadId", required = false)
|
||||
String uploadId) {
|
||||
return Result.ok(agentDocumentService.upload(
|
||||
file, mode, agentId, sessionId, uploadId, SaTokenUtil.getLoginAccount()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询当前账号一个上传文档的读取状态。
|
||||
*
|
||||
* @param uploadId 上传 ID
|
||||
* @return 最新状态
|
||||
*/
|
||||
@GetMapping("/media/document/status")
|
||||
@SaCheckPermission(value = {
|
||||
"/api/v1/agent/session/query", "/api/v1/agent/save"
|
||||
}, mode = SaMode.OR)
|
||||
public Result<AgentDocumentUploadView> documentStatus(@RequestParam("uploadId") String uploadId) {
|
||||
return Result.ok(agentDocumentService.status(uploadId, SaTokenUtil.getLoginAccount()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 重试一次明确失败的文档读取。
|
||||
*
|
||||
* @param uploadId 上传 ID
|
||||
* @return 重试后的状态
|
||||
*/
|
||||
@PostMapping("/media/document/retry")
|
||||
@SaCheckPermission(value = {
|
||||
"/api/v1/agent/session/query", "/api/v1/agent/save"
|
||||
}, mode = SaMode.OR)
|
||||
public Result<AgentDocumentUploadView> retryDocument(
|
||||
@JsonBody(value = "uploadId", required = true) String uploadId) {
|
||||
return Result.ok(agentDocumentService.retry(uploadId, SaTokenUtil.getLoginAccount()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除当前账号尚未发送的临时文档。
|
||||
*
|
||||
* @param uploadId 上传 ID
|
||||
* @return 操作结果
|
||||
*/
|
||||
@PostMapping("/media/document/delete")
|
||||
@SaCheckPermission(value = {
|
||||
"/api/v1/agent/session/query", "/api/v1/agent/save"
|
||||
}, mode = SaMode.OR)
|
||||
public Result<Void> deleteDocument(
|
||||
@JsonBody(value = "uploadId", required = true) String uploadId) {
|
||||
agentDocumentService.deleteUpload(uploadId, SaTokenUtil.getLoginAccount());
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过鉴权代理流式下载 Agent 私有聊天文档。
|
||||
*
|
||||
* @param reference 稳定文档引用
|
||||
* @return 文档流
|
||||
*/
|
||||
@GetMapping("/media/document/content")
|
||||
@SaCheckPermission(value = {
|
||||
"/api/v1/agent/session/query", "/api/v1/agent/save"
|
||||
}, mode = SaMode.OR)
|
||||
@LogReporterDisabled
|
||||
public ResponseEntity<StreamingResponseBody> documentContent(
|
||||
@RequestParam("reference") String reference) {
|
||||
AgentDocumentResource resource = agentDocumentService.load(
|
||||
reference, SaTokenUtil.getLoginAccount());
|
||||
StreamingResponseBody body = output -> {
|
||||
try (var input = resource.inputStream()) {
|
||||
input.transferTo(output);
|
||||
}
|
||||
};
|
||||
ContentDisposition disposition = ContentDisposition.attachment()
|
||||
.filename(resource.name(), StandardCharsets.UTF_8)
|
||||
.build();
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.CACHE_CONTROL, "private, no-store")
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, disposition.toString())
|
||||
.contentType(MediaType.parseMediaType(resource.mimeType()))
|
||||
.contentLength(resource.size())
|
||||
.body(body);
|
||||
}
|
||||
|
||||
/**
|
||||
* 为输入框预分配稳定会话 ID。
|
||||
*
|
||||
* @param mode 聊天模式
|
||||
* @return 会话信息
|
||||
*/
|
||||
@PostMapping("/composer/session")
|
||||
@SaCheckPermission(value = {
|
||||
"/api/v1/agent/session/query", "/api/v1/agent/save"
|
||||
}, mode = SaMode.OR)
|
||||
public Result<AgentComposerSession> allocateComposerSession(
|
||||
@JsonBody(value = "mode", required = true) String mode) {
|
||||
return Result.ok(agentComposerDraftService.allocateSession(mode));
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存 Agent 输入草稿。
|
||||
*
|
||||
* @param draft 输入草稿
|
||||
* @return 保存后的草稿
|
||||
*/
|
||||
@PostMapping("/composer/draft/persist")
|
||||
@SaCheckPermission(value = {
|
||||
"/api/v1/agent/session/query", "/api/v1/agent/save"
|
||||
}, mode = SaMode.OR)
|
||||
public Result<AgentComposerDraft> saveComposerDraft(@JsonBody AgentComposerDraft draft) {
|
||||
return Result.ok(agentComposerDraftService.save(draft, SaTokenUtil.getLoginAccount()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前会话或最近未发送会话的输入草稿。
|
||||
*
|
||||
* @param mode 聊天模式
|
||||
* @param agentId Agent ID
|
||||
* @param sessionId 会话 ID,可为空
|
||||
* @return 输入草稿
|
||||
*/
|
||||
@GetMapping("/composer/draft")
|
||||
@SaCheckPermission(value = {
|
||||
"/api/v1/agent/session/query", "/api/v1/agent/save"
|
||||
}, mode = SaMode.OR)
|
||||
public Result<AgentComposerDraft> getComposerDraft(@RequestParam("mode") String mode,
|
||||
@RequestParam("agentId") String agentId,
|
||||
@RequestParam(value = "sessionId", required = false) String sessionId) {
|
||||
return Result.ok(agentComposerDraftService.get(mode, agentId, sessionId, SaTokenUtil.getLoginAccount())
|
||||
.orElse(null));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除已发送或主动清空的输入草稿。
|
||||
*
|
||||
* @param mode 聊天模式
|
||||
* @param agentId Agent ID
|
||||
* @param sessionId 会话 ID
|
||||
* @param imageUploadIds 调用方仍持有的上传 ID
|
||||
* @param documentUploadIds 调用方仍持有的文档上传 ID
|
||||
* @param deleteUploads 是否同时删除临时附件
|
||||
* @return 操作结果
|
||||
*/
|
||||
@PostMapping("/composer/draft/delete")
|
||||
@SaCheckPermission(value = {
|
||||
"/api/v1/agent/session/query", "/api/v1/agent/save"
|
||||
}, mode = SaMode.OR)
|
||||
public Result<Void> deleteComposerDraft(@JsonBody(value = "mode", required = true) String mode,
|
||||
@JsonBody(value = "agentId", required = true) String agentId,
|
||||
@JsonBody(value = "sessionId", required = true) String sessionId,
|
||||
@JsonBody(value = "imageUploadIds") List<String> imageUploadIds,
|
||||
@JsonBody(value = "documentUploadIds") List<String> documentUploadIds,
|
||||
@JsonBody(value = "deleteUploads") Boolean deleteUploads) {
|
||||
agentComposerDraftService.delete(mode, agentId, sessionId, imageUploadIds, documentUploadIds,
|
||||
!Boolean.FALSE.equals(deleteUploads), SaTokenUtil.getLoginAccount());
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理 Agent 草稿试运行会话。
|
||||
*
|
||||
* @param sessionId 草稿试运行会话 ID
|
||||
* @return 操作结果
|
||||
*/
|
||||
@PostMapping("/chat/draft/clear")
|
||||
@SaCheckPermission("/api/v1/agent/save")
|
||||
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")
|
||||
@SaCheckPermission(value = {
|
||||
"/api/v1/agent/session/query", "/api/v1/agent/save"
|
||||
}, mode = SaMode.OR)
|
||||
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")
|
||||
@SaCheckPermission(value = {
|
||||
"/api/v1/agent/session/query", "/api/v1/agent/save"
|
||||
}, mode = SaMode.OR)
|
||||
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));
|
||||
}
|
||||
|
||||
/**
|
||||
* 原子替换 Agent 的全部 Skill 草稿绑定。
|
||||
*
|
||||
* @param request 白名单 Skill 引用请求
|
||||
* @return 服务端生成的安全 Skill 摘要
|
||||
*/
|
||||
@PostMapping("/skillBinding/update")
|
||||
@SaCheckPermission("/api/v1/agent/save")
|
||||
public Result<List<AgentDetailView.SkillBindingView>> updateSkillBinding(
|
||||
@JsonBody(required = true, skipConvertError = false) AgentSkillBindingUpdateRequest request) {
|
||||
if (request == null || request.getAgentId() == null) {
|
||||
throw new BusinessException("Agent ID 不能为空");
|
||||
}
|
||||
List<tech.easyflow.agent.entity.AgentSkillBinding> bindings = request.getBindings() == null
|
||||
? List.of()
|
||||
: request.getBindings().stream().map(AgentSkillBindingUpdateRequest.Binding::toEntity).toList();
|
||||
return Result.ok(agentSkillBindingService.replaceBindings(request.getAgentId(), bindings)
|
||||
.stream().map(AgentDetailView.SkillBindingView::from).toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交发布审批。
|
||||
*
|
||||
* @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
|
||||
public Result<?> remove(Serializable id) {
|
||||
throw new BusinessException("Agent 仅支持通过生命周期审批删除");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<?> removeBatch(Collection<Serializable> ids) {
|
||||
throw new BusinessException("Agent 仅支持通过生命周期审批删除");
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询 Agent 分页。
|
||||
*
|
||||
* @param page 分页参数
|
||||
* @param queryWrapper 查询条件
|
||||
* @return Agent 分页
|
||||
*/
|
||||
@Override
|
||||
protected Page<Agent> queryPage(Page<Agent> page, QueryWrapper queryWrapper) {
|
||||
agentVisibilityQueryHelper.applyReadableAccess(queryWrapper);
|
||||
applyPublishedOnlyFilter(queryWrapper);
|
||||
Page<Agent> result = service.page(page, queryWrapper);
|
||||
if (isPublishedOnlyRequest()) {
|
||||
result.setRecords(result.getRecords().stream().map(agent -> service.fromSnapshot(agent.getPublishedSnapshotJson())).toList());
|
||||
}
|
||||
result.getRecords().forEach(this::sanitizeListItem);
|
||||
agentApprovalStateService.fillAgentApprovalState(result.getRecords());
|
||||
aiResourceCreatorNameSupport.fillAgentCreatorNames(result.getRecords());
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理列表无需返回的配置和发布快照,避免敏感运行配置进入浏览器。
|
||||
*
|
||||
* @param agent Agent 列表项
|
||||
*/
|
||||
private void sanitizeListItem(Agent agent) {
|
||||
if (agent == null) {
|
||||
return;
|
||||
}
|
||||
agent.setModelConfigJson(Collections.emptyMap());
|
||||
agent.setGenerationConfigJson(Collections.emptyMap());
|
||||
agent.setPromptConfigJson(Collections.emptyMap());
|
||||
agent.setMemoryConfigJson(Collections.emptyMap());
|
||||
agent.setExecutionConfigJson(Collections.emptyMap());
|
||||
agent.setInteractionConfigJson(Collections.emptyMap());
|
||||
agent.setPublishedSnapshotJson(Collections.emptyMap());
|
||||
agent.setToolBindings(null);
|
||||
agent.setKnowledgeBindings(null);
|
||||
agent.setSkillBindings(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 为仅发布查询追加发布状态条件。
|
||||
*
|
||||
* @param queryWrapper Agent 查询条件
|
||||
*/
|
||||
private void applyPublishedOnlyFilter(QueryWrapper queryWrapper) {
|
||||
if (isPublishedOnlyRequest()) {
|
||||
queryWrapper.eq("publish_status", PublishStatus.PUBLISHED.getCode());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前请求是否只查询已发布 Agent。
|
||||
*
|
||||
* @return 是否仅查询已发布 Agent
|
||||
*/
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将审批执行结果转换为统一响应。
|
||||
*
|
||||
* @param actionResult 审批动作结果
|
||||
* @param approvalMessage 进入审批时的提示
|
||||
* @param directMessage 直接执行时的提示
|
||||
* @return 审批实例响应
|
||||
*/
|
||||
private Result<BigInteger> buildApprovalActionResult(ApprovalActionResult actionResult,
|
||||
String approvalMessage,
|
||||
String directMessage) {
|
||||
return Result.ok(actionResult.isApprovalRequired() ? approvalMessage : directMessage, actionResult.getInstanceId());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package tech.easyflow.admin.controller.agent;
|
||||
|
||||
import tech.easyflow.agent.entity.Agent;
|
||||
import tech.easyflow.agent.entity.AgentKnowledgeBinding;
|
||||
import tech.easyflow.agent.entity.AgentSkillBinding;
|
||||
import tech.easyflow.agent.entity.AgentToolBinding;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 管理端 Agent 草稿安全详情。
|
||||
*
|
||||
* <p>该视图明确排除发布快照以及各绑定的内部资源快照。</p>
|
||||
*/
|
||||
public record AgentDetailView(
|
||||
BigInteger id,
|
||||
BigInteger deptId,
|
||||
String name,
|
||||
String description,
|
||||
String avatar,
|
||||
BigInteger categoryId,
|
||||
BigInteger modelId,
|
||||
Map<String, Object> modelConfigJson,
|
||||
Map<String, Object> generationConfigJson,
|
||||
Map<String, Object> promptConfigJson,
|
||||
Map<String, Object> memoryConfigJson,
|
||||
Map<String, Object> executionConfigJson,
|
||||
Map<String, Object> interactionConfigJson,
|
||||
Integer status,
|
||||
String visibilityScope,
|
||||
String publishStatus,
|
||||
BigInteger currentApprovalInstanceId,
|
||||
Date publishedAt,
|
||||
BigInteger publishedBy,
|
||||
Date created,
|
||||
BigInteger createdBy,
|
||||
Date modified,
|
||||
BigInteger modifiedBy,
|
||||
Boolean approvalPending,
|
||||
String currentApprovalActionType,
|
||||
String displayPublishStatus,
|
||||
String createdByName,
|
||||
List<ToolBindingView> toolBindings,
|
||||
List<KnowledgeBindingView> knowledgeBindings,
|
||||
List<SkillBindingView> skillBindings) {
|
||||
|
||||
/**
|
||||
* 从领域实体构造安全详情。
|
||||
*
|
||||
* @param agent Agent 领域实体
|
||||
* @return 安全详情
|
||||
*/
|
||||
public static AgentDetailView from(Agent agent) {
|
||||
return new AgentDetailView(agent.getId(), agent.getDeptId(), agent.getName(), agent.getDescription(),
|
||||
agent.getAvatar(), agent.getCategoryId(), agent.getModelId(), agent.getModelConfigJson(),
|
||||
agent.getGenerationConfigJson(), agent.getPromptConfigJson(), agent.getMemoryConfigJson(),
|
||||
agent.getExecutionConfigJson(), agent.getInteractionConfigJson(), agent.getStatus(),
|
||||
agent.getVisibilityScope(), agent.getPublishStatus(), agent.getCurrentApprovalInstanceId(),
|
||||
agent.getPublishedAt(), agent.getPublishedBy(), agent.getCreated(), agent.getCreatedBy(),
|
||||
agent.getModified(), agent.getModifiedBy(), agent.getApprovalPending(),
|
||||
agent.getCurrentApprovalActionType(), agent.getDisplayPublishStatus(), agent.getCreatedByName(),
|
||||
mapTools(agent.getToolBindings()), mapKnowledges(agent.getKnowledgeBindings()),
|
||||
mapSkills(agent.getSkillBindings()));
|
||||
}
|
||||
|
||||
private static List<ToolBindingView> mapTools(List<AgentToolBinding> bindings) {
|
||||
return bindings == null ? List.of() : bindings.stream().map(ToolBindingView::from).toList();
|
||||
}
|
||||
|
||||
private static List<KnowledgeBindingView> mapKnowledges(List<AgentKnowledgeBinding> bindings) {
|
||||
return bindings == null ? List.of() : bindings.stream().map(KnowledgeBindingView::from).toList();
|
||||
}
|
||||
|
||||
private static List<SkillBindingView> mapSkills(List<AgentSkillBinding> bindings) {
|
||||
return bindings == null ? List.of() : bindings.stream().map(SkillBindingView::from).toList();
|
||||
}
|
||||
|
||||
/** Agent 直接 Tool 草稿绑定。 */
|
||||
public record ToolBindingView(BigInteger id, String toolType, BigInteger targetId, String toolName,
|
||||
Boolean enabled, Boolean hitlEnabled, Map<String, Object> hitlConfigJson,
|
||||
Map<String, Object> optionsJson, Integer sortNo,
|
||||
Map<String, Object> resourceSummary) {
|
||||
/** @param value 实体 @return 安全绑定 */
|
||||
static ToolBindingView from(AgentToolBinding value) {
|
||||
return new ToolBindingView(value.getId(), value.getToolType(), value.getTargetId(), value.getToolName(),
|
||||
value.getEnabled(), value.getHitlEnabled(), value.getHitlConfigJson(), value.getOptionsJson(),
|
||||
value.getSortNo(), value.getResourceSummary());
|
||||
}
|
||||
}
|
||||
|
||||
/** Agent 知识库草稿绑定。 */
|
||||
public record KnowledgeBindingView(BigInteger id, BigInteger knowledgeId, String retrievalMode,
|
||||
Boolean enabled, Map<String, Object> optionsJson, Integer sortNo,
|
||||
Map<String, Object> resourceSummary) {
|
||||
/** @param value 实体 @return 安全绑定 */
|
||||
static KnowledgeBindingView from(AgentKnowledgeBinding value) {
|
||||
return new KnowledgeBindingView(value.getId(), value.getKnowledgeId(), value.getRetrievalMode(),
|
||||
value.getEnabled(), value.getOptionsJson(), value.getSortNo(), value.getResourceSummary());
|
||||
}
|
||||
}
|
||||
|
||||
/** Agent Skill 草稿绑定。 */
|
||||
public record SkillBindingView(BigInteger id, BigInteger skillId, Integer sortNo,
|
||||
Map<String, Object> resourceSummary) {
|
||||
/** @param value 实体 @return 安全绑定 */
|
||||
static SkillBindingView from(AgentSkillBinding value) {
|
||||
return new SkillBindingView(value.getId(), value.getSkillId(), value.getSortNo(),
|
||||
value.getResourceSummary());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package tech.easyflow.admin.controller.agent;
|
||||
|
||||
import tech.easyflow.agent.entity.Agent;
|
||||
import tech.easyflow.agent.entity.AgentKnowledgeBinding;
|
||||
import tech.easyflow.agent.entity.AgentSkillBinding;
|
||||
import tech.easyflow.agent.entity.AgentToolBinding;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Agent 设计器原子保存请求。
|
||||
*
|
||||
* <p>绑定变更标记由设计器基于加载后的稳定业务字段计算。服务端仍会执行权限、状态与幂等比较,
|
||||
* 标记为未变化的绑定不会进入查询、外部资源校验或整组重写流程。</p>
|
||||
*/
|
||||
public class AgentDraftSaveRequest {
|
||||
|
||||
private Agent agent;
|
||||
private List<AgentToolBinding> toolBindings;
|
||||
private boolean replaceToolBindings;
|
||||
private List<AgentKnowledgeBinding> knowledgeBindings;
|
||||
private boolean replaceKnowledgeBindings;
|
||||
private List<AgentSkillBindingUpdateRequest.Binding> skillBindings;
|
||||
private boolean replaceSkillBindings;
|
||||
|
||||
/** 创建空请求。 */
|
||||
public AgentDraftSaveRequest() {
|
||||
}
|
||||
|
||||
/** @return Agent 草稿 */
|
||||
public Agent getAgent() {
|
||||
return agent;
|
||||
}
|
||||
|
||||
/** @param agent Agent 草稿 */
|
||||
public void setAgent(Agent agent) {
|
||||
this.agent = agent;
|
||||
}
|
||||
|
||||
/** @return 工具绑定 */
|
||||
public List<AgentToolBinding> getToolBindings() {
|
||||
return toolBindings;
|
||||
}
|
||||
|
||||
/** @param toolBindings 工具绑定 */
|
||||
public void setToolBindings(List<AgentToolBinding> toolBindings) {
|
||||
this.toolBindings = toolBindings;
|
||||
}
|
||||
|
||||
/** @return 是否替换工具绑定 */
|
||||
public boolean isReplaceToolBindings() {
|
||||
return replaceToolBindings;
|
||||
}
|
||||
|
||||
/** @param replaceToolBindings 是否替换工具绑定 */
|
||||
public void setReplaceToolBindings(boolean replaceToolBindings) {
|
||||
this.replaceToolBindings = replaceToolBindings;
|
||||
}
|
||||
|
||||
/** @return 知识库绑定 */
|
||||
public List<AgentKnowledgeBinding> getKnowledgeBindings() {
|
||||
return knowledgeBindings;
|
||||
}
|
||||
|
||||
/** @param knowledgeBindings 知识库绑定 */
|
||||
public void setKnowledgeBindings(List<AgentKnowledgeBinding> knowledgeBindings) {
|
||||
this.knowledgeBindings = knowledgeBindings;
|
||||
}
|
||||
|
||||
/** @return 是否替换知识库绑定 */
|
||||
public boolean isReplaceKnowledgeBindings() {
|
||||
return replaceKnowledgeBindings;
|
||||
}
|
||||
|
||||
/** @param replaceKnowledgeBindings 是否替换知识库绑定 */
|
||||
public void setReplaceKnowledgeBindings(boolean replaceKnowledgeBindings) {
|
||||
this.replaceKnowledgeBindings = replaceKnowledgeBindings;
|
||||
}
|
||||
|
||||
/** @return Skill 绑定 */
|
||||
public List<AgentSkillBindingUpdateRequest.Binding> getSkillBindings() {
|
||||
return skillBindings;
|
||||
}
|
||||
|
||||
/** @param skillBindings Skill 绑定 */
|
||||
public void setSkillBindings(List<AgentSkillBindingUpdateRequest.Binding> skillBindings) {
|
||||
this.skillBindings = skillBindings;
|
||||
}
|
||||
|
||||
/** @return 是否替换 Skill 绑定 */
|
||||
public boolean isReplaceSkillBindings() {
|
||||
return replaceSkillBindings;
|
||||
}
|
||||
|
||||
/** @param replaceSkillBindings 是否替换 Skill 绑定 */
|
||||
public void setReplaceSkillBindings(boolean replaceSkillBindings) {
|
||||
this.replaceSkillBindings = replaceSkillBindings;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 Skill 白名单引用转换为领域绑定。
|
||||
*
|
||||
* @return 最小 Skill 绑定列表
|
||||
*/
|
||||
public List<AgentSkillBinding> toSkillBindings() {
|
||||
return skillBindings == null
|
||||
? List.of() : skillBindings.stream().map(AgentSkillBindingUpdateRequest.Binding::toEntity).toList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
package tech.easyflow.admin.controller.agent;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
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.agent.service.AgentOptionQueryService;
|
||||
import tech.easyflow.agent.vo.AgentOptionView;
|
||||
import tech.easyflow.agent.vo.AgentResourceOptionsView;
|
||||
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.exceptions.BusinessException;
|
||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Agent 管理端会话控制器。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/agent/session")
|
||||
@SaCheckPermission("/api/v1/agent/session/query")
|
||||
public class AgentSessionController {
|
||||
|
||||
private final AgentSessionService agentSessionService;
|
||||
private final AgentOptionQueryService agentOptionQueryService;
|
||||
|
||||
/**
|
||||
* 创建 Agent 管理端会话控制器。
|
||||
*
|
||||
* @param agentSessionService Agent 会话服务
|
||||
* @param agentOptionQueryService Agent 安全选项服务
|
||||
*/
|
||||
public AgentSessionController(AgentSessionService agentSessionService,
|
||||
AgentOptionQueryService agentOptionQueryService) {
|
||||
this.agentSessionService = agentSessionService;
|
||||
this.agentOptionQueryService = agentOptionQueryService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询正式聊天可使用的已发布 Agent。
|
||||
*
|
||||
* @return Agent 安全选项
|
||||
*/
|
||||
@GetMapping("/options")
|
||||
public Result<List<AgentOptionView>> options() {
|
||||
return Result.ok(agentOptionQueryService.listAgentOptions(true));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询正式聊天可附加的知识库。
|
||||
*
|
||||
* @return 知识库安全选项
|
||||
*/
|
||||
@GetMapping("/knowledgeOptions")
|
||||
public Result<List<AgentResourceOptionsView.ResourceOption>> knowledgeOptions() {
|
||||
return Result.ok(agentOptionQueryService.listKnowledgeOptions());
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前登录账号。
|
||||
*
|
||||
* @return 当前登录账号
|
||||
* @throws BusinessException 登录信息失效时抛出
|
||||
*/
|
||||
private LoginAccount currentAccount() {
|
||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||
if (account == null || account.getId() == null || account.getTenantId() == null) {
|
||||
throw new BusinessException("当前登录状态失效,请重新登录后再试");
|
||||
}
|
||||
return account;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package tech.easyflow.admin.controller.agent;
|
||||
|
||||
import tech.easyflow.agent.entity.AgentSkillBinding;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Agent Skill 整组替换请求。
|
||||
*
|
||||
* <p>使用标准 JavaBean 以兼容 {@code @JsonBody} 的 Fastjson 1 嵌套列表转换。</p>
|
||||
*/
|
||||
public class AgentSkillBindingUpdateRequest {
|
||||
|
||||
private BigInteger agentId;
|
||||
private List<Binding> bindings;
|
||||
|
||||
/** 创建空请求。 */
|
||||
public AgentSkillBindingUpdateRequest() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 Agent Skill 绑定请求。
|
||||
*
|
||||
* @param agentId Agent ID
|
||||
* @param bindings Skill 引用
|
||||
*/
|
||||
public AgentSkillBindingUpdateRequest(BigInteger agentId, List<Binding> bindings) {
|
||||
this.agentId = agentId;
|
||||
this.bindings = bindings;
|
||||
}
|
||||
|
||||
/** @return Agent ID */
|
||||
public BigInteger getAgentId() { return agentId; }
|
||||
/** @param agentId Agent ID */
|
||||
public void setAgentId(BigInteger agentId) { this.agentId = agentId; }
|
||||
/** @return Skill 引用 */
|
||||
public List<Binding> getBindings() { return bindings; }
|
||||
/** @param bindings Skill 引用 */
|
||||
public void setBindings(List<Binding> bindings) { this.bindings = bindings; }
|
||||
|
||||
/** 客户端允许提交的最小 Skill 引用。 */
|
||||
public static class Binding {
|
||||
|
||||
private BigInteger skillId;
|
||||
private Integer sortNo;
|
||||
|
||||
/** 创建空绑定。 */
|
||||
public Binding() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建最小 Skill 绑定。
|
||||
*
|
||||
* @param skillId Skill ID
|
||||
* @param sortNo 排序号
|
||||
*/
|
||||
public Binding(BigInteger skillId, Integer sortNo) {
|
||||
this.skillId = skillId;
|
||||
this.sortNo = sortNo;
|
||||
}
|
||||
|
||||
/** @return Skill ID */
|
||||
public BigInteger getSkillId() { return skillId; }
|
||||
/** @param skillId Skill ID */
|
||||
public void setSkillId(BigInteger skillId) { this.skillId = skillId; }
|
||||
/** @return 排序号 */
|
||||
public Integer getSortNo() { return sortNo; }
|
||||
/** @param sortNo 排序号 */
|
||||
public void setSortNo(Integer sortNo) { this.sortNo = sortNo; }
|
||||
|
||||
/**
|
||||
* 转换为不含任何服务端快照的领域引用。
|
||||
*
|
||||
* @return 最小 Skill 绑定
|
||||
*/
|
||||
public AgentSkillBinding toEntity() {
|
||||
AgentSkillBinding value = new AgentSkillBinding();
|
||||
value.setSkillId(skillId);
|
||||
value.setSortNo(sortNo);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,12 +13,19 @@ import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
import tech.easyflow.admin.controller.ai.support.BotResourceCreatorNameSupport;
|
||||
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.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.impl.BotServiceImpl;
|
||||
import tech.easyflow.common.audio.core.AudioServiceManager;
|
||||
@@ -28,9 +35,11 @@ import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||
import tech.easyflow.chatlog.service.ChatRoundOperateService;
|
||||
import tech.easyflow.core.chat.protocol.sse.ChatSseEmitter;
|
||||
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;
|
||||
@@ -70,6 +79,14 @@ public class BotController extends BaseCurdController<BotService, Bot> {
|
||||
private CategoryPermissionService categoryPermissionService;
|
||||
@Resource
|
||||
private BotPublishAppService botPublishAppService;
|
||||
@Resource
|
||||
private ChatRoundOperateService chatRoundOperateService;
|
||||
@Resource
|
||||
private BotApprovalStateService botApprovalStateService;
|
||||
@Resource
|
||||
private BotResourceCreatorNameSupport botResourceCreatorNameSupport;
|
||||
@Resource
|
||||
private ChatWorkspaceService chatWorkspaceService;
|
||||
|
||||
public BotController(BotService service, ModelService modelService, BotWorkflowService botWorkflowService,
|
||||
BotDocumentCollectionService botDocumentCollectionService, BotMessageService botMessageService) {
|
||||
@@ -80,6 +97,16 @@ public class BotController extends BaseCurdController<BotService, Bot> {
|
||||
this.botMessageService = botMessageService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取智能体列表关键字搜索字段。
|
||||
*
|
||||
* @return 标题和描述属性
|
||||
*/
|
||||
@Override
|
||||
protected String[] getKeywordSearchProperties() {
|
||||
return new String[]{"title", "description"};
|
||||
}
|
||||
|
||||
@Resource
|
||||
private BotPluginService botPluginService;
|
||||
|
||||
@@ -155,13 +182,30 @@ public class BotController extends BaseCurdController<BotService, Bot> {
|
||||
@JsonBody(value = "botId", required = true) BigInteger botId,
|
||||
@JsonBody(value = "conversationId", required = true) BigInteger conversationId,
|
||||
@JsonBody(value = "messages") List<Map<String, String>> messages,
|
||||
@JsonBody(value = "attachments") List<String> attachments
|
||||
@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();
|
||||
if (usePublishedOnly) {
|
||||
chatWorkspaceService.assertSessionContinuable(requireCurrentLoginAccount(), conversationId, botId);
|
||||
}
|
||||
if (regenerateRoundId != null) {
|
||||
chatRoundOperateService.requireRegeneratableRound(conversationId, regenerateRoundId);
|
||||
}
|
||||
|
||||
// 前置校验:失败则直接返回错误SseEmitter
|
||||
SseEmitter errorEmitter = botService.checkChatBeforeStart(botId, prompt, conversationId.toString(), chatCheckResult);
|
||||
SseEmitter errorEmitter = botService.checkChatBeforeStart(
|
||||
botId,
|
||||
prompt,
|
||||
conversationId.toString(),
|
||||
chatCheckResult,
|
||||
usePublishedOnly,
|
||||
regenerateRoundId
|
||||
);
|
||||
if (errorEmitter != null) {
|
||||
return errorEmitter;
|
||||
}
|
||||
@@ -172,7 +216,7 @@ public class BotController extends BaseCurdController<BotService, Bot> {
|
||||
messages,
|
||||
chatCheckResult,
|
||||
attachments,
|
||||
buildRuntimeContext(chatCheckResult.getAiBot(), conversationId, prompt, attachments)
|
||||
buildRuntimeContext(chatCheckResult.getAiBot(), conversationId, prompt, attachments, extraKnowledgeIds, regenerateRoundId)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -187,33 +231,52 @@ public class BotController extends BaseCurdController<BotService, Bot> {
|
||||
@GetMapping("getDetail")
|
||||
@SaIgnore
|
||||
public Result<Bot> getDetail(String id) {
|
||||
Bot bot = StpUtil.isLogin() ? botService.getDetail(id) : botService.getPublishedDetail(id);
|
||||
if (bot != null && StpUtil.isLogin()) {
|
||||
categoryPermissionService.assertCategoryResourceVisible("BOT", bot.getCreatedBy(), bot.getCategoryId(), "无权限访问聊天助手");
|
||||
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 (bot == null) {
|
||||
if (rawBot == null) {
|
||||
return Result.ok(null);
|
||||
}
|
||||
if (!StpUtil.isLogin() && !tech.easyflow.ai.enums.PublishStatus.from(bot.getPublishStatus()).isExternallyVisible()) {
|
||||
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()) {
|
||||
botApprovalStateService.fillApprovalState(bot);
|
||||
}
|
||||
return Result.ok(bot);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SaIgnore
|
||||
public Result<Bot> detail(String id) {
|
||||
Bot data = StpUtil.isLogin() ? botService.getDetail(id) : botService.getPublishedDetail(id);
|
||||
if (data == null) {
|
||||
return Result.ok(data);
|
||||
boolean publishedOnly = isPublishedOnlyRequest();
|
||||
Bot rawData = StpUtil.isLogin() ? botService.getDetail(id) : botService.getPublishedDetail(id);
|
||||
if (rawData == null) {
|
||||
return Result.ok(rawData);
|
||||
}
|
||||
if (StpUtil.isLogin()) {
|
||||
categoryPermissionService.assertCategoryResourceVisible("BOT", data.getCreatedBy(), data.getCategoryId(), "无权限访问聊天助手");
|
||||
categoryPermissionService.assertCategoryResourceVisible("BOT", rawData.getCreatedBy(), rawData.getCategoryId(), "无权限访问聊天助手");
|
||||
}
|
||||
|
||||
if (!StpUtil.isLogin() && !tech.easyflow.ai.enums.PublishStatus.from(data.getPublishStatus()).isExternallyVisible()) {
|
||||
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();
|
||||
if (llmOptions == null) {
|
||||
@@ -221,6 +284,9 @@ public class BotController extends BaseCurdController<BotService, Bot> {
|
||||
}
|
||||
|
||||
if (data.getModelId() == null) {
|
||||
if (StpUtil.isLogin()) {
|
||||
botApprovalStateService.fillApprovalState(data);
|
||||
}
|
||||
return Result.ok(data);
|
||||
}
|
||||
|
||||
@@ -229,6 +295,9 @@ public class BotController extends BaseCurdController<BotService, Bot> {
|
||||
|
||||
if (llm == null) {
|
||||
data.setModelId(null);
|
||||
if (StpUtil.isLogin()) {
|
||||
botApprovalStateService.fillApprovalState(data);
|
||||
}
|
||||
return Result.ok(data);
|
||||
}
|
||||
|
||||
@@ -242,33 +311,98 @@ public class BotController extends BaseCurdController<BotService, Bot> {
|
||||
|
||||
}
|
||||
|
||||
if (StpUtil.isLogin()) {
|
||||
botApprovalStateService.fillApprovalState(data);
|
||||
}
|
||||
return Result.ok(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交聊天助手发布审批。
|
||||
*
|
||||
* @param id 助手 ID
|
||||
* @param applicationReason 审批说明
|
||||
* @return 审批实例 ID
|
||||
*/
|
||||
@PostMapping("/submitPublishApproval")
|
||||
@SaCheckPermission("/api/v1/bot/save")
|
||||
public Result<BigInteger> submitPublishApproval(@JsonBody("id") BigInteger id) {
|
||||
return Result.ok(botPublishAppService.submitPublishApproval(id));
|
||||
public Result<BigInteger> submitPublishApproval(
|
||||
@JsonBody("id") BigInteger id,
|
||||
@JsonBody("applicationReason") String applicationReason
|
||||
) {
|
||||
return buildApprovalActionResult(
|
||||
botPublishAppService.submitPublishApproval(id, applicationReason),
|
||||
"已提交发布审批",
|
||||
"已直接发布"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 预检聊天助手发布是否命中审批流。
|
||||
*
|
||||
* @param id 助手 ID
|
||||
* @return 是否需要审批
|
||||
*/
|
||||
@GetMapping("/publishApprovalRequirement")
|
||||
@SaCheckPermission("/api/v1/bot/save")
|
||||
public Result<Boolean> publishApprovalRequirement(@RequestParam BigInteger id) {
|
||||
return Result.ok(botPublishAppService.isPublishApprovalRequired(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 Result.ok(botPublishAppService.submitDeleteApproval(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()));
|
||||
return Result.ok(service.list(queryWrapper));
|
||||
List<Bot> bots = service.list(queryWrapper);
|
||||
if (isPublishedOnlyRequest()) {
|
||||
bots = bots.stream().map(botService::toPublishedView).toList();
|
||||
}
|
||||
botApprovalStateService.fillApprovalState(bots);
|
||||
return Result.ok(bots);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Page<Bot> queryPage(Page<Bot> page, QueryWrapper queryWrapper) {
|
||||
applyCategoryPermission(queryWrapper);
|
||||
return super.queryPage(page, queryWrapper);
|
||||
applyPublishedOnlyFilter(queryWrapper);
|
||||
Page<Bot> result = super.queryPage(page, queryWrapper);
|
||||
if (isPublishedOnlyRequest()) {
|
||||
result.setRecords(result.getRecords().stream().map(botService::toPublishedView).toList());
|
||||
}
|
||||
botApprovalStateService.fillApprovalState(result.getRecords());
|
||||
botResourceCreatorNameSupport.fillCreatorNames(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) {
|
||||
@@ -356,24 +490,55 @@ public class BotController extends BaseCurdController<BotService, Bot> {
|
||||
return result;
|
||||
}
|
||||
|
||||
private ChatRuntimeContext buildRuntimeContext(Bot bot, BigInteger conversationId, String prompt, List<String> attachments) {
|
||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||
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 == null ? BigInteger.ZERO : account.getTenantId());
|
||||
context.setDeptId(account == null ? BigInteger.ZERO : account.getDeptId());
|
||||
context.setUserId(account == null ? BigInteger.ZERO : account.getId());
|
||||
context.setUserAccount(account == null ? "admin" : account.getLoginName());
|
||||
context.setUserName(account == null ? "管理员" : (StringUtils.hasText(account.getNickname()) ? account.getNickname() : account.getLoginName()));
|
||||
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
|
||||
protected Result<?> onRemoveBefore(Collection<Serializable> ids) {
|
||||
QueryWrapper queryWrapperKnowledge = QueryWrapper.create().in(BotDocumentCollection::getBotId, ids);
|
||||
@@ -388,7 +553,7 @@ public class BotController extends BaseCurdController<BotService, Bot> {
|
||||
@Override
|
||||
@PostMapping("remove")
|
||||
public Result<?> remove(@JsonBody(value = "id", required = true) Serializable id) {
|
||||
return Result.fail(1, "请提交删除审批");
|
||||
return Result.fail(1, "请使用发布状态操作删除聊天助手");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,6 +4,7 @@ import org.springframework.web.bind.annotation.PostMapping;
|
||||
import tech.easyflow.ai.entity.Plugin;
|
||||
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.domain.Result;
|
||||
import tech.easyflow.common.tree.Tree;
|
||||
@@ -58,7 +59,14 @@ public class BotPluginController extends BaseCurdController<BotPluginService, Bo
|
||||
List<BotPlugin> visibleList = new ArrayList<>();
|
||||
for (BotPlugin relation : botPlugins) {
|
||||
Plugin plugin = relation.getAiPlugin();
|
||||
if (plugin == null || pluginVisibilityService.canAccessPlugin(plugin.getCreatedBy(), plugin.getId())) {
|
||||
if (plugin == null) {
|
||||
visibleList.add(relation);
|
||||
continue;
|
||||
}
|
||||
if (PluginType.isWorkflow(plugin.getType())) {
|
||||
continue;
|
||||
}
|
||||
if (pluginVisibilityService.canAccessPlugin(plugin.getCreatedBy(), plugin.getId())) {
|
||||
visibleList.add(relation);
|
||||
}
|
||||
}
|
||||
@@ -73,7 +81,13 @@ public class BotPluginController extends BaseCurdController<BotPluginService, Bo
|
||||
List<Plugin> plugins = botPluginService.getList(botId);
|
||||
List<Plugin> visibleList = new ArrayList<>();
|
||||
for (Plugin plugin : plugins) {
|
||||
if (plugin == null || pluginVisibilityService.canAccessPlugin(plugin.getCreatedBy(), plugin.getId())) {
|
||||
if (plugin == null) {
|
||||
continue;
|
||||
}
|
||||
if (PluginType.isWorkflow(plugin.getType())) {
|
||||
continue;
|
||||
}
|
||||
if (pluginVisibilityService.canAccessPlugin(plugin.getCreatedBy(), plugin.getId())) {
|
||||
visibleList.add(plugin);
|
||||
}
|
||||
}
|
||||
@@ -105,6 +119,9 @@ public class BotPluginController extends BaseCurdController<BotPluginService, Bo
|
||||
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(), "无权限绑定插件");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package tech.easyflow.admin.controller.ai;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import tech.easyflow.ai.entity.PluginItem;
|
||||
import tech.easyflow.ai.service.BotPluginService;
|
||||
import tech.easyflow.common.annotation.UsePermission;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 旧 Bot 插件工具绑定查询控制器。
|
||||
*
|
||||
* <p>保留历史接口地址,同时将 Bot 表依赖限制在 Bot 专属代码中。</p>
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/pluginItem")
|
||||
@UsePermission(moduleName = "/api/v1/plugin")
|
||||
public class BotPluginItemController {
|
||||
|
||||
private final BotPluginService botPluginService;
|
||||
|
||||
/**
|
||||
* 创建 Bot 插件工具绑定查询控制器。
|
||||
*
|
||||
* @param botPluginService Bot 插件绑定服务
|
||||
*/
|
||||
public BotPluginItemController(BotPluginService botPluginService) {
|
||||
this.botPluginService = botPluginService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询插件工具,并标记指定 Bot 已绑定的工具。
|
||||
*
|
||||
* @param pluginId 插件 ID
|
||||
* @param botId Bot ID
|
||||
* @return 插件工具列表
|
||||
*/
|
||||
@PostMapping("/toolsList")
|
||||
@SaCheckPermission("/api/v1/plugin/query")
|
||||
public Result<List<PluginItem>> searchPluginTools(
|
||||
@JsonBody(value = "pluginId", required = true) BigInteger pluginId,
|
||||
@JsonBody(value = "botId", required = false) BigInteger botId) {
|
||||
return Result.ok(botPluginService.searchPluginTools(pluginId, botId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询指定 Bot 已绑定的插件工具。
|
||||
*
|
||||
* @param botId Bot ID
|
||||
* @return 已绑定插件工具列表
|
||||
*/
|
||||
@PostMapping("/tool/list")
|
||||
@SaCheckPermission("/api/v1/plugin/query")
|
||||
public Result<List<PluginItem>> getPluginTools(
|
||||
@JsonBody(value = "botId", required = true) BigInteger botId) {
|
||||
return Result.ok(botPluginService.getPluginTools(botId));
|
||||
}
|
||||
}
|
||||
@@ -1,41 +1,151 @@
|
||||
package tech.easyflow.admin.controller.ai;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.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.agent.service.AgentOptionQueryService;
|
||||
import tech.easyflow.agent.vo.AgentOptionView;
|
||||
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 tech.easyflow.system.service.CategoryPermissionService;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/chatHistory")
|
||||
@SaCheckPermission("/api/v1/chatHistory/query")
|
||||
public class ChatHistoryController {
|
||||
|
||||
private final ChatHistoryManageService chatHistoryManageService;
|
||||
private final CategoryPermissionService categoryPermissionService;
|
||||
private final AgentOptionQueryService agentOptionQueryService;
|
||||
|
||||
public ChatHistoryController(ChatHistoryManageService chatHistoryManageService) {
|
||||
/**
|
||||
* 创建聊天历史控制器。
|
||||
*
|
||||
* @param chatHistoryManageService 聊天历史管理服务
|
||||
* @param categoryPermissionService 账号权限服务
|
||||
* @param agentOptionQueryService Agent 安全选项服务
|
||||
*/
|
||||
public ChatHistoryController(ChatHistoryManageService chatHistoryManageService,
|
||||
CategoryPermissionService categoryPermissionService,
|
||||
AgentOptionQueryService agentOptionQueryService) {
|
||||
this.chatHistoryManageService = chatHistoryManageService;
|
||||
this.categoryPermissionService = categoryPermissionService;
|
||||
this.agentOptionQueryService = agentOptionQueryService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询聊天记录筛选可使用的 Agent。
|
||||
*
|
||||
* @return Agent 安全选项
|
||||
*/
|
||||
@GetMapping("/agentOptions")
|
||||
public Result<List<AgentOptionView>> agentOptions() {
|
||||
return Result.ok(agentOptionQueryService.listAgentOptions(false));
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询当前账号可见的 Agent 会话。
|
||||
*
|
||||
* @param query 会话筛选条件
|
||||
* @return 会话分页结果
|
||||
*/
|
||||
@GetMapping("/sessions")
|
||||
public Result<ChatSessionPage> listSessions(ChatSessionFilterQuery query) {
|
||||
return Result.ok(chatHistoryManageService.queryAdminSessions(query));
|
||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||
return Result.ok(chatHistoryManageService.queryAdminSessions(
|
||||
account.getId(),
|
||||
categoryPermissionService.isSuperAdmin(account),
|
||||
query
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前账号可见的 Agent 会话详情。
|
||||
*
|
||||
* @param sessionId 会话 ID
|
||||
* @return 会话详情
|
||||
*/
|
||||
@GetMapping("/sessions/{sessionId}")
|
||||
public Result<ChatSessionSummary> getSession(@PathVariable BigInteger sessionId) {
|
||||
return Result.ok(chatHistoryManageService.getAdminSession(sessionId));
|
||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||
return Result.ok(chatHistoryManageService.getAdminSession(
|
||||
account.getId(),
|
||||
categoryPermissionService.isSuperAdmin(account),
|
||||
sessionId
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询当前账号可见会话的消息。
|
||||
*
|
||||
* @param sessionId 会话 ID
|
||||
* @param query 消息分页条件
|
||||
* @return 消息分页结果
|
||||
*/
|
||||
@GetMapping("/sessions/{sessionId}/messages")
|
||||
public Result<ChatHistoryPage> queryMessages(@PathVariable BigInteger sessionId, ChatPageQuery query) {
|
||||
return Result.ok(chatHistoryManageService.queryAdminMessages(sessionId, query));
|
||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||
return Result.ok(chatHistoryManageService.queryAdminMessages(
|
||||
account.getId(),
|
||||
categoryPermissionService.isSuperAdmin(account),
|
||||
sessionId,
|
||||
query
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询当前账号可见会话的答案版本。
|
||||
*
|
||||
* @param sessionId 会话 ID
|
||||
* @param roundId 对话轮次 ID
|
||||
* @return 答案版本列表
|
||||
*/
|
||||
@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.listAdminRoundVariants(
|
||||
account.getId(),
|
||||
categoryPermissionService.isSuperAdmin(account),
|
||||
sessionId,
|
||||
roundId
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择当前账号可见会话的答案版本。
|
||||
*
|
||||
* @param sessionId 会话 ID
|
||||
* @param roundId 对话轮次 ID
|
||||
* @param variantIndex 目标版本索引
|
||||
* @return 选中的答案记录
|
||||
*/
|
||||
@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(
|
||||
account.getId(),
|
||||
categoryPermissionService.isSuperAdmin(account),
|
||||
sessionId,
|
||||
roundId,
|
||||
variantIndex
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,18 @@
|
||||
package tech.easyflow.admin.controller.ai;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import com.easyagents.core.model.embedding.EmbeddingModel;
|
||||
import com.mybatisflex.core.paginate.Page;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import tech.easyflow.ai.dto.DocumentChunkContentUpdateRequest;
|
||||
import tech.easyflow.ai.dto.DocumentChunkSyncRetryRequest;
|
||||
import tech.easyflow.ai.dto.DocumentChunkSyncStatus;
|
||||
import tech.easyflow.ai.dto.DocumentChunkSyncStatusRequest;
|
||||
import tech.easyflow.ai.entity.DocumentChunk;
|
||||
import tech.easyflow.ai.entity.DocumentCollection;
|
||||
import tech.easyflow.ai.entity.Model;
|
||||
import tech.easyflow.ai.service.DocumentChunkService;
|
||||
import tech.easyflow.ai.service.DocumentCollectionService;
|
||||
import tech.easyflow.ai.service.ModelService;
|
||||
import tech.easyflow.common.annotation.UsePermission;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||
import tech.easyflow.common.web.controller.BaseController;
|
||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||
import com.easyagents.core.document.Document;
|
||||
import com.easyagents.core.store.DocumentStore;
|
||||
import com.easyagents.core.store.StoreOptions;
|
||||
import com.easyagents.core.store.StoreResult;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -27,12 +23,8 @@ import tech.easyflow.system.enums.ResourceAction;
|
||||
import tech.easyflow.system.enums.ResourceLookup;
|
||||
import tech.easyflow.system.permission.resource.RequireResourceAccess;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 控制层。
|
||||
@@ -43,19 +35,12 @@ import java.util.Map;
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/documentChunk")
|
||||
@UsePermission(moduleName = "/api/v1/documentCollection")
|
||||
public class DocumentChunkController extends BaseCurdController<DocumentChunkService, DocumentChunk> {
|
||||
public class DocumentChunkController extends BaseController {
|
||||
|
||||
@Resource
|
||||
DocumentCollectionService documentCollectionService;
|
||||
|
||||
@Resource
|
||||
ModelService modelService;
|
||||
|
||||
@Resource
|
||||
DocumentChunkService documentChunkService;
|
||||
private final DocumentChunkService documentChunkService;
|
||||
|
||||
public DocumentChunkController(DocumentChunkService service) {
|
||||
super(service);
|
||||
this.documentChunkService = service;
|
||||
}
|
||||
|
||||
@GetMapping("page")
|
||||
@@ -67,9 +52,30 @@ public class DocumentChunkController extends BaseCurdController<DocumentChunkSer
|
||||
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);
|
||||
public Result<Page<DocumentChunk>> page(
|
||||
HttpServletRequest request,
|
||||
Long pageNumber,
|
||||
Long pageSize
|
||||
) {
|
||||
String documentIdValue = request.getParameter("documentId");
|
||||
if (documentIdValue == null || documentIdValue.isBlank()) {
|
||||
return Result.<Page<DocumentChunk>>fail("documentId不能为空", null);
|
||||
}
|
||||
BigInteger documentId;
|
||||
try {
|
||||
documentId = new BigInteger(documentIdValue);
|
||||
} catch (NumberFormatException e) {
|
||||
return Result.<Page<DocumentChunk>>fail("documentId格式不正确", null);
|
||||
}
|
||||
long normalizedPageNumber = pageNumber == null || pageNumber < 1 ? 1 : pageNumber;
|
||||
long normalizedPageSize = pageSize == null || pageSize < 1 ? 10 : pageSize;
|
||||
QueryWrapper query = QueryWrapper.create()
|
||||
.eq(DocumentChunk::getDocumentId, documentId)
|
||||
.orderBy("sorting asc");
|
||||
return Result.ok(documentChunkService.page(
|
||||
new Page<>(normalizedPageNumber, normalizedPageSize),
|
||||
query
|
||||
));
|
||||
}
|
||||
|
||||
@PostMapping("update")
|
||||
@@ -78,39 +84,23 @@ public class DocumentChunkController extends BaseCurdController<DocumentChunkSer
|
||||
resource = CategoryResourceType.KNOWLEDGE,
|
||||
action = ResourceAction.MANAGE,
|
||||
lookup = ResourceLookup.DOCUMENT_CHUNK_ID,
|
||||
idExpr = "#documentChunk.id",
|
||||
idExpr = "#request.id",
|
||||
denyMessage = "无权限管理知识库"
|
||||
)
|
||||
public Result<?> update(@JsonBody DocumentChunk documentChunk) {
|
||||
boolean success = service.updateById(documentChunk);
|
||||
if (success){
|
||||
DocumentChunk record = documentChunkService.getById(documentChunk.getId());
|
||||
DocumentCollection knowledge = documentCollectionService.getById(record.getDocumentCollectionId());
|
||||
if (knowledge == null) {
|
||||
return Result.fail(1, "知识库不存在");
|
||||
public Result<?> update(
|
||||
@JsonBody(required = true, skipConvertError = false)
|
||||
DocumentChunkContentUpdateRequest request
|
||||
) {
|
||||
DocumentChunk current = documentChunkService.getById(request.getId());
|
||||
if (current == null) {
|
||||
return Result.fail(1, "记录不存在");
|
||||
}
|
||||
DocumentStore documentStore = knowledge.toDocumentStore();
|
||||
if (documentStore == null) {
|
||||
return Result.fail(2, "知识库没有配置向量库");
|
||||
}
|
||||
// 设置向量模型
|
||||
Model model = modelService.getModelInstance(knowledge.getVectorEmbedModelId());
|
||||
if (model == null) {
|
||||
return Result.fail(3, "知识库没有配置向量模型");
|
||||
}
|
||||
EmbeddingModel embeddingModel = model.toEmbeddingModel();
|
||||
documentStore.setEmbeddingModel(embeddingModel);
|
||||
StoreOptions options = StoreOptions.ofCollectionName(knowledge.getVectorStoreCollection());
|
||||
Document document = Document.of(documentChunk.getContent());
|
||||
document.setId(documentChunk.getId());
|
||||
Map<String, Object> metadata = new HashMap<>();
|
||||
metadata.put("keywords", documentChunk.getMetadataKeyWords());
|
||||
metadata.put("questions", documentChunk.getMetadataQuestions());
|
||||
document.setMetadataMap(metadata);
|
||||
StoreResult result = documentStore.update(document, options); // 更新已有记录
|
||||
return Result.ok(result);
|
||||
}
|
||||
return Result.ok(false);
|
||||
DocumentChunk updated = documentChunkService.updateContent(
|
||||
current.getDocumentCollectionId(),
|
||||
current.getId(),
|
||||
request.getContent()
|
||||
);
|
||||
return Result.ok(updated);
|
||||
}
|
||||
|
||||
@PostMapping("removeChunk")
|
||||
@@ -122,32 +112,58 @@ public class DocumentChunkController extends BaseCurdController<DocumentChunkSer
|
||||
idExpr = "#chunkId",
|
||||
denyMessage = "无权限管理知识库"
|
||||
)
|
||||
public Result<?> remove(@JsonBody(value = "id", required = true) BigInteger chunkId) {
|
||||
public Result<?> removeChunk(@JsonBody(value = "id", required = true) BigInteger chunkId) {
|
||||
DocumentChunk docChunk = documentChunkService.getById(chunkId);
|
||||
if (docChunk == null) {
|
||||
return Result.fail(1, "记录不存在");
|
||||
}
|
||||
DocumentCollection knowledge = documentCollectionService.getById(docChunk.getDocumentCollectionId());
|
||||
if (knowledge == null) {
|
||||
return Result.fail(2, "知识库不存在");
|
||||
return Result.ok(documentChunkService.deleteChunk(
|
||||
docChunk.getDocumentCollectionId(),
|
||||
chunkId
|
||||
));
|
||||
}
|
||||
DocumentStore documentStore = knowledge.toDocumentStore();
|
||||
if (documentStore == null) {
|
||||
return Result.fail(3, "知识库没有配置向量库");
|
||||
}
|
||||
// 设置向量模型
|
||||
Model model = modelService.getModelInstance(knowledge.getVectorEmbedModelId());
|
||||
if (model == null) {
|
||||
return Result.fail(4, "知识库没有配置向量模型");
|
||||
}
|
||||
EmbeddingModel embeddingModel = model.toEmbeddingModel();
|
||||
documentStore.setEmbeddingModel(embeddingModel);
|
||||
StoreOptions options = StoreOptions.ofCollectionName(knowledge.getVectorStoreCollection());
|
||||
List<BigInteger> deleteList = new ArrayList<>();
|
||||
deleteList.add(chunkId);
|
||||
documentStore.delete(deleteList, options);
|
||||
documentChunkService.removeChunk(knowledge, chunkId);
|
||||
|
||||
return super.remove(chunkId);
|
||||
@PostMapping("syncStatus")
|
||||
@SaCheckPermission("/api/v1/documentCollection/query")
|
||||
@RequireResourceAccess(
|
||||
resource = CategoryResourceType.KNOWLEDGE,
|
||||
action = ResourceAction.READ,
|
||||
lookup = ResourceLookup.DOCUMENT_ID,
|
||||
idExpr = "#request.documentId",
|
||||
denyMessage = "无权限访问知识库"
|
||||
)
|
||||
public Result<List<DocumentChunkSyncStatus>> syncStatus(
|
||||
@JsonBody(required = true, skipConvertError = false)
|
||||
DocumentChunkSyncStatusRequest request
|
||||
) {
|
||||
return Result.ok(documentChunkService.listIndexSyncStatus(
|
||||
null,
|
||||
request.getDocumentId(),
|
||||
request.getIds()
|
||||
));
|
||||
}
|
||||
|
||||
@PostMapping("retrySync")
|
||||
@SaCheckPermission("/api/v1/documentCollection/save")
|
||||
@RequireResourceAccess(
|
||||
resource = CategoryResourceType.KNOWLEDGE,
|
||||
action = ResourceAction.MANAGE,
|
||||
lookup = ResourceLookup.DOCUMENT_CHUNK_ID,
|
||||
idExpr = "#request.id",
|
||||
denyMessage = "无权限管理知识库"
|
||||
)
|
||||
public Result<?> retrySync(
|
||||
@JsonBody(required = true, skipConvertError = false)
|
||||
DocumentChunkSyncRetryRequest request
|
||||
) {
|
||||
DocumentChunk current = documentChunkService.getById(request.getId());
|
||||
if (current == null || request.getIndexSyncVersion() == null) {
|
||||
return Result.fail(1, "记录不存在或同步版本缺失");
|
||||
}
|
||||
return Result.ok(documentChunkService.retryIndexSync(
|
||||
current.getDocumentCollectionId(),
|
||||
current.getId(),
|
||||
request.getIndexSyncVersion()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,17 +13,21 @@ import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import tech.easyflow.admin.controller.ai.support.AiResourceCreatorNameSupport;
|
||||
import tech.easyflow.agent.entity.AgentKnowledgeBinding;
|
||||
import tech.easyflow.agent.service.AgentKnowledgeBindingService;
|
||||
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.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.DocumentChunkService;
|
||||
import tech.easyflow.ai.service.DocumentCollectionService;
|
||||
import tech.easyflow.ai.service.ModelService;
|
||||
@@ -64,13 +68,17 @@ public class DocumentCollectionController extends BaseCurdController<DocumentCol
|
||||
private final ModelService llmService;
|
||||
|
||||
@Resource
|
||||
private BotDocumentCollectionService botDocumentCollectionService;
|
||||
private AgentKnowledgeBindingService agentKnowledgeBindingService;
|
||||
@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) {
|
||||
super(service);
|
||||
@@ -78,6 +86,16 @@ public class DocumentCollectionController extends BaseCurdController<DocumentCol
|
||||
this.llmService = llmService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取知识库列表关键字搜索字段。
|
||||
*
|
||||
* @return 标题和描述属性
|
||||
*/
|
||||
@Override
|
||||
protected String[] getKeywordSearchProperties() {
|
||||
return new String[]{"title", "description"};
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Result<?> onSaveOrUpdateBefore(DocumentCollection entity, boolean isSave) {
|
||||
normalizeVisibilityScope(entity, isSave);
|
||||
@@ -138,10 +156,14 @@ public class DocumentCollectionController extends BaseCurdController<DocumentCol
|
||||
)
|
||||
public Result<List<KnowledgeSearchResultItem>> search(@RequestParam BigInteger knowledgeId,
|
||||
@RequestParam String keyword,
|
||||
@RequestParam(required = false) String retrievalMode) {
|
||||
@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));
|
||||
@@ -157,11 +179,11 @@ public class DocumentCollectionController extends BaseCurdController<DocumentCol
|
||||
}
|
||||
|
||||
QueryWrapper queryWrapper = QueryWrapper.create();
|
||||
queryWrapper.in(BotDocumentCollection::getDocumentCollectionId, ids);
|
||||
queryWrapper.in(AgentKnowledgeBinding::getKnowledgeId, ids);
|
||||
|
||||
boolean exists = botDocumentCollectionService.exists(queryWrapper);
|
||||
boolean exists = agentKnowledgeBindingService.exists(queryWrapper);
|
||||
if (exists){
|
||||
throw new BusinessException("此知识库还关联着bot,请先取消关联!");
|
||||
throw new BusinessException("此知识库仍被智能体使用,请先取消绑定后再删除");
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -177,6 +199,7 @@ public class DocumentCollectionController extends BaseCurdController<DocumentCol
|
||||
)
|
||||
public Result<DocumentCollection> detail(String id) {
|
||||
DocumentCollection detail = service.getDetail(id);
|
||||
aiResourceApprovalStateService.fillKnowledgeApprovalState(detail);
|
||||
return Result.ok(detail);
|
||||
}
|
||||
|
||||
@@ -190,12 +213,67 @@ public class DocumentCollectionController extends BaseCurdController<DocumentCol
|
||||
* 提交发布审批。
|
||||
*
|
||||
* @param id 知识库 ID
|
||||
* @param applicationReason 审批说明
|
||||
* @return 审批实例 ID
|
||||
*/
|
||||
@PostMapping("/submitPublishApproval")
|
||||
@SaCheckPermission("/api/v1/documentCollection/save")
|
||||
public Result<BigInteger> submitPublishApproval(@JsonBody("id") BigInteger id) {
|
||||
return Result.ok(knowledgePublishAppService.submitPublishApproval(id));
|
||||
public Result<BigInteger> submitPublishApproval(
|
||||
@JsonBody("id") BigInteger id,
|
||||
@JsonBody("applicationReason") String applicationReason
|
||||
) {
|
||||
return buildApprovalActionResult(
|
||||
knowledgePublishAppService.submitPublishApproval(id, applicationReason),
|
||||
"已提交发布审批",
|
||||
"已直接发布"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 预检知识库发布是否命中审批流。
|
||||
*
|
||||
* @param id 知识库 ID
|
||||
* @return 是否需要审批
|
||||
*/
|
||||
@GetMapping("/publishApprovalRequirement")
|
||||
@SaCheckPermission("/api/v1/documentCollection/save")
|
||||
public Result<Boolean> publishApprovalRequirement(@RequestParam BigInteger id) {
|
||||
return Result.ok(knowledgePublishAppService.isPublishApprovalRequired(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));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -207,7 +285,11 @@ public class DocumentCollectionController extends BaseCurdController<DocumentCol
|
||||
@PostMapping("/submitDeleteApproval")
|
||||
@SaCheckPermission("/api/v1/documentCollection/remove")
|
||||
public Result<BigInteger> submitDeleteApproval(@JsonBody("id") BigInteger id) {
|
||||
return Result.ok(knowledgePublishAppService.submitDeleteApproval(id));
|
||||
return buildApprovalActionResult(
|
||||
knowledgePublishAppService.submitDeleteApproval(id),
|
||||
"已提交删除审批",
|
||||
"已直接删除"
|
||||
);
|
||||
}
|
||||
|
||||
@PostMapping("splitterProfile/save")
|
||||
@@ -250,20 +332,34 @@ public class DocumentCollectionController extends BaseCurdController<DocumentCol
|
||||
knowledgeVisibilityQueryHelper.applyReadableAccess(queryWrapper);
|
||||
applyPublishedOnlyFilter(queryWrapper);
|
||||
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||
return Result.ok(service.list(queryWrapper));
|
||||
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);
|
||||
return super.queryPage(page, 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, "请提交删除审批");
|
||||
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) {
|
||||
@@ -337,6 +433,8 @@ public class DocumentCollectionController extends BaseCurdController<DocumentCol
|
||||
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)));
|
||||
|
||||
@@ -8,9 +8,15 @@ import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
import tech.easyflow.ai.documentimport.DocumentImportBatchDtos;
|
||||
import tech.easyflow.ai.documentimport.DocumentImportDtos;
|
||||
import tech.easyflow.ai.documentimport.task.DocumentImportBatchAppService;
|
||||
import tech.easyflow.ai.documentimport.task.DocumentImportTaskStatusStreamService;
|
||||
import tech.easyflow.ai.entity.Document;
|
||||
import tech.easyflow.ai.entity.DocumentCollection;
|
||||
import tech.easyflow.ai.entity.DocumentCollectionSplitParams;
|
||||
@@ -77,6 +83,12 @@ public class DocumentController extends BaseCurdController<DocumentService, Docu
|
||||
@Autowired
|
||||
private ResourceAccessService resourceAccessService;
|
||||
|
||||
@Autowired
|
||||
private DocumentImportTaskStatusStreamService documentImportTaskStatusStreamService;
|
||||
|
||||
@Autowired
|
||||
private DocumentImportBatchAppService documentImportBatchAppService;
|
||||
|
||||
@Value("${easyflow.storage.local.root:}")
|
||||
private String fileUploadPath;
|
||||
|
||||
@@ -87,6 +99,16 @@ public class DocumentController extends BaseCurdController<DocumentService, Docu
|
||||
super(service);
|
||||
this.knowledgeService = knowledgeService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取知识库文档关键字搜索字段。
|
||||
*
|
||||
* @return 文件标题属性
|
||||
*/
|
||||
@Override
|
||||
protected String[] getKeywordSearchProperties() {
|
||||
return new String[]{"title"};
|
||||
}
|
||||
@PostMapping("removeDoc")
|
||||
@Transactional
|
||||
@SaCheckPermission("/api/v1/documentCollection/remove")
|
||||
@@ -96,12 +118,10 @@ public class DocumentController extends BaseCurdController<DocumentService, Docu
|
||||
List<Serializable> ids = Collections.singletonList(id);
|
||||
Result<?> result = onRemoveBefore(ids);
|
||||
if (result != null) return result;
|
||||
boolean isSuccess = documentService.removeDoc(id);
|
||||
if (!isSuccess){
|
||||
return Result.ok(false);
|
||||
}
|
||||
boolean success = service.removeById(id);
|
||||
boolean success = documentService.removeDoc(id);
|
||||
if (success) {
|
||||
onRemoveAfter(ids);
|
||||
}
|
||||
return Result.ok(success);
|
||||
}
|
||||
|
||||
@@ -136,13 +156,19 @@ public class DocumentController extends BaseCurdController<DocumentService, Docu
|
||||
|
||||
@GetMapping("documentList")
|
||||
@SaCheckPermission("/api/v1/documentCollection/query")
|
||||
public Result<Page<Document>> documentList(@RequestParam(name="title", required = false) String fileName, @RequestParam(name="pageSize") int pageSize, @RequestParam(name = "pageNumber") int pageNumber) {
|
||||
public Result<Page<Document>> documentList(
|
||||
@RequestParam(name = "keyword", required = false) String keyword,
|
||||
@RequestParam(name = "title", required = false) String legacyTitle,
|
||||
@RequestParam(name = "pageSize") int pageSize,
|
||||
@RequestParam(name = "pageNumber") int pageNumber) {
|
||||
String kbSlug = RequestUtil.getParamAsString("id");
|
||||
if (StringUtil.noText(kbSlug)) {
|
||||
throw new BusinessException("知识库id不能为空");
|
||||
}
|
||||
DocumentCollection knowledge = getDocumentCollection(kbSlug, ResourceAction.READ, "无权限访问知识库");
|
||||
Page<Document> documentList = documentService.getDocumentList(knowledge.getId().toString(), pageSize, pageNumber,fileName);
|
||||
String effectiveKeyword = StringUtil.hasText(keyword) ? keyword : legacyTitle;
|
||||
Page<Document> documentList = documentService.getDocumentList(
|
||||
knowledge.getId().toString(), pageSize, pageNumber, effectiveKeyword);
|
||||
return Result.ok(documentList);
|
||||
}
|
||||
|
||||
@@ -233,6 +259,215 @@ public class DocumentController extends BaseCurdController<DocumentService, Docu
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建文档批量上传清单。
|
||||
*
|
||||
* @param request 文件清单
|
||||
* @return 批次与服务端文件项
|
||||
*/
|
||||
@PostMapping("import/batch/create")
|
||||
@SaCheckPermission("/api/v1/documentCollection/save")
|
||||
public Result<DocumentImportBatchDtos.CreateResponse> createImportBatch(
|
||||
@JsonBody DocumentImportBatchDtos.CreateRequest request) {
|
||||
if (request == null || request.getKnowledgeId() == null) {
|
||||
throw new BusinessException("知识库id不能为空");
|
||||
}
|
||||
getDocumentCollection(request.getKnowledgeId().toString(), ResourceAction.MANAGE, "无权限管理知识库");
|
||||
return Result.ok(documentImportBatchAppService.createBatch(request));
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传一个批次文件。
|
||||
*
|
||||
* @param batchId 批次 ID
|
||||
* @param itemId 文件项 ID
|
||||
* @param knowledgeId 知识库 ID
|
||||
* @param file 上传文件
|
||||
* @return 文件项状态
|
||||
*/
|
||||
@PostMapping(value = "import/batch/{batchId}/item/{itemId}/upload",
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@SaCheckPermission("/api/v1/documentCollection/save")
|
||||
public Result<DocumentImportBatchDtos.ItemResponse> uploadImportBatchItem(
|
||||
@PathVariable BigInteger batchId,
|
||||
@PathVariable BigInteger itemId,
|
||||
@RequestParam BigInteger knowledgeId,
|
||||
@RequestPart("file") MultipartFile file) {
|
||||
getDocumentCollection(knowledgeId.toString(), ResourceAction.MANAGE, "无权限管理知识库");
|
||||
return Result.ok(documentImportBatchAppService.uploadItem(
|
||||
knowledgeId, batchId, itemId, file));
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动手动或自动批量导入。
|
||||
*
|
||||
* @param request 启动请求
|
||||
* @return 批次状态
|
||||
*/
|
||||
@PostMapping("import/batch/start")
|
||||
@SaCheckPermission("/api/v1/documentCollection/save")
|
||||
public Result<DocumentImportBatchDtos.StatusResponse> startImportBatch(
|
||||
@JsonBody DocumentImportBatchDtos.StartRequest request) {
|
||||
if (request == null || request.getKnowledgeId() == null) {
|
||||
throw new BusinessException("知识库id不能为空");
|
||||
}
|
||||
getDocumentCollection(request.getKnowledgeId().toString(), ResourceAction.MANAGE, "无权限管理知识库");
|
||||
return Result.ok(documentImportBatchAppService.startBatch(request));
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消一个尚未启动的上传批次。
|
||||
*
|
||||
* @param knowledgeId 知识库 ID
|
||||
* @param batchId 批次 ID
|
||||
* @return 空结果
|
||||
*/
|
||||
@PostMapping("import/batch/cancel")
|
||||
@SaCheckPermission("/api/v1/documentCollection/save")
|
||||
public Result<Void> cancelImportBatch(
|
||||
@JsonBody(value = "knowledgeId", required = true) BigInteger knowledgeId,
|
||||
@JsonBody(value = "batchId", required = true) BigInteger batchId) {
|
||||
getDocumentCollection(knowledgeId.toString(), ResourceAction.MANAGE, "无权限管理知识库");
|
||||
documentImportBatchAppService.cancelBatch(knowledgeId, batchId);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询批次状态。
|
||||
*
|
||||
* @param knowledgeId 知识库 ID
|
||||
* @param batchId 批次 ID
|
||||
* @return 批次状态
|
||||
*/
|
||||
@GetMapping("import/batch/status")
|
||||
@SaCheckPermission("/api/v1/documentCollection/query")
|
||||
public Result<DocumentImportBatchDtos.StatusResponse> getImportBatchStatus(
|
||||
@RequestParam BigInteger knowledgeId,
|
||||
@RequestParam BigInteger batchId) {
|
||||
getDocumentCollection(knowledgeId.toString(), ResourceAction.READ, "无权限访问知识库");
|
||||
return Result.ok(documentImportBatchAppService.getBatchStatus(knowledgeId, batchId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询知识库最近一个自动导入批次。
|
||||
*
|
||||
* @param knowledgeId 知识库 ID
|
||||
* @return 最近批次状态
|
||||
*/
|
||||
@GetMapping("import/batch/current")
|
||||
@SaCheckPermission("/api/v1/documentCollection/query")
|
||||
public Result<DocumentImportBatchDtos.StatusResponse> getCurrentImportBatch(
|
||||
@RequestParam BigInteger knowledgeId) {
|
||||
getDocumentCollection(knowledgeId.toString(), ResourceAction.READ, "无权限访问知识库");
|
||||
return Result.ok(documentImportBatchAppService.getLatestAutoBatch(knowledgeId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 继续中断或部分失败的自动导入批次。
|
||||
*
|
||||
* @param knowledgeId 知识库 ID
|
||||
* @param batchId 批次 ID
|
||||
* @return 继续后的批次状态
|
||||
*/
|
||||
@PostMapping("import/batch/continue")
|
||||
@SaCheckPermission("/api/v1/documentCollection/save")
|
||||
public Result<DocumentImportBatchDtos.StatusResponse> continueImportBatch(
|
||||
@JsonBody(value = "knowledgeId", required = true) BigInteger knowledgeId,
|
||||
@JsonBody(value = "batchId", required = true) BigInteger batchId) {
|
||||
getDocumentCollection(knowledgeId.toString(), ResourceAction.MANAGE, "无权限管理知识库");
|
||||
return Result.ok(documentImportBatchAppService.continueBatch(knowledgeId, batchId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据解析、分块或向量化失败阶段统一重试。
|
||||
*
|
||||
* @param request 重试请求
|
||||
* @return 重试任务状态
|
||||
*/
|
||||
@PostMapping("import/task/retry")
|
||||
@SaCheckPermission("/api/v1/documentCollection/save")
|
||||
public Result<DocumentImportDtos.TaskStartIndexResponse> retryImportTask(
|
||||
@JsonBody DocumentImportDtos.TaskRetryRequest request) {
|
||||
if (request == null || request.getKnowledgeId() == null || request.getDocumentId() == null) {
|
||||
throw new BusinessException("重试信息不完整");
|
||||
}
|
||||
getDocumentCollection(request.getKnowledgeId().toString(), ResourceAction.MANAGE, "无权限管理知识库");
|
||||
return documentService.retryFailedTask(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 entity
|
||||
*
|
||||
|
||||
@@ -2,6 +2,7 @@ package tech.easyflow.admin.controller.ai;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import com.mybatisflex.core.paginate.Page;
|
||||
import com.mybatisflex.core.query.QueryColumn;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
@@ -47,7 +48,7 @@ import java.util.Set;
|
||||
@UsePermission(moduleName = "/api/v1/documentCollection")
|
||||
public class FaqItemController extends BaseCurdController<FaqItemService, FaqItem> {
|
||||
|
||||
private static final long MAX_IMAGE_SIZE_BYTES = 5L * 1024L * 1024L;
|
||||
private static final long MAX_IMAGE_SIZE_BYTES = 20L * 1024L * 1024L;
|
||||
private static final Set<String> ALLOWED_IMAGE_TYPES = new HashSet<>(Arrays.asList(
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
@@ -114,9 +115,14 @@ public class FaqItemController extends BaseCurdController<FaqItemService, FaqIte
|
||||
QueryWrapper queryWrapper = QueryWrapper.create()
|
||||
.eq(FaqItem::getCollectionId, collectionId);
|
||||
|
||||
String question = request.getParameter("question");
|
||||
if (question != null && !question.trim().isEmpty()) {
|
||||
queryWrapper.like(FaqItem::getQuestion, question.trim());
|
||||
String keyword = normalizeSearchKeyword(request.getParameter("keyword"));
|
||||
String question = normalizeSearchKeyword(request.getParameter("question"));
|
||||
if (StringUtils.hasText(keyword)) {
|
||||
queryWrapper.and(buildLiteralContainsCondition(
|
||||
keyword, new QueryColumn("question"), new QueryColumn("answer_text")));
|
||||
} else if (StringUtils.hasText(question)) {
|
||||
// 兼容旧客户端仅按问题字段搜索。
|
||||
queryWrapper.and(buildLiteralContainsCondition(question, new QueryColumn("question")));
|
||||
}
|
||||
|
||||
String categoryIdText = request.getParameter("categoryId");
|
||||
@@ -215,7 +221,7 @@ public class FaqItemController extends BaseCurdController<FaqItemService, FaqIte
|
||||
throw new BusinessException("图片不能为空");
|
||||
}
|
||||
if (file.getSize() > MAX_IMAGE_SIZE_BYTES) {
|
||||
throw new BusinessException("图片大小不能超过5MB");
|
||||
throw new BusinessException("图片大小不能超过20MB");
|
||||
}
|
||||
if (!isAllowedImageType(file)) {
|
||||
throw new BusinessException("仅支持 JPG/PNG/WEBP/GIF 图片");
|
||||
|
||||
@@ -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;
|
||||
|
||||
import com.easyagents.mcp.client.McpEnvironmentCheckResult;
|
||||
import com.mybatisflex.core.paginate.Page;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
@@ -8,16 +9,19 @@ import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import tech.easyflow.ai.entity.BotMcp;
|
||||
import tech.easyflow.ai.entity.Mcp;
|
||||
import tech.easyflow.ai.service.BotMcpService;
|
||||
import tech.easyflow.ai.service.AgentResourceReferenceService;
|
||||
import tech.easyflow.ai.service.McpService;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigInteger;
|
||||
|
||||
/**
|
||||
* 控制层。
|
||||
@@ -32,8 +36,18 @@ public class McpController extends BaseCurdController<McpService, Mcp> {
|
||||
super(service);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 MCP 列表关键字搜索字段。
|
||||
*
|
||||
* @return 标题和描述属性
|
||||
*/
|
||||
@Override
|
||||
protected String[] getKeywordSearchProperties() {
|
||||
return new String[]{"title", "description"};
|
||||
}
|
||||
|
||||
@Resource
|
||||
private BotMcpService botMcpService;
|
||||
private AgentResourceReferenceService agentResourceReferenceService;
|
||||
@Override
|
||||
public Result<?> save(Mcp entity) {
|
||||
return service.saveMcp(entity);
|
||||
@@ -44,11 +58,29 @@ public class McpController extends BaseCurdController<McpService, Mcp> {
|
||||
return service.updateMcp(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除未被 Agent 绑定的 MCP。
|
||||
*
|
||||
* @param id MCP ID
|
||||
* @return 删除结果
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Result<?> remove(Serializable id) {
|
||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||
if (account == null || account.getTenantId() == null) {
|
||||
throw new BusinessException("当前登录状态失效,请重新登录后再试");
|
||||
}
|
||||
// 锁定 MCP 资源行,与 Agent 绑定校验串行,避免检查后并发写入绑定。
|
||||
Mcp mcp = service.getOne(QueryWrapper.create()
|
||||
.eq(Mcp::getId, id)
|
||||
.eq(Mcp::getTenantId, account.getTenantId())
|
||||
.forUpdate());
|
||||
if (mcp == null) {
|
||||
throw new BusinessException("MCP 不存在或无权删除");
|
||||
}
|
||||
agentResourceReferenceService.assertMcpUnused(new BigInteger(String.valueOf(id)));
|
||||
service.removeMcp(id);
|
||||
botMcpService.remove(QueryWrapper.create().eq(BotMcp::getMcpId, id));
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@@ -64,6 +96,11 @@ public class McpController extends BaseCurdController<McpService, Mcp> {
|
||||
return Result.ok(service.getMcpTools(id));
|
||||
}
|
||||
|
||||
@PostMapping("/check")
|
||||
public Result<McpEnvironmentCheckResult> check(@JsonBody("configJson") String configJson) {
|
||||
return Result.ok(service.checkMcp(configJson));
|
||||
}
|
||||
|
||||
|
||||
@GetMapping("pageTools")
|
||||
public Result<Page<Mcp>> pageTools(HttpServletRequest request, String sortKey, String sortType, Long pageNumber, Long pageSize) {
|
||||
|
||||
@@ -9,15 +9,21 @@ import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import tech.easyflow.ai.dto.ModelInvokeConfigDtos;
|
||||
import tech.easyflow.ai.entity.Model;
|
||||
import tech.easyflow.ai.entity.ModelProvider;
|
||||
import tech.easyflow.ai.entity.table.ModelTableDef;
|
||||
import tech.easyflow.ai.mapper.ModelMapper;
|
||||
import tech.easyflow.ai.service.AgentResourceReferenceService;
|
||||
import tech.easyflow.ai.service.ModelService;
|
||||
import tech.easyflow.admin.model.ai.ModelGatewayConfigView;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.system.entity.SysOption;
|
||||
import tech.easyflow.system.service.SysOptionService;
|
||||
import tech.easyflow.ai.service.capability.ModelCapabilityResolution;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.tree.Tree;
|
||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
@@ -26,7 +32,6 @@ import java.math.BigInteger;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
@@ -39,6 +44,11 @@ import java.util.stream.Collectors;
|
||||
@RequestMapping("/api/v1/model")
|
||||
public class ModelController extends BaseCurdController<ModelService, Model> {
|
||||
|
||||
private static final String CHAT_PUBLISH_BASE_URL = "chat_publish_base_url";
|
||||
|
||||
@Autowired
|
||||
private SysOptionService sysOptionService;
|
||||
|
||||
public ModelController(ModelService service) {
|
||||
super(service);
|
||||
}
|
||||
@@ -46,22 +56,30 @@ public class ModelController extends BaseCurdController<ModelService, Model> {
|
||||
@Autowired
|
||||
ModelService modelService;
|
||||
|
||||
/**
|
||||
* 查询模型统一网关页面所需的安全配置。
|
||||
*
|
||||
* @return 仅包含模型发布基础地址的配置
|
||||
*/
|
||||
@GetMapping("/gatewayConfig")
|
||||
@SaCheckPermission("/api/v1/model/query")
|
||||
public Result<ModelGatewayConfigView> gatewayConfig() {
|
||||
SysOption option = sysOptionService.getByOptionKey(
|
||||
CHAT_PUBLISH_BASE_URL,
|
||||
SaTokenUtil.getLoginAccount().getTenantId()
|
||||
);
|
||||
return Result.ok(new ModelGatewayConfigView(option == null ? null : option.getValue()));
|
||||
}
|
||||
|
||||
@Resource
|
||||
ModelMapper modelMapper;
|
||||
@Resource
|
||||
AgentResourceReferenceService agentResourceReferenceService;
|
||||
|
||||
@GetMapping("list")
|
||||
@SaCheckPermission("/api/v1/model/query")
|
||||
public Result<List<Model>> list(Model entity, Boolean asTree, String sortKey, String sortType) {
|
||||
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
|
||||
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||
List<Model> list = Tree.tryToTree(modelMapper.selectListWithRelationsByQuery(queryWrapper), asTree);
|
||||
list.forEach(item -> {
|
||||
String providerName = Optional.ofNullable(item.getModelProvider())
|
||||
.map(ModelProvider::getProviderName)
|
||||
.orElse("-");
|
||||
item.setTitle(providerName + "/" + item.getTitle());
|
||||
});
|
||||
return Result.ok(list);
|
||||
return Result.ok(service.listSelectableModels(entity, asTree, sortKey, sortType));
|
||||
}
|
||||
|
||||
@GetMapping("getList")
|
||||
@@ -92,9 +110,39 @@ public class ModelController extends BaseCurdController<ModelService, Model> {
|
||||
return Result.ok(modelService.verifyModelConfig(model));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据模型 ID 返回自动识别的类型和能力。
|
||||
*
|
||||
* @param providerId 供应商 ID
|
||||
* @param modelName 模型 ID
|
||||
* @return 模型能力识别结果
|
||||
*/
|
||||
@GetMapping("capabilities")
|
||||
@SaCheckPermission("/api/v1/model/query")
|
||||
public Result<ModelCapabilityResolution> resolveCapabilities(
|
||||
@RequestParam(required = false) BigInteger providerId,
|
||||
@RequestParam String modelName) {
|
||||
return Result.ok(modelService.resolveModelCapabilities(providerId, modelName));
|
||||
}
|
||||
|
||||
@PostMapping("/removeByEntity")
|
||||
@SaCheckPermission("/api/v1/model/remove")
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Result<?> removeByEntity(@RequestBody Model entity) {
|
||||
LoginAccount account = requireAccount();
|
||||
List<Model> models = service.list(QueryWrapper.create()
|
||||
.select(Model::getId)
|
||||
.eq(Model::getProviderId, entity.getProviderId())
|
||||
.eq(Model::getGroupName, entity.getGroupName())
|
||||
.eq(Model::getTenantId, account.getTenantId())
|
||||
.orderBy(Model::getId, true)
|
||||
.forUpdate());
|
||||
if (models.isEmpty()) {
|
||||
throw new BusinessException("模型不存在或无权删除");
|
||||
}
|
||||
agentResourceReferenceService.assertModelsUnused(
|
||||
models.stream().map(Model::getId).toList());
|
||||
entity.setTenantId(account.getTenantId());
|
||||
modelService.removeByEntity(entity);
|
||||
return Result.ok();
|
||||
}
|
||||
@@ -145,8 +193,10 @@ public class ModelController extends BaseCurdController<ModelService, Model> {
|
||||
QueryWrapper queryWrapper = QueryWrapper.create();
|
||||
queryWrapper.eq(Model::getProviderId, providerId);
|
||||
queryWrapper.eq(Model::getModelType, modelType);
|
||||
if (StringUtils.hasLength(selectText)) {
|
||||
queryWrapper.and(ModelTableDef.MODEL.TITLE.like(selectText).or(ModelTableDef.MODEL.MODEL_NAME.like(selectText)));
|
||||
String keyword = normalizeSearchKeyword(selectText);
|
||||
if (StringUtils.hasText(keyword)) {
|
||||
queryWrapper.and(buildLiteralContainsCondition(
|
||||
keyword, ModelTableDef.MODEL.TITLE, ModelTableDef.MODEL.MODEL_NAME));
|
||||
}
|
||||
List<Model> totalList = service.getMapper().selectListWithRelationsByQuery(queryWrapper);
|
||||
Map<String, List<Model>> groupList = totalList.stream().collect(Collectors.groupingBy(Model::getGroupName));
|
||||
@@ -167,11 +217,39 @@ public class ModelController extends BaseCurdController<ModelService, Model> {
|
||||
}
|
||||
|
||||
@PostMapping("removeLlmByIds")
|
||||
@Transactional
|
||||
@SaCheckPermission("/api/v1/model/remove")
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Result<?> removeLlm(@JsonBody(value = "id", required = true) Serializable id) {
|
||||
LoginAccount account = requireAccount();
|
||||
List<Serializable> ids = Collections.singletonList(id);
|
||||
QueryWrapper queryWrapper = QueryWrapper.create().in(Model::getId, ids);
|
||||
service.remove(queryWrapper);
|
||||
QueryWrapper queryWrapper = QueryWrapper.create()
|
||||
.in(Model::getId, ids)
|
||||
.eq(Model::getTenantId, account.getTenantId())
|
||||
.orderBy(Model::getId, true)
|
||||
.forUpdate();
|
||||
List<Model> models = service.list(queryWrapper);
|
||||
if (models.isEmpty()) {
|
||||
throw new BusinessException("模型不存在或无权删除");
|
||||
}
|
||||
agentResourceReferenceService.assertModelsUnused(
|
||||
models.stream().map(Model::getId).toList());
|
||||
service.remove(QueryWrapper.create()
|
||||
.in(Model::getId, ids)
|
||||
.eq(Model::getTenantId, account.getTenantId()));
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取带租户信息的当前登录账号。
|
||||
*
|
||||
* @return 当前登录账号
|
||||
* @throws BusinessException 登录状态无效时抛出
|
||||
*/
|
||||
private LoginAccount requireAccount() {
|
||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||
if (account == null || account.getTenantId() == null) {
|
||||
throw new BusinessException("当前登录状态失效,请重新登录后再试");
|
||||
}
|
||||
return account;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,33 @@
|
||||
package tech.easyflow.admin.controller.ai;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import tech.easyflow.ai.dto.RemoteModelImportRequest;
|
||||
import tech.easyflow.ai.entity.Model;
|
||||
import tech.easyflow.ai.entity.ModelProvider;
|
||||
import tech.easyflow.ai.service.ModelProviderService;
|
||||
import tech.easyflow.ai.service.ModelService;
|
||||
import tech.easyflow.ai.service.discovery.RemoteModelDiscoveryService;
|
||||
import tech.easyflow.ai.service.discovery.RemoteModelImportResult;
|
||||
import tech.easyflow.ai.service.discovery.RemoteModelImportService;
|
||||
import tech.easyflow.ai.service.discovery.RemoteModelListResult;
|
||||
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.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigInteger;
|
||||
|
||||
/**
|
||||
* 控制层。
|
||||
@@ -28,12 +40,33 @@ import java.io.Serializable;
|
||||
@UsePermission(moduleName = "/api/v1/model")
|
||||
public class ModelProviderController extends BaseCurdController<ModelProviderService, ModelProvider> {
|
||||
private final ModelService modelService;
|
||||
private final RemoteModelDiscoveryService remoteModelDiscoveryService;
|
||||
private final RemoteModelImportService remoteModelImportService;
|
||||
|
||||
public ModelProviderController(ModelProviderService service, ModelService modelService) {
|
||||
/**
|
||||
* 创建模型服务商控制器。
|
||||
*
|
||||
* @param service 模型服务商服务
|
||||
* @param modelService 模型服务
|
||||
* @param remoteModelDiscoveryService 远端模型发现服务
|
||||
* @param remoteModelImportService 远端模型一键添加服务
|
||||
*/
|
||||
public ModelProviderController(ModelProviderService service,
|
||||
ModelService modelService,
|
||||
RemoteModelDiscoveryService remoteModelDiscoveryService,
|
||||
RemoteModelImportService remoteModelImportService) {
|
||||
super(service);
|
||||
this.modelService = modelService;
|
||||
this.remoteModelDiscoveryService = remoteModelDiscoveryService;
|
||||
this.remoteModelImportService = remoteModelImportService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除没有子模型的服务商。
|
||||
*
|
||||
* @param id 服务商 ID
|
||||
* @return 删除结果
|
||||
*/
|
||||
@Override
|
||||
@PostMapping("remove")
|
||||
@Transactional
|
||||
@@ -45,4 +78,35 @@ public class ModelProviderController extends BaseCurdController<ModelProviderSer
|
||||
}
|
||||
return Result.ok(service.removeById(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 主动获取当前服务商的远端模型列表。
|
||||
*
|
||||
* @param providerId 服务商 ID
|
||||
* @return 已补全能力和本地添加状态的远端模型列表
|
||||
*/
|
||||
@GetMapping("{providerId}/remoteModels")
|
||||
@SaCheckPermission("/api/v1/model/save")
|
||||
public Result<RemoteModelListResult> remoteModels(@PathVariable BigInteger providerId) {
|
||||
return Result.ok(remoteModelDiscoveryService.discover(providerId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 幂等添加单个远端模型。
|
||||
*
|
||||
* @param providerId 服务商 ID
|
||||
* @param request 一键添加请求
|
||||
* @return 创建或已存在结果
|
||||
*/
|
||||
@PostMapping("{providerId}/remoteModels/import")
|
||||
@SaCheckPermission("/api/v1/model/save")
|
||||
public Result<RemoteModelImportResult> importRemoteModel(
|
||||
@PathVariable BigInteger providerId,
|
||||
@RequestBody RemoteModelImportRequest request) {
|
||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||
Model model = new Model();
|
||||
commonFiled(model, account.getId(), account.getTenantId(), account.getDeptId());
|
||||
String modelId = request == null ? null : request.getModelId();
|
||||
return Result.ok(remoteModelImportService.importModel(providerId, modelId, model));
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
package tech.easyflow.admin.controller.ai;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import tech.easyflow.ai.entity.PluginCategory;
|
||||
import tech.easyflow.ai.entity.PluginCategoryMapping;
|
||||
import tech.easyflow.ai.service.PluginCategoryMappingService;
|
||||
import tech.easyflow.common.annotation.UsePermission;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||
@@ -21,6 +23,7 @@ import java.util.List;
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/pluginCategoryMapping")
|
||||
@UsePermission(moduleName = "/api/v1/plugin")
|
||||
public class PluginCategoryMappingController extends BaseCurdController<PluginCategoryMappingService, PluginCategoryMapping> {
|
||||
public PluginCategoryMappingController(PluginCategoryMappingService service) {
|
||||
super(service);
|
||||
@@ -30,6 +33,7 @@ public class PluginCategoryMappingController extends BaseCurdController<PluginCa
|
||||
private PluginCategoryMappingService relationService;
|
||||
|
||||
@PostMapping("/updateRelation")
|
||||
@SaCheckPermission("/api/v1/plugin/save")
|
||||
public Result<Boolean> updateRelation(
|
||||
@JsonBody(value="pluginId") BigInteger pluginId,
|
||||
@JsonBody(value="categoryIds") ArrayList<BigInteger> categoryIds
|
||||
|
||||
@@ -2,25 +2,39 @@ package tech.easyflow.admin.controller.ai;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import com.mybatisflex.core.paginate.Page;
|
||||
import com.mybatisflex.core.query.QueryColumn;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import tech.easyflow.admin.controller.ai.support.AiResourceCreatorNameSupport;
|
||||
import tech.easyflow.ai.entity.Model;
|
||||
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.PluginItemService;
|
||||
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.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||
import tech.easyflow.ai.service.PluginService;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||
import 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 java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
@@ -35,8 +49,24 @@ import static tech.easyflow.ai.entity.table.PluginTableDef.PLUGIN;
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/plugin")
|
||||
public class PluginController extends BaseCurdController<PluginService, Plugin> {
|
||||
/**
|
||||
* 创建插件控制器。
|
||||
*
|
||||
* @param service 插件服务
|
||||
*/
|
||||
public PluginController(PluginService service) {
|
||||
super(service);
|
||||
this.pluginService = service;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取插件列表关键字搜索字段。
|
||||
*
|
||||
* @return 插件名称和描述属性
|
||||
*/
|
||||
@Override
|
||||
protected String[] getKeywordSearchProperties() {
|
||||
return new String[]{"name", "description"};
|
||||
}
|
||||
|
||||
@Resource
|
||||
@@ -47,6 +77,16 @@ public class PluginController extends BaseCurdController<PluginService, Plugin>
|
||||
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
|
||||
protected Result<?> onSaveOrUpdateBefore(Plugin entity, boolean isSave) {
|
||||
@@ -67,10 +107,15 @@ public class PluginController extends BaseCurdController<PluginService, Plugin>
|
||||
return Result.ok(pluginService.updatePlugin(plugin));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除插件。
|
||||
*
|
||||
* @param id 插件 ID
|
||||
* @return 删除结果
|
||||
*/
|
||||
@PostMapping("/plugin/remove")
|
||||
@SaCheckPermission("/api/v1/plugin/remove")
|
||||
public Result<Boolean> removePlugin(@JsonBody(value = "id", required = true) String id){
|
||||
|
||||
return Result.ok(pluginService.removePlugin(id));
|
||||
}
|
||||
|
||||
@@ -79,12 +124,28 @@ public class PluginController extends BaseCurdController<PluginService, Plugin>
|
||||
public Result<List<Plugin>> getList(){
|
||||
QueryWrapper queryWrapper = QueryWrapper.create().select();
|
||||
applyCategoryPermission(queryWrapper);
|
||||
return Result.ok(service.getMapper().selectListByQuery(queryWrapper));
|
||||
List<Plugin> plugins = service.getMapper().selectListWithRelationsByQuery(queryWrapper);
|
||||
return Result.ok(pluginService.preparePluginsForCurrentUser(plugins, true, false));
|
||||
}
|
||||
|
||||
/**
|
||||
* 按分类分页查询插件,并支持按名称、描述模糊查询。
|
||||
*
|
||||
* @param request 当前请求
|
||||
* @param sortKey 排序字段
|
||||
* @param sortType 排序方向
|
||||
* @param pageNumber 页码
|
||||
* @param pageSize 每页数量
|
||||
* @param category 分类 ID,0 表示全部分类
|
||||
* @param keyword 插件名称或描述关键字
|
||||
* @param name 兼容旧客户端的插件名称关键字
|
||||
* @return 插件分页结果
|
||||
*/
|
||||
@GetMapping("/pageByCategory")
|
||||
@SaCheckPermission("/api/v1/plugin/query")
|
||||
public Result<Page<Plugin>> pageByCategory(HttpServletRequest request, String sortKey, String sortType, Long pageNumber, Long pageSize, int category) {
|
||||
public Result<Page<Plugin>> pageByCategory(HttpServletRequest request, String sortKey, String sortType,
|
||||
Long pageNumber, Long pageSize, int category,
|
||||
String keyword, String name) {
|
||||
if (pageNumber == null || pageNumber < 1) {
|
||||
pageNumber = 1L;
|
||||
}
|
||||
@@ -97,8 +158,32 @@ public class PluginController extends BaseCurdController<PluginService, Plugin>
|
||||
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||
return Result.ok(queryPage(new Page<>(pageNumber, pageSize), queryWrapper));
|
||||
} else {
|
||||
return pluginService.pageByCategory(pageNumber, pageSize, category);
|
||||
String effectiveKeyword = normalizeSearchKeyword(
|
||||
keyword == null || keyword.isBlank() ? name : keyword);
|
||||
Result<Page<Plugin>> result = pluginService.pageByCategory(
|
||||
pageNumber, pageSize, category, effectiveKeyword);
|
||||
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.and(buildLiteralContainsCondition(keyword, new QueryColumn("title")));
|
||||
}
|
||||
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")
|
||||
@@ -110,14 +195,25 @@ public class PluginController extends BaseCurdController<PluginService, Plugin>
|
||||
@Override
|
||||
protected Page<Plugin> queryPage(Page<Plugin> page, QueryWrapper queryWrapper) {
|
||||
applyCategoryPermission(queryWrapper);
|
||||
return service.getMapper().paginateWithRelations(page, 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.getById(id);
|
||||
Plugin plugin = service.getMapper().selectOneWithRelationsById(id);
|
||||
if (plugin != null) {
|
||||
pluginVisibilityService.assertPluginVisible(plugin.getCreatedBy(), plugin.getId(), "无权限访问插件");
|
||||
pluginService.preparePluginForCurrentUser(plugin);
|
||||
}
|
||||
return Result.ok(plugin);
|
||||
}
|
||||
@@ -138,4 +234,30 @@ public class PluginController extends BaseCurdController<PluginService, Plugin>
|
||||
}
|
||||
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,55 @@
|
||||
package tech.easyflow.admin.controller.ai;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.easyagents.flow.core.chain.runtime.ChainExecutor;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import tech.easyflow.ai.entity.BotPlugin;
|
||||
import tech.easyflow.ai.easyagentsflow.entity.ChainInfo;
|
||||
import tech.easyflow.ai.easyagentsflow.entity.NodeInfo;
|
||||
import tech.easyflow.ai.easyagentsflow.entity.WorkflowCheckStage;
|
||||
import tech.easyflow.ai.easyagentsflow.service.TinyFlowService;
|
||||
import tech.easyflow.ai.easyagentsflow.service.WorkflowCheckService;
|
||||
import tech.easyflow.ai.easyagentsflow.service.WorkflowResumeService;
|
||||
import tech.easyflow.ai.easyagentsflow.support.PublishedWorkflowDefinitionIds;
|
||||
import tech.easyflow.ai.entity.Plugin;
|
||||
import tech.easyflow.ai.entity.PluginItem;
|
||||
import tech.easyflow.ai.service.BotPluginService;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
import tech.easyflow.ai.entity.WorkflowExecResult;
|
||||
import tech.easyflow.ai.enums.PluginType;
|
||||
import tech.easyflow.ai.plugin.workflow.snapshot.WorkflowPluginSnapshotResolver;
|
||||
import tech.easyflow.ai.service.PluginService;
|
||||
import tech.easyflow.ai.service.PluginItemService;
|
||||
import tech.easyflow.ai.service.AgentResourceReferenceService;
|
||||
import tech.easyflow.ai.service.PluginVisibilityService;
|
||||
import tech.easyflow.ai.service.WorkflowService;
|
||||
import tech.easyflow.ai.service.WorkflowExecResultService;
|
||||
import tech.easyflow.common.constant.Constants;
|
||||
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.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 控制层。
|
||||
@@ -36,15 +61,48 @@ import java.util.List;
|
||||
@RequestMapping("/api/v1/pluginItem")
|
||||
@UsePermission(moduleName = "/api/v1/plugin")
|
||||
public class PluginItemController extends BaseCurdController<PluginItemService, PluginItem> {
|
||||
/**
|
||||
* 创建插件工具控制器。
|
||||
*
|
||||
* @param service 插件工具服务
|
||||
*/
|
||||
public PluginItemController(PluginItemService service) {
|
||||
super(service);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取插件工具列表关键字搜索字段。
|
||||
*
|
||||
* @return 工具名称和描述属性
|
||||
*/
|
||||
@Override
|
||||
protected String[] getKeywordSearchProperties() {
|
||||
return new String[]{"name", "description"};
|
||||
}
|
||||
|
||||
@Resource
|
||||
private PluginItemService pluginItemService;
|
||||
|
||||
@Resource
|
||||
private BotPluginService botPluginService;
|
||||
private AgentResourceReferenceService agentResourceReferenceService;
|
||||
@Resource
|
||||
private PluginService pluginService;
|
||||
@Resource
|
||||
private PluginVisibilityService pluginVisibilityService;
|
||||
@Resource
|
||||
private WorkflowPluginSnapshotResolver workflowPluginSnapshotResolver;
|
||||
@Resource
|
||||
private WorkflowService workflowService;
|
||||
@Resource
|
||||
private WorkflowExecResultService workflowExecResultService;
|
||||
@Resource
|
||||
private ChainExecutor chainExecutor;
|
||||
@Resource
|
||||
private TinyFlowService tinyFlowService;
|
||||
@Resource
|
||||
private WorkflowCheckService workflowCheckService;
|
||||
@Resource
|
||||
private WorkflowResumeService workflowResumeService;
|
||||
|
||||
@PostMapping("/tool/save")
|
||||
@SaCheckPermission("/api/v1/plugin/save")
|
||||
@@ -60,25 +118,12 @@ public class PluginItemController extends BaseCurdController<PluginItemService,
|
||||
return pluginItemService.searchPlugin(aiPluginToolId);
|
||||
}
|
||||
|
||||
@PostMapping("/toolsList")
|
||||
@SaCheckPermission("/api/v1/plugin/query")
|
||||
public Result<List<PluginItem>> searchPluginToolByPluginId(@JsonBody(value = "pluginId", required = true) BigInteger pluginId,
|
||||
@JsonBody(value = "botId", required = false) BigInteger botId){
|
||||
return Result.ok(pluginItemService.searchPluginToolByPluginId(pluginId, botId));
|
||||
}
|
||||
|
||||
@PostMapping("/tool/update")
|
||||
@SaCheckPermission("/api/v1/plugin/save")
|
||||
public Result<Boolean> updatePlugin(@JsonBody PluginItem pluginItem){
|
||||
return Result.ok(pluginItemService.updatePlugin(pluginItem));
|
||||
}
|
||||
|
||||
@PostMapping("/tool/list")
|
||||
@SaCheckPermission("/api/v1/plugin/query")
|
||||
public Result<List<PluginItem>> getPluginToolList(@JsonBody(value = "botId", required = true) BigInteger botId){
|
||||
return Result.ok(pluginItemService.getPluginToolList(botId));
|
||||
}
|
||||
|
||||
@GetMapping("/getTinyFlowData")
|
||||
@SaCheckPermission("/api/v1/plugin/query")
|
||||
public Result<?> getTinyFlowData(BigInteger id) {
|
||||
@@ -87,8 +132,18 @@ public class PluginItemController extends BaseCurdController<PluginItemService,
|
||||
if (record == null) {
|
||||
return Result.ok(nodeData);
|
||||
}
|
||||
Plugin plugin = pluginService.getById(record.getPluginId());
|
||||
nodeData.put("pluginId", record.getId().toString());
|
||||
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 outputDefs = new JSONArray();
|
||||
@@ -104,6 +159,7 @@ public class PluginItemController extends BaseCurdController<PluginItemService,
|
||||
handleArray(array);
|
||||
outputDefs = array;
|
||||
}
|
||||
nodeData.put("schemaHash", resolveSchemaHash(record, plugin));
|
||||
nodeData.put("parameters", parameters);
|
||||
nodeData.put("outputDefs", outputDefs);
|
||||
return Result.ok(nodeData);
|
||||
@@ -119,6 +175,95 @@ public class PluginItemController extends BaseCurdController<PluginItemService,
|
||||
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) {
|
||||
assertPluginTestExecutionOwnership(executeId);
|
||||
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) {
|
||||
assertPluginTestExecutionOwnership(executeId);
|
||||
workflowResumeService.resume(executeId, confirmParams);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验插件试运行实例由当前登录用户发起。
|
||||
*
|
||||
* @param executeId 执行实例 ID
|
||||
*/
|
||||
private void assertPluginTestExecutionOwnership(String executeId) {
|
||||
if (StrUtil.isBlank(executeId)) {
|
||||
throw new BusinessException("执行ID不能为空");
|
||||
}
|
||||
WorkflowExecResult record = workflowExecResultService.getByExecKey(executeId);
|
||||
if (record == null) {
|
||||
throw new BusinessException(404, 404, "工作流执行记录不存在或已过期");
|
||||
}
|
||||
LoginAccount currentAccount = SaTokenUtil.getLoginAccount();
|
||||
if (currentAccount == null
|
||||
|| currentAccount.getId() == null
|
||||
|| record.getCreatedBy() == null
|
||||
|| !currentAccount.getId().toString().equals(record.getCreatedBy())) {
|
||||
throw new BusinessException(403, 403, "无权限访问当前插件试运行实例");
|
||||
}
|
||||
}
|
||||
|
||||
private void handleArray(JSONArray array) {
|
||||
for (Object o : array) {
|
||||
JSONObject obj = (JSONObject) o;
|
||||
@@ -134,17 +279,98 @@ public class PluginItemController extends BaseCurdController<PluginItemService,
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Result<?> onRemoveBefore(Collection<Serializable> ids) {
|
||||
|
||||
QueryWrapper queryWrapper = QueryWrapper.create();
|
||||
queryWrapper.in(BotPlugin::getPluginItemId, ids);
|
||||
|
||||
boolean exists = botPluginService.exists(queryWrapper);
|
||||
if (exists){
|
||||
return Result.fail(1, "此工具还关联着bot,请先取消关联!");
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除插件工具前锁定资源并校验 Agent 绑定。
|
||||
*
|
||||
* @param ids 插件工具 ID 集合
|
||||
* @return 校验失败结果;允许删除时返回 {@code null}
|
||||
*/
|
||||
@Override
|
||||
protected Result<?> onRemoveBefore(Collection<Serializable> ids) {
|
||||
Set<BigInteger> uniquePluginItemIds = new LinkedHashSet<>();
|
||||
try {
|
||||
for (Serializable id : ids) {
|
||||
if (id == null) {
|
||||
throw new NumberFormatException("null");
|
||||
}
|
||||
uniquePluginItemIds.add(new BigInteger(String.valueOf(id)));
|
||||
}
|
||||
} catch (NumberFormatException exception) {
|
||||
throw new BusinessException("插件工具 ID 不合法");
|
||||
}
|
||||
|
||||
// BaseCurdController#remove 已开启事务;锁定工具行后校验权限和 Agent 引用。
|
||||
List<PluginItem> lockedPluginItems = pluginItemService.list(QueryWrapper.create()
|
||||
.in(PluginItem::getId, uniquePluginItemIds)
|
||||
.orderBy(PluginItem::getId, true)
|
||||
.forUpdate());
|
||||
if (lockedPluginItems == null || lockedPluginItems.size() != uniquePluginItemIds.size()) {
|
||||
throw new BusinessException("插件工具不存在或已被删除");
|
||||
}
|
||||
|
||||
LoginAccount loginAccount = SaTokenUtil.getLoginAccount();
|
||||
if (loginAccount == null || loginAccount.getTenantId() == null) {
|
||||
throw new BusinessException("当前登录信息无效");
|
||||
}
|
||||
Map<BigInteger, Plugin> plugins = new HashMap<>();
|
||||
for (PluginItem pluginItem : lockedPluginItems) {
|
||||
if (pluginItem.getPluginId() == null) {
|
||||
throw new BusinessException("插件工具关联的插件不存在");
|
||||
}
|
||||
Plugin plugin = plugins.get(pluginItem.getPluginId());
|
||||
if (plugin == null) {
|
||||
plugin = pluginService.getById(pluginItem.getPluginId());
|
||||
if (plugin == null || plugin.getTenantId() == null
|
||||
|| !loginAccount.getTenantId().toString().equals(plugin.getTenantId().toString())) {
|
||||
throw new BusinessException("无权限删除该插件工具");
|
||||
}
|
||||
pluginVisibilityService.assertPluginVisible(
|
||||
plugin.getCreatedBy(),
|
||||
plugin.getId(),
|
||||
"无权限删除该插件工具"
|
||||
);
|
||||
plugins.put(pluginItem.getPluginId(), plugin);
|
||||
}
|
||||
if (PluginType.isWorkflow(plugin.getType())) {
|
||||
return Result.fail(1, "工作流插件工具由系统自动维护,不支持删除");
|
||||
}
|
||||
}
|
||||
|
||||
agentResourceReferenceService.assertPluginItemsUnused(List.copyOf(uniquePluginItemIds));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
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")
|
||||
@@ -35,4 +40,24 @@ public class PublicChatSessionController {
|
||||
);
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,14 +4,19 @@ import cn.hutool.core.io.FileTypeUtil;
|
||||
import cn.hutool.http.HttpUtil;
|
||||
import com.mybatisflex.core.paginate.Page;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.springframework.util.StringUtils;
|
||||
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.ai.ResourcePreviewVo;
|
||||
import tech.easyflow.ai.entity.Resource;
|
||||
import tech.easyflow.ai.service.ResourceService;
|
||||
import tech.easyflow.ai.utils.DocUtil;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
|
||||
import tech.easyflow.system.service.CategoryPermissionService;
|
||||
|
||||
@@ -31,6 +36,8 @@ import static tech.easyflow.ai.entity.table.ResourceTableDef.RESOURCE;
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/resource")
|
||||
public class ResourceController extends BaseCurdController<ResourceService, Resource> {
|
||||
private static final int RESOURCE_PREVIEW_CONTENT_LIMIT = 20_000;
|
||||
|
||||
@javax.annotation.Resource
|
||||
private CategoryPermissionService categoryPermissionService;
|
||||
|
||||
@@ -38,6 +45,16 @@ public class ResourceController extends BaseCurdController<ResourceService, Reso
|
||||
super(service);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取资源列表关键字搜索字段。
|
||||
*
|
||||
* @return 资源名称属性
|
||||
*/
|
||||
@Override
|
||||
protected String[] getKeywordSearchProperties() {
|
||||
return new String[]{"resourceName"};
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Result<?> onSaveOrUpdateBefore(Resource entity, boolean isSave) {
|
||||
LoginAccount loginUser = SaTokenUtil.getLoginAccount();
|
||||
@@ -79,6 +96,26 @@ public class ResourceController extends BaseCurdController<ResourceService, Reso
|
||||
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()) {
|
||||
@@ -90,4 +127,37 @@ public class ResourceController extends BaseCurdController<ResourceService, Reso
|
||||
}
|
||||
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,990 @@
|
||||
package tech.easyflow.admin.controller.ai;
|
||||
|
||||
import cn.hutool.core.io.IoUtil;
|
||||
import com.mybatisflex.core.paginate.Page;
|
||||
import com.mybatisflex.core.query.QueryColumn;
|
||||
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.DocumentChunkContentUpdateRequest;
|
||||
import tech.easyflow.ai.dto.DocumentChunkDeleteResult;
|
||||
import tech.easyflow.ai.dto.DocumentChunkSyncRetryRequest;
|
||||
import tech.easyflow.ai.dto.DocumentChunkSyncStatus;
|
||||
import tech.easyflow.ai.dto.DocumentChunkSyncStatusRequest;
|
||||
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.vo.FaqImportResultVo;
|
||||
import tech.easyflow.ai.vo.KnowledgeShareAuthContext;
|
||||
import tech.easyflow.ai.vo.KnowledgeShareViewDetail;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.util.SearchKeywordUtil;
|
||||
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.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 = 20L * 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(required = true, skipConvertError = false)
|
||||
DocumentChunkContentUpdateRequest request
|
||||
) {
|
||||
KnowledgeShareAuthContext context = knowledgeShareService.assertUrlShareAccess(
|
||||
shareKey,
|
||||
null,
|
||||
KnowledgeShareActionScope.CONTENT_UPDATE.name()
|
||||
);
|
||||
DocumentChunk current = documentChunkService.getById(request.getId());
|
||||
if (current == null || current.getDocumentCollectionId() == null
|
||||
|| current.getDocumentCollectionId().compareTo(context.getKnowledge().getId()) != 0) {
|
||||
throw new BusinessException("记录不存在");
|
||||
}
|
||||
DocumentChunk updated = documentChunkService.updateContent(
|
||||
context.getKnowledge().getId(),
|
||||
current.getId(),
|
||||
request.getContent()
|
||||
);
|
||||
audit(context, "更新分享文档 Chunk", "KNOWLEDGE_SHARE_URL_WRITE", true,
|
||||
auditDetail("knowledgeId", context.getKnowledge().getId(), "chunkId", request.getId()));
|
||||
return Result.ok(updated);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除 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, "记录不存在");
|
||||
}
|
||||
DocumentChunkDeleteResult removed = documentChunkService.deleteChunk(
|
||||
context.getKnowledge().getId(),
|
||||
chunkId
|
||||
);
|
||||
audit(context, "删除分享文档 Chunk", "KNOWLEDGE_SHARE_URL_WRITE", true,
|
||||
auditDetail("knowledgeId", context.getKnowledge().getId(), "chunkId", chunkId));
|
||||
return Result.ok(removed);
|
||||
}
|
||||
|
||||
@PostMapping("/documentChunk/syncStatus")
|
||||
public Result<List<DocumentChunkSyncStatus>> documentChunkSyncStatus(
|
||||
@RequestParam String shareKey,
|
||||
@JsonBody DocumentChunkSyncStatusRequest request
|
||||
) {
|
||||
KnowledgeShareAuthContext context = knowledgeShareService.assertUrlShareAccess(
|
||||
shareKey, null, KnowledgeShareActionScope.VIEW.name()
|
||||
);
|
||||
Document document = documentService.getById(request.getDocumentId());
|
||||
if (document == null || document.getCollectionId() == null
|
||||
|| document.getCollectionId().compareTo(context.getKnowledge().getId()) != 0) {
|
||||
throw new BusinessException("文档不存在");
|
||||
}
|
||||
return Result.ok(documentChunkService.listIndexSyncStatus(
|
||||
context.getKnowledge().getId(), request.getDocumentId(), request.getIds()
|
||||
));
|
||||
}
|
||||
|
||||
@PostMapping("/documentChunk/retrySync")
|
||||
public Result<DocumentChunk> retryDocumentChunkSync(
|
||||
@RequestParam String shareKey,
|
||||
@JsonBody DocumentChunkSyncRetryRequest request
|
||||
) {
|
||||
KnowledgeShareAuthContext context = knowledgeShareService.assertUrlShareAccess(
|
||||
shareKey, null, KnowledgeShareActionScope.CONTENT_UPDATE.name()
|
||||
);
|
||||
if (request.getIndexSyncVersion() == null) {
|
||||
throw new BusinessException("同步版本不能为空");
|
||||
}
|
||||
DocumentChunk chunk = documentChunkService.retryIndexSync(
|
||||
context.getKnowledge().getId(), request.getId(), request.getIndexSyncVersion()
|
||||
);
|
||||
audit(context, "重试分享文档 Chunk 索引同步", "KNOWLEDGE_SHARE_URL_WRITE", true,
|
||||
auditDetail("knowledgeId", context.getKnowledge().getId(), "chunkId", request.getId()));
|
||||
return Result.ok(chunk);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 keyword = request.getParameter("keyword");
|
||||
String question = request.getParameter("question");
|
||||
if (StringUtils.hasText(keyword)) {
|
||||
String pattern = SearchKeywordUtil.literalContainsPattern(keyword);
|
||||
queryWrapper.and(new QueryColumn("question").likeRaw(pattern)
|
||||
.or(new QueryColumn("answer_text").likeRaw(pattern)));
|
||||
} else if (StringUtils.hasText(question)) {
|
||||
queryWrapper.and(new QueryColumn("question")
|
||||
.likeRaw(SearchKeywordUtil.literalContainsPattern(question)));
|
||||
}
|
||||
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("图片大小不能超过20MB");
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,88 +1,45 @@
|
||||
package tech.easyflow.admin.controller.ai;
|
||||
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.easyagents.flow.core.chain.ChainDefinition;
|
||||
import com.easyagents.flow.core.chain.Node;
|
||||
import com.easyagents.flow.core.node.ConfirmNode;
|
||||
import com.easyagents.flow.core.node.EndNode;
|
||||
import com.easyagents.flow.core.node.StartNode;
|
||||
import com.easyagents.flow.core.parser.ChainParser;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import tech.easyflow.ai.easyagentsflow.service.WorkflowDatacenterContentService;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
import tech.easyflow.ai.service.WorkflowService;
|
||||
import tech.easyflow.admin.service.ai.WorkflowDesignerOptionService;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.List;
|
||||
import java.math.BigInteger;
|
||||
|
||||
/**
|
||||
* 工作流节点兼容接口。
|
||||
*/
|
||||
@RequestMapping("/api/v1/workflowNode")
|
||||
@RestController
|
||||
public class WorkFlowNodeController {
|
||||
|
||||
@Resource
|
||||
private WorkflowService workflowService;
|
||||
@Resource
|
||||
private ChainParser chainParser;
|
||||
@Resource
|
||||
private WorkflowDatacenterContentService workflowDatacenterContentService;
|
||||
private final WorkflowDesignerOptionService workflowDesignerOptionService;
|
||||
|
||||
/**
|
||||
* 创建工作流节点兼容控制器。
|
||||
*
|
||||
* @param workflowDesignerOptionService 工作流设计器选项服务
|
||||
*/
|
||||
public WorkFlowNodeController(WorkflowDesignerOptionService workflowDesignerOptionService) {
|
||||
this.workflowDesignerOptionService = workflowDesignerOptionService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询子流程输入输出定义。
|
||||
*
|
||||
* @param currentId 当前工作流 ID
|
||||
* @param workflowId 子流程 ID
|
||||
* @return 子流程节点配置
|
||||
* @deprecated 请使用 {@code /api/v1/workflow/designer/childWorkflow}
|
||||
*/
|
||||
@Deprecated
|
||||
@GetMapping("/getChainParams")
|
||||
public Result<?> getChainParams(String currentId, String workflowId) {
|
||||
if (workflowId.equals(currentId)) {
|
||||
throw new BusinessException("工作流不能作为自身子节点");
|
||||
}
|
||||
JSONObject nodeData = new JSONObject();
|
||||
Workflow workflow = workflowService.getById(workflowId);
|
||||
if (workflow == null) {
|
||||
throw new BusinessException("工作流不存在: " + workflowId);
|
||||
}
|
||||
nodeData.put("workflowId", workflow.getId());
|
||||
nodeData.put("workflowName", workflow.getTitle());
|
||||
|
||||
ChainDefinition definition = chainParser.parse(workflowDatacenterContentService.prepareContent(workflow.getContent()));
|
||||
List<Node> nodes = definition.getNodes();
|
||||
JSONArray inputs = new JSONArray();
|
||||
JSONArray outputs = new JSONArray();
|
||||
for (Node node : nodes) {
|
||||
if (node instanceof StartNode) {
|
||||
inputs = JSON.parseArray(JSON.toJSONString(node.getParameters()));
|
||||
handleArray(inputs);
|
||||
}
|
||||
if (node instanceof EndNode) {
|
||||
outputs = JSON.parseArray(JSON.toJSONString(((EndNode) node).getOutputDefs()));
|
||||
handleArray(outputs);
|
||||
}
|
||||
if (node instanceof ConfirmNode) {
|
||||
throw new BusinessException("工作流存在【确认节点】,暂不支持作为子节点");
|
||||
}
|
||||
}
|
||||
nodeData.put("parameters", inputs);
|
||||
nodeData.put("outputDefs", outputs);
|
||||
return Result.ok(nodeData);
|
||||
}
|
||||
|
||||
private void handleArray(JSONArray array) {
|
||||
if (array != null) {
|
||||
for (Object o : array) {
|
||||
JSONObject obj = (JSONObject) o;
|
||||
obj.put("id", IdUtil.simpleUUID());
|
||||
obj.put("nameDisabled", true);
|
||||
obj.put("dataTypeDisabled", true);
|
||||
obj.put("deleteDisabled", true);
|
||||
obj.put("addChildDisabled", true);
|
||||
obj.put("refType", "ref");
|
||||
JSONArray children = obj.getJSONArray("children");
|
||||
if (children != null) {
|
||||
handleArray(children);
|
||||
}
|
||||
}
|
||||
}
|
||||
@SaCheckPermission("/api/v1/workflow/query")
|
||||
public Result<JSONObject> getChainParams(BigInteger currentId, BigInteger workflowId) {
|
||||
return Result.ok(workflowDesignerOptionService.getChildWorkflowNodeData(currentId, workflowId));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,344 @@
|
||||
package tech.easyflow.admin.controller.ai;
|
||||
|
||||
import com.easyagents.flow.core.chain.runtime.ChainExecutor;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
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.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
import tech.easyflow.admin.service.ai.WorkflowChatEventStream;
|
||||
import tech.easyflow.ai.easyagentsflow.entity.WorkflowCheckStage;
|
||||
import tech.easyflow.ai.easyagentsflow.service.WorkflowCheckService;
|
||||
import tech.easyflow.ai.easyagentsflow.service.WorkflowRunningParameterResolver;
|
||||
import tech.easyflow.ai.easyagentsflow.service.WorkflowResumeService;
|
||||
import tech.easyflow.ai.easyagentsflow.support.PublishedWorkflowDefinitionIds;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
import tech.easyflow.ai.entity.WorkflowExecResult;
|
||||
import tech.easyflow.ai.entity.WorkflowExecStep;
|
||||
import tech.easyflow.ai.enums.PublishStatus;
|
||||
import tech.easyflow.ai.service.WorkflowExecResultService;
|
||||
import tech.easyflow.ai.service.WorkflowExecStepService;
|
||||
import tech.easyflow.ai.service.WorkflowService;
|
||||
import tech.easyflow.ai.service.WorkflowShareService;
|
||||
import tech.easyflow.ai.share.WorkflowSharePolicy;
|
||||
import tech.easyflow.ai.utils.WorkFlowUtil;
|
||||
import tech.easyflow.common.constant.Constants;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceAction;
|
||||
import tech.easyflow.system.service.ResourceAccessService;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 工作流管理端与分享端的对话运行接口。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/workflowChat")
|
||||
public class WorkflowChatController {
|
||||
|
||||
@Resource
|
||||
private WorkflowService workflowService;
|
||||
@Resource
|
||||
private WorkflowShareService workflowShareService;
|
||||
@Resource
|
||||
private WorkflowCheckService workflowCheckService;
|
||||
@Resource
|
||||
private WorkflowRunningParameterResolver parameterResolver;
|
||||
@Resource
|
||||
private ResourceAccessService resourceAccessService;
|
||||
@Resource
|
||||
private WorkflowChatEventStream eventStream;
|
||||
@Resource
|
||||
private ChainExecutor chainExecutor;
|
||||
@Resource
|
||||
private WorkflowResumeService workflowResumeService;
|
||||
@Resource
|
||||
private WorkflowExecResultService execResultService;
|
||||
@Resource
|
||||
private WorkflowExecStepService execStepService;
|
||||
|
||||
/**
|
||||
* 获取工作流的对话运行描述和输入表单。
|
||||
*
|
||||
* @param workflowId 工作流 ID
|
||||
* @param request HTTP 请求
|
||||
* @return 对话运行描述
|
||||
*/
|
||||
@GetMapping("/descriptor")
|
||||
public Result<Map<String, Object>> descriptor(
|
||||
BigInteger workflowId,
|
||||
HttpServletRequest request
|
||||
) {
|
||||
Workflow workflow = loadRunnableWorkflow(workflowId, request);
|
||||
workflowCheckService.checkOrThrow(
|
||||
workflow.getContent(),
|
||||
WorkflowCheckStage.PRE_EXECUTE,
|
||||
workflow.getId()
|
||||
);
|
||||
Map<String, Object> descriptor =
|
||||
parameterResolver.buildRunningParametersView(workflow);
|
||||
if (descriptor == null) {
|
||||
throw new BusinessException("工作流输入配置无法解析");
|
||||
}
|
||||
descriptor.put("workflowId", workflow.getId());
|
||||
descriptor.put("publishStatus", workflow.getPublishStatus());
|
||||
descriptor.put("shareable", isStrictlyPublished(workflow));
|
||||
return Result.ok(descriptor);
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动工作流,并流式返回全部可见输出。
|
||||
*
|
||||
* @param workflowId 工作流 ID
|
||||
* @param variables 工作流运行变量
|
||||
* @param request HTTP 请求
|
||||
* @return 工作流 SSE 事件流
|
||||
*/
|
||||
@PostMapping(
|
||||
value = "/run",
|
||||
produces = MediaType.TEXT_EVENT_STREAM_VALUE
|
||||
)
|
||||
public SseEmitter run(
|
||||
@JsonBody(value = "workflowId", required = true)
|
||||
BigInteger workflowId,
|
||||
@JsonBody("variables") Map<String, Object> variables,
|
||||
HttpServletRequest request
|
||||
) {
|
||||
Workflow workflow = loadRunnableWorkflow(workflowId, request);
|
||||
workflowCheckService.checkOrThrow(
|
||||
workflow.getContent(),
|
||||
WorkflowCheckStage.PRE_EXECUTE,
|
||||
workflow.getId()
|
||||
);
|
||||
Map<String, Object> normalizedVariables =
|
||||
parameterResolver.normalizeRuntimeVariables(
|
||||
workflow.getContent(),
|
||||
variables
|
||||
);
|
||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||
normalizedVariables.put(Constants.LOGIN_USER_KEY, account);
|
||||
normalizedVariables.put(
|
||||
WorkFlowUtil.CREATED_KEY_MEMORY_KEY,
|
||||
hasChatShareKey(request)
|
||||
? WorkFlowUtil.WORKFLOW_CHAT_SHARE
|
||||
: WorkFlowUtil.WORKFLOW_CHAT
|
||||
);
|
||||
return eventStream.start(
|
||||
isStrictlyPublished(workflow)
|
||||
? PublishedWorkflowDefinitionIds.published(workflowId.toString())
|
||||
: workflowId.toString(),
|
||||
normalizedVariables
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消当前用户发起的工作流执行。
|
||||
*
|
||||
* @param executeId 执行实例 ID
|
||||
* @return 是否完成取消状态转换
|
||||
*/
|
||||
@PostMapping("/cancel")
|
||||
public Result<Boolean> cancel(
|
||||
@JsonBody(value = "executeId", required = true)
|
||||
String executeId
|
||||
) {
|
||||
assertExecutionOwnership(executeId);
|
||||
return Result.ok(chainExecutor.cancel(executeId, "用户已中止运行"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复当前用户发起并等待确认的工作流执行。
|
||||
*
|
||||
* @param executeId 执行实例 ID
|
||||
* @param confirmParams 确认参数
|
||||
* @return 空结果
|
||||
*/
|
||||
@PostMapping("/resume")
|
||||
public Result<Void> resume(
|
||||
@JsonBody(value = "executeId", required = true)
|
||||
String executeId,
|
||||
@JsonBody("confirmParams")
|
||||
Map<String, Object> confirmParams
|
||||
) {
|
||||
assertExecutionOwnership(executeId);
|
||||
workflowResumeService.resume(executeId, confirmParams);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户工作流执行的运行详情。
|
||||
*
|
||||
* @param executeId 执行实例 ID
|
||||
* @return 执行记录和有序节点步骤
|
||||
*/
|
||||
@GetMapping("/execution")
|
||||
public Result<Map<String, Object>> detail(String executeId) {
|
||||
WorkflowExecResult record = assertExecutionOwnership(executeId);
|
||||
List<WorkflowExecStep> steps = execStepService.list(
|
||||
QueryWrapper.create()
|
||||
.eq(WorkflowExecStep::getRecordId, record.getId())
|
||||
.orderBy(WorkflowExecStep::getStartTime, true)
|
||||
);
|
||||
List<Map<String, Object>> stepViews = new ArrayList<>(steps.size());
|
||||
for (WorkflowExecStep step : steps) {
|
||||
Map<String, Object> view = new LinkedHashMap<>();
|
||||
view.put("id", step.getId());
|
||||
view.put("attemptKey", step.getExecKey());
|
||||
view.put("nodeId", step.getNodeId());
|
||||
view.put("nodeName", step.getNodeName());
|
||||
view.put("input", step.getInput());
|
||||
view.put("output", step.getOutput());
|
||||
view.put("status", step.getStatus());
|
||||
view.put("errorInfo", step.getErrorInfo());
|
||||
view.put("startTime", step.getStartTime());
|
||||
view.put("endTime", step.getEndTime());
|
||||
view.put("execTime", step.getExecTime());
|
||||
stepViews.add(view);
|
||||
}
|
||||
|
||||
Map<String, Object> recordView = new LinkedHashMap<>();
|
||||
recordView.put("executeId", record.getExecKey());
|
||||
recordView.put("workflowId", record.getWorkflowId());
|
||||
recordView.put("title", record.getTitle());
|
||||
recordView.put("status", record.getStatus());
|
||||
recordView.put("input", record.getInput());
|
||||
recordView.put("output", record.getOutput());
|
||||
recordView.put("errorInfo", record.getErrorInfo());
|
||||
recordView.put("startTime", record.getStartTime());
|
||||
recordView.put("endTime", record.getEndTime());
|
||||
recordView.put("execTime", record.getExecTime());
|
||||
|
||||
Map<String, Object> detail = new LinkedHashMap<>();
|
||||
detail.put("record", recordView);
|
||||
detail.put("steps", stepViews);
|
||||
return Result.ok(detail);
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载可运行工作流,并校验直接访问或对话分享权限。
|
||||
* 管理端直接运行时,已发布工作流使用发布快照,未发布工作流使用当前内容;
|
||||
* 分享运行始终要求严格发布快照。
|
||||
*
|
||||
* @param workflowId 工作流 ID
|
||||
* @param request HTTP 请求
|
||||
* @return 可运行工作流视图
|
||||
*/
|
||||
private Workflow loadRunnableWorkflow(
|
||||
BigInteger workflowId,
|
||||
HttpServletRequest request
|
||||
) {
|
||||
if (workflowId == null) {
|
||||
throw new BusinessException("工作流ID不能为空");
|
||||
}
|
||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||
Workflow current = workflowService.getById(workflowId);
|
||||
if (current == null) {
|
||||
throw new BusinessException("工作流不存在");
|
||||
}
|
||||
boolean sharedRequest = hasChatShareKey(request);
|
||||
if (sharedRequest) {
|
||||
workflowShareService.assertChatShareAccess(
|
||||
request.getHeader(
|
||||
WorkflowSharePolicy.CHAT_SHARE_KEY_HEADER
|
||||
),
|
||||
workflowId,
|
||||
account.getTenantId()
|
||||
);
|
||||
} else {
|
||||
resourceAccessService.assertAccess(
|
||||
CategoryResourceType.WORKFLOW,
|
||||
current,
|
||||
ResourceAction.USE,
|
||||
"无权限运行工作流"
|
||||
);
|
||||
}
|
||||
|
||||
Workflow published = workflowService.getPublishedById(workflowId);
|
||||
if (isStrictlyPublished(published)) {
|
||||
return published;
|
||||
}
|
||||
if (sharedRequest) {
|
||||
throw new BusinessException(
|
||||
409,
|
||||
409,
|
||||
"工作流尚未发布或已下线"
|
||||
);
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验执行记录属于当前用户发起的工作流对话。
|
||||
*
|
||||
* @param executeId 执行实例 ID
|
||||
* @return 执行记录
|
||||
*/
|
||||
private WorkflowExecResult assertExecutionOwnership(String executeId) {
|
||||
if (executeId == null || executeId.isBlank()) {
|
||||
throw new BusinessException("执行ID不能为空");
|
||||
}
|
||||
WorkflowExecResult record = execResultService.getByExecKey(executeId);
|
||||
if (record == null) {
|
||||
throw new BusinessException("工作流执行记录不存在,请稍后重试");
|
||||
}
|
||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||
boolean chatSource = WorkFlowUtil.WORKFLOW_CHAT.equals(
|
||||
record.getCreatedKey()
|
||||
) || WorkFlowUtil.WORKFLOW_CHAT_SHARE.equals(record.getCreatedKey());
|
||||
if (!chatSource
|
||||
|| account.getId() == null
|
||||
|| !account.getId().toString().equals(
|
||||
record.getCreatedBy()
|
||||
)) {
|
||||
throw new BusinessException(
|
||||
403,
|
||||
403,
|
||||
"无权限访问当前工作流执行记录"
|
||||
);
|
||||
}
|
||||
return record;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断请求是否携带对话分享密钥。
|
||||
*
|
||||
* @param request HTTP 请求
|
||||
* @return 携带非空对话分享密钥时返回 {@code true}
|
||||
*/
|
||||
private boolean hasChatShareKey(HttpServletRequest request) {
|
||||
String shareKey = request == null
|
||||
? null
|
||||
: request.getHeader(
|
||||
WorkflowSharePolicy.CHAT_SHARE_KEY_HEADER
|
||||
);
|
||||
return shareKey != null && !shareKey.isBlank();
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断工作流是否可按严格发布快照运行。
|
||||
*
|
||||
* @param workflow 工作流
|
||||
* @return 已发布且存在快照时返回 {@code true}
|
||||
*/
|
||||
private boolean isStrictlyPublished(Workflow workflow) {
|
||||
return workflow != null
|
||||
&& PublishStatus.PUBLISHED.getCode().equals(
|
||||
workflow.getPublishStatus()
|
||||
)
|
||||
&& workflow.getPublishedSnapshotJson() != null
|
||||
&& !workflow.getPublishedSnapshotJson().isEmpty();
|
||||
}
|
||||
}
|
||||
@@ -4,17 +4,23 @@ import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.hutool.core.io.IoUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.mybatisflex.core.paginate.Page;
|
||||
import com.easyagents.flow.core.chain.ChainDefinition;
|
||||
import com.easyagents.flow.core.chain.Parameter;
|
||||
import com.easyagents.flow.core.chain.runtime.ChainExecutor;
|
||||
import com.easyagents.flow.core.parser.ChainParser;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import tech.easyflow.admin.controller.ai.support.AiResourceCreatorNameSupport;
|
||||
import tech.easyflow.admin.model.ai.WorkflowDesignerOptionsView;
|
||||
import tech.easyflow.admin.service.ai.WorkflowDesignerOptionService;
|
||||
import tech.easyflow.agent.entity.AgentToolBinding;
|
||||
import tech.easyflow.agent.enums.AgentToolType;
|
||||
import tech.easyflow.agent.service.AgentToolBindingService;
|
||||
import tech.easyflow.ai.permission.WorkflowShareResourceAccessGrantProvider;
|
||||
import tech.easyflow.ai.permission.WorkflowVisibilityQueryHelper;
|
||||
import tech.easyflow.ai.easyagentsflow.entity.ChainInfo;
|
||||
import tech.easyflow.ai.easyagentsflow.entity.NodeInfo;
|
||||
@@ -24,10 +30,14 @@ import tech.easyflow.ai.easyagentsflow.service.CodeEngineCapabilityService;
|
||||
import tech.easyflow.ai.easyagentsflow.service.TinyFlowService;
|
||||
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.easyagentsflow.service.WorkflowResumeService;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
import tech.easyflow.ai.enums.PublishStatus;
|
||||
import tech.easyflow.ai.publish.WorkflowPublishAppService;
|
||||
import tech.easyflow.ai.service.BotWorkflowService;
|
||||
import tech.easyflow.ai.service.AiResourceApprovalStateService;
|
||||
import tech.easyflow.ai.vo.OfflineImpactCheckVo;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalActionResult;
|
||||
import tech.easyflow.ai.service.ModelService;
|
||||
import tech.easyflow.ai.service.WorkflowService;
|
||||
import tech.easyflow.common.constant.Constants;
|
||||
@@ -37,6 +47,7 @@ import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||
import tech.easyflow.datacenter.execution.model.DatasetRef;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceAction;
|
||||
import tech.easyflow.system.enums.ResourceLookup;
|
||||
@@ -51,6 +62,7 @@ import java.io.Serializable;
|
||||
import java.math.BigInteger;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -69,12 +81,10 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
||||
@Resource
|
||||
private SysApiKeyService apiKeyService;
|
||||
@Resource
|
||||
private BotWorkflowService botWorkflowService;
|
||||
private AgentToolBindingService agentToolBindingService;
|
||||
@Resource
|
||||
private ChainExecutor chainExecutor;
|
||||
@Resource
|
||||
private ChainParser chainParser;
|
||||
@Resource
|
||||
private TinyFlowService tinyFlowService;
|
||||
@Resource
|
||||
private CodeEngineCapabilityService codeEngineCapabilityService;
|
||||
@@ -83,17 +93,143 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
||||
@Resource
|
||||
private WorkflowDatacenterContentService workflowDatacenterContentService;
|
||||
@Resource
|
||||
private WorkflowRunningParameterResolver workflowRunningParameterResolver;
|
||||
@Resource
|
||||
private WorkflowResumeService workflowResumeService;
|
||||
@Resource
|
||||
private ResourceAccessService resourceAccessService;
|
||||
@Resource
|
||||
private WorkflowVisibilityQueryHelper workflowVisibilityQueryHelper;
|
||||
@Resource
|
||||
private WorkflowPublishAppService workflowPublishAppService;
|
||||
@Resource
|
||||
private AiResourceApprovalStateService aiResourceApprovalStateService;
|
||||
@Resource
|
||||
private AiResourceCreatorNameSupport aiResourceCreatorNameSupport;
|
||||
@Resource
|
||||
private WorkflowShareResourceAccessGrantProvider workflowShareGrantProvider;
|
||||
@Resource
|
||||
private WorkflowDesignerOptionService workflowDesignerOptionService;
|
||||
|
||||
public WorkflowController(WorkflowService service, ModelService modelService) {
|
||||
super(service);
|
||||
this.modelService = modelService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取工作流列表关键字搜索字段。
|
||||
*
|
||||
* @return 标题和描述属性
|
||||
*/
|
||||
@Override
|
||||
protected String[] getKeywordSearchProperties() {
|
||||
return new String[]{"title", "description"};
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询工作流设计器初始化所需的安全选项。
|
||||
*
|
||||
* @return 模型、知识库和代码引擎选项
|
||||
*/
|
||||
@GetMapping("/designer/options")
|
||||
@SaCheckPermission("/api/v1/workflow/query")
|
||||
public Result<WorkflowDesignerOptionsView> designerOptions() {
|
||||
return Result.ok(workflowDesignerOptionService.listOptions(
|
||||
codeEngineCapabilityService.listSupportedCodeEngines()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询工作流设计器可用插件。
|
||||
*
|
||||
* @param pageNumber 页码
|
||||
* @param pageSize 每页数量
|
||||
* @param keyword 插件名称或描述关键字
|
||||
* @return 插件安全选项分页
|
||||
*/
|
||||
@GetMapping("/designer/plugins")
|
||||
@SaCheckPermission("/api/v1/workflow/query")
|
||||
public Result<Page<WorkflowDesignerOptionsView.PluginOption>> designerPlugins(
|
||||
Long pageNumber,
|
||||
Long pageSize,
|
||||
String keyword) {
|
||||
return Result.ok(workflowDesignerOptionService.pagePlugins(pageNumber, pageSize, keyword));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询工作流插件节点配置。
|
||||
*
|
||||
* @param id 插件工具 ID
|
||||
* @return 插件节点配置
|
||||
*/
|
||||
@GetMapping("/designer/pluginTinyFlow")
|
||||
@SaCheckPermission("/api/v1/workflow/query")
|
||||
public Result<?> designerPluginTinyFlow(BigInteger id) {
|
||||
return Result.ok(workflowDesignerOptionService.getPluginTinyFlowData(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询工作流设计器的子流程节点配置。
|
||||
*
|
||||
* @param currentId 当前工作流 ID
|
||||
* @param workflowId 子流程 ID
|
||||
* @return 子流程输入输出定义
|
||||
*/
|
||||
@GetMapping("/designer/childWorkflow")
|
||||
@SaCheckPermission("/api/v1/workflow/query")
|
||||
public Result<JSONObject> designerChildWorkflow(BigInteger currentId, BigInteger workflowId) {
|
||||
return Result.ok(workflowDesignerOptionService.getChildWorkflowNodeData(currentId, workflowId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询工作流数据节点可见的数据源。
|
||||
*
|
||||
* @return 数据源安全选项
|
||||
*/
|
||||
@GetMapping("/designer/dataSources")
|
||||
@SaCheckPermission("/api/v1/workflow/query")
|
||||
public Result<List<WorkflowDesignerOptionsView.DataSourceOption>> designerDataSources() {
|
||||
return Result.ok(workflowDesignerOptionService.listDataSources());
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询工作流数据节点的数据目录。
|
||||
*
|
||||
* @param sourceId 数据源 ID
|
||||
* @return 目录安全选项
|
||||
*/
|
||||
@GetMapping("/designer/catalogs")
|
||||
@SaCheckPermission("/api/v1/workflow/query")
|
||||
public Result<List<WorkflowDesignerOptionsView.CatalogOption>> designerCatalogs(BigInteger sourceId) {
|
||||
return Result.ok(workflowDesignerOptionService.listCatalogs(sourceId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询工作流数据节点的已接入数据集。
|
||||
*
|
||||
* @param sourceId 数据源 ID
|
||||
* @param catalogId 目录 ID
|
||||
* @return 数据集安全选项
|
||||
*/
|
||||
@GetMapping("/designer/managedTables")
|
||||
@SaCheckPermission("/api/v1/workflow/query")
|
||||
public Result<List<WorkflowDesignerOptionsView.DatasetOption>> designerManagedTables(
|
||||
BigInteger sourceId,
|
||||
BigInteger catalogId) {
|
||||
return Result.ok(workflowDesignerOptionService.listManagedDatasets(sourceId, catalogId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询工作流数据节点的数据集结构。
|
||||
*
|
||||
* @param datasetRef 数据集引用
|
||||
* @return 数据集结构安全视图
|
||||
*/
|
||||
@GetMapping("/designer/schema")
|
||||
@SaCheckPermission("/api/v1/workflow/query")
|
||||
public Result<WorkflowDesignerOptionsView.DatasetSchemaOption> designerSchema(DatasetRef datasetRef) {
|
||||
return Result.ok(workflowDesignerOptionService.getDatasetSchema(datasetRef));
|
||||
}
|
||||
|
||||
/**
|
||||
* 节点单独运行
|
||||
*/
|
||||
@@ -115,6 +251,16 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
||||
if (workflow == null) {
|
||||
return Result.fail(1, "工作流不存在");
|
||||
}
|
||||
workflowCheckService.checkOrThrow(
|
||||
workflow.getContent(), WorkflowCheckStage.PRE_EXECUTE, workflow.getId());
|
||||
workflowDesignerOptionService.assertContentReferences(workflow.getContent());
|
||||
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);
|
||||
return Result.ok(res);
|
||||
}
|
||||
@@ -141,6 +287,8 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
||||
throw new RuntimeException("工作流不存在");
|
||||
}
|
||||
workflowCheckService.checkOrThrow(workflow.getContent(), WorkflowCheckStage.PRE_EXECUTE, workflow.getId());
|
||||
workflowDesignerOptionService.assertContentReferences(workflow.getContent());
|
||||
variables = workflowRunningParameterResolver.normalizeRuntimeVariables(workflow.getContent(), variables);
|
||||
if (StpUtil.isLogin()) {
|
||||
variables.put(Constants.LOGIN_USER_KEY, SaTokenUtil.getLoginAccount());
|
||||
}
|
||||
@@ -179,7 +327,7 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
||||
)
|
||||
public Result<Void> resume(@JsonBody(value = "executeId", required = true) String executeId,
|
||||
@JsonBody("confirmParams") Map<String, Object> confirmParams) {
|
||||
chainExecutor.resumeAsync(executeId, confirmParams);
|
||||
workflowResumeService.resume(executeId, confirmParams);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@@ -231,17 +379,11 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
||||
return Result.fail(1, "can not find the workflow by id: " + id);
|
||||
}
|
||||
workflowCheckService.checkOrThrow(workflow.getContent(), WorkflowCheckStage.PRE_EXECUTE, workflow.getId());
|
||||
|
||||
ChainDefinition definition = chainParser.parse(workflowDatacenterContentService.prepareContent(workflow.getContent()));
|
||||
if (definition == null) {
|
||||
workflowDesignerOptionService.assertContentReferences(workflow.getContent());
|
||||
Map<String, Object> res = workflowRunningParameterResolver.buildRunningParametersView(workflow);
|
||||
if (res == null) {
|
||||
return Result.fail(2, "节点配置错误,请检查! ");
|
||||
}
|
||||
List<Parameter> chainParameters = definition.getStartParameters();
|
||||
Map<String, Object> res = new HashMap<>();
|
||||
res.put("parameters", chainParameters);
|
||||
res.put("title", workflow.getTitle());
|
||||
res.put("description", workflow.getDescription());
|
||||
res.put("icon", workflow.getIcon());
|
||||
return Result.ok(res);
|
||||
}
|
||||
|
||||
@@ -249,12 +391,88 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
||||
* 提交发布审批。
|
||||
*
|
||||
* @param id 工作流 ID
|
||||
* @param applicationReason 审批说明
|
||||
* @return 审批实例 ID
|
||||
*/
|
||||
@PostMapping("/submitPublishApproval")
|
||||
@SaCheckPermission("/api/v1/workflow/save")
|
||||
public Result<BigInteger> submitPublishApproval(@JsonBody("id") BigInteger id) {
|
||||
return Result.ok(workflowPublishAppService.submitPublishApproval(id));
|
||||
@RequireResourceAccess(
|
||||
resource = CategoryResourceType.WORKFLOW,
|
||||
action = ResourceAction.MANAGE,
|
||||
lookup = ResourceLookup.WORKFLOW_ID,
|
||||
idExpr = "#id",
|
||||
denyMessage = "无权限发布工作流"
|
||||
)
|
||||
public Result<BigInteger> submitPublishApproval(
|
||||
@JsonBody("id") BigInteger id,
|
||||
@JsonBody("applicationReason") String applicationReason
|
||||
) {
|
||||
return buildApprovalActionResult(
|
||||
workflowPublishAppService.submitPublishApproval(id, applicationReason),
|
||||
"已提交发布审批",
|
||||
"已直接发布"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 预检工作流发布是否命中审批流。
|
||||
*
|
||||
* @param id 工作流 ID
|
||||
* @return 是否需要审批
|
||||
*/
|
||||
@GetMapping("/publishApprovalRequirement")
|
||||
@SaCheckPermission("/api/v1/workflow/save")
|
||||
@RequireResourceAccess(
|
||||
resource = CategoryResourceType.WORKFLOW,
|
||||
action = ResourceAction.MANAGE,
|
||||
lookup = ResourceLookup.WORKFLOW_ID,
|
||||
idExpr = "#id",
|
||||
denyMessage = "无权限发布工作流"
|
||||
)
|
||||
public Result<Boolean> publishApprovalRequirement(@RequestParam BigInteger id) {
|
||||
return Result.ok(workflowPublishAppService.isPublishApprovalRequired(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交下线审批。
|
||||
*
|
||||
* @param id 工作流 ID
|
||||
* @return 审批实例 ID
|
||||
*/
|
||||
@PostMapping("/submitOfflineApproval")
|
||||
@SaCheckPermission("/api/v1/workflow/save")
|
||||
@RequireResourceAccess(
|
||||
resource = CategoryResourceType.WORKFLOW,
|
||||
action = ResourceAction.MANAGE,
|
||||
lookup = ResourceLookup.WORKFLOW_ID,
|
||||
idExpr = "#id",
|
||||
denyMessage = "无权限下线工作流"
|
||||
)
|
||||
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));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -265,8 +483,19 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
||||
*/
|
||||
@PostMapping("/submitDeleteApproval")
|
||||
@SaCheckPermission("/api/v1/workflow/remove")
|
||||
@RequireResourceAccess(
|
||||
resource = CategoryResourceType.WORKFLOW,
|
||||
action = ResourceAction.MANAGE,
|
||||
lookup = ResourceLookup.WORKFLOW_ID,
|
||||
idExpr = "#id",
|
||||
denyMessage = "无权限删除工作流"
|
||||
)
|
||||
public Result<BigInteger> submitDeleteApproval(@JsonBody("id") BigInteger id) {
|
||||
return Result.ok(workflowPublishAppService.submitDeleteApproval(id));
|
||||
return buildApprovalActionResult(
|
||||
workflowPublishAppService.submitDeleteApproval(id),
|
||||
"已提交删除审批",
|
||||
"已直接删除"
|
||||
);
|
||||
}
|
||||
|
||||
@GetMapping("/supportedCodeEngines")
|
||||
@@ -307,6 +536,7 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
||||
)
|
||||
public Result<Workflow> detail(String id) {
|
||||
Workflow workflow = service.getDetail(id);
|
||||
aiResourceApprovalStateService.fillWorkflowApprovalState(workflow);
|
||||
return Result.ok(workflow);
|
||||
}
|
||||
|
||||
@@ -327,20 +557,76 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
||||
}
|
||||
workflow.setId(null);
|
||||
workflow.setAlias(IdUtil.fastSimpleUUID());
|
||||
workflow.setRevision(0);
|
||||
commonFiled(workflow, account.getId(), account.getTenantId(), account.getDeptId());
|
||||
workflowDesignerOptionService.assertContentReferences(workflow.getContent());
|
||||
service.save(workflow);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新工作流;分享访问只允许按修订号更新设计内容。
|
||||
*
|
||||
* @param entity 工作流更新内容
|
||||
* @return 更新结果
|
||||
*/
|
||||
@Override
|
||||
@PostMapping("update")
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Result<?> update(@JsonBody Workflow entity) {
|
||||
if (entity == null || entity.getId() == null) {
|
||||
throw new BusinessException("工作流 ID 不能为空");
|
||||
}
|
||||
LoginAccount loginAccount = SaTokenUtil.getLoginAccount();
|
||||
boolean sharedRequest = workflowShareGrantProvider.isSharedRequestFor(
|
||||
entity.getId(),
|
||||
loginAccount
|
||||
);
|
||||
if (sharedRequest && entity.getContent() == null) {
|
||||
throw new BusinessException(403, 403, "分享链接仅允许编辑工作流内容");
|
||||
}
|
||||
if (entity.getContent() == null) {
|
||||
return super.update(entity);
|
||||
}
|
||||
if (entity.getRevision() == null) {
|
||||
throw workflowRevisionConflict();
|
||||
}
|
||||
Result<?> beforeResult = onSaveOrUpdateBefore(entity, false);
|
||||
if (beforeResult != null) {
|
||||
return beforeResult;
|
||||
}
|
||||
boolean updated = service.updateContentByRevision(
|
||||
entity.getId(),
|
||||
entity.getContent(),
|
||||
entity.getRevision(),
|
||||
new Date(),
|
||||
loginAccount.getId()
|
||||
);
|
||||
if (!updated) {
|
||||
throw workflowRevisionConflict();
|
||||
}
|
||||
entity.setRevision(entity.getRevision() + 1);
|
||||
if (!sharedRequest) {
|
||||
entity.setContent(null);
|
||||
service.updateById(entity);
|
||||
}
|
||||
onSaveOrUpdateAfter(entity, false);
|
||||
return Result.ok(Map.of("revision", entity.getRevision()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Result onSaveOrUpdateBefore(Workflow entity, boolean isSave) {
|
||||
normalizeVisibilityScope(entity, isSave);
|
||||
if (isSave && entity.getRevision() == null) {
|
||||
entity.setRevision(0);
|
||||
}
|
||||
if (!isSave && entity.getId() != null) {
|
||||
Workflow existed = requireWorkflow(String.valueOf(entity.getId()));
|
||||
resourceAccessService.assertAccess(CategoryResourceType.WORKFLOW, existed, ResourceAction.MANAGE, "无权限管理工作流");
|
||||
}
|
||||
if (StringUtils.hasLength(entity.getContent())) {
|
||||
workflowCheckService.checkOrThrow(entity.getContent(), WorkflowCheckStage.SAVE, entity.getId());
|
||||
workflowDesignerOptionService.assertContentReferences(entity.getContent());
|
||||
}
|
||||
|
||||
String alias = entity.getAlias();
|
||||
@@ -368,20 +654,34 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
||||
workflowVisibilityQueryHelper.applyReadableAccess(queryWrapper);
|
||||
applyPublishedOnlyFilter(queryWrapper);
|
||||
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||
return Result.ok(service.list(queryWrapper));
|
||||
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);
|
||||
return super.queryPage(page, 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, "请提交删除审批");
|
||||
return Result.fail(1, "请使用发布状态操作删除工作流");
|
||||
}
|
||||
|
||||
private Result<BigInteger> buildApprovalActionResult(ApprovalActionResult actionResult,
|
||||
String approvalMessage,
|
||||
String directMessage) {
|
||||
return Result.ok(
|
||||
actionResult.isApprovalRequired() ? approvalMessage : directMessage,
|
||||
actionResult.getInstanceId()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -390,11 +690,12 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
||||
Workflow workflow = requireWorkflow(String.valueOf(id));
|
||||
resourceAccessService.assertAccess(CategoryResourceType.WORKFLOW, workflow, ResourceAction.MANAGE, "无权限管理工作流");
|
||||
}
|
||||
QueryWrapper queryWrapper = QueryWrapper.create();
|
||||
queryWrapper.in("workflow_id", ids);
|
||||
boolean exists = botWorkflowService.exists(queryWrapper);
|
||||
QueryWrapper queryWrapper = QueryWrapper.create()
|
||||
.eq(AgentToolBinding::getToolType, AgentToolType.WORKFLOW.name())
|
||||
.in(AgentToolBinding::getTargetId, ids);
|
||||
boolean exists = agentToolBindingService.exists(queryWrapper);
|
||||
if (exists) {
|
||||
return Result.fail(1, "此工作流还关联有bot,请先取消关联后再删除!");
|
||||
return Result.fail(1, "此工作流仍被智能体使用,请先取消绑定后再删除");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -420,6 +721,15 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
||||
return workflow;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建工作流内容修订冲突异常。
|
||||
*
|
||||
* @return HTTP 409 业务异常
|
||||
*/
|
||||
private BusinessException workflowRevisionConflict() {
|
||||
return new BusinessException(409, 409, "工作流已被其他人更新,请刷新后重新编辑");
|
||||
}
|
||||
|
||||
private void applyPublishedOnlyFilter(QueryWrapper queryWrapper) {
|
||||
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
if (attributes == null) {
|
||||
|
||||
@@ -17,6 +17,7 @@ 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.log.annotation.LogRecord;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigInteger;
|
||||
@@ -36,9 +37,20 @@ public class WorkflowExecResultController extends BaseCurdController<WorkflowExe
|
||||
super(service);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取工作流执行记录关键字搜索字段。
|
||||
*
|
||||
* @return 执行 Key 属性
|
||||
*/
|
||||
@Override
|
||||
protected String[] getKeywordSearchProperties() {
|
||||
return new String[]{"execKey"};
|
||||
}
|
||||
|
||||
@GetMapping("/del")
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@SaCheckPermission("/api/v1/workflow/remove")
|
||||
@LogRecord("删除工作流执行记录")
|
||||
public Result<Void> del(BigInteger id) {
|
||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||
WorkflowExecResult record = service.getById(id);
|
||||
|
||||
@@ -37,6 +37,16 @@ public class WorkflowExecStepController extends BaseCurdController<WorkflowExecS
|
||||
super(service);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取执行步骤关键字搜索字段。
|
||||
*
|
||||
* @return 节点名称和节点 ID 属性
|
||||
*/
|
||||
@Override
|
||||
protected String[] getKeywordSearchProperties() {
|
||||
return new String[]{"nodeName", "nodeId"};
|
||||
}
|
||||
|
||||
@GetMapping("/getListByRecordId")
|
||||
public Result<List<WorkflowExecStep>> getListByRecordId(BigInteger recordId) {
|
||||
if (recordId == null) {
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
package tech.easyflow.admin.controller.ai;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaIgnore;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
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.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
import tech.easyflow.admin.service.ai.WorkflowPublicChatService;
|
||||
import tech.easyflow.ai.share.WorkflowSharePolicy;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.vo.UploadResVo;
|
||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 工作流对话匿名分享接口。
|
||||
*/
|
||||
@SaIgnore
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/workflowChat/public")
|
||||
public class WorkflowPublicChatController {
|
||||
|
||||
private final WorkflowPublicChatService publicChatService;
|
||||
|
||||
public WorkflowPublicChatController(
|
||||
WorkflowPublicChatService publicChatService
|
||||
) {
|
||||
this.publicChatService = publicChatService;
|
||||
}
|
||||
|
||||
@GetMapping("/descriptor")
|
||||
public Result<Map<String, Object>> descriptor(HttpServletRequest request) {
|
||||
return Result.ok(publicChatService.descriptor(
|
||||
shareKey(request),
|
||||
visitorId(request)
|
||||
));
|
||||
}
|
||||
|
||||
@PostMapping(value = "/run", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||
public SseEmitter run(
|
||||
@JsonBody("variables") Map<String, Object> variables,
|
||||
HttpServletRequest request
|
||||
) {
|
||||
return publicChatService.run(
|
||||
shareKey(request),
|
||||
visitorId(request),
|
||||
variables
|
||||
);
|
||||
}
|
||||
|
||||
@GetMapping("/execution")
|
||||
public Result<Map<String, Object>> execution(
|
||||
@RequestParam String executeId,
|
||||
HttpServletRequest request
|
||||
) {
|
||||
return Result.ok(publicChatService.detail(
|
||||
shareKey(request),
|
||||
visitorId(request),
|
||||
executeId
|
||||
));
|
||||
}
|
||||
|
||||
@PostMapping("/cancel")
|
||||
public Result<Boolean> cancel(
|
||||
@JsonBody(value = "executeId", required = true) String executeId,
|
||||
HttpServletRequest request
|
||||
) {
|
||||
return Result.ok(publicChatService.cancel(
|
||||
shareKey(request),
|
||||
visitorId(request),
|
||||
executeId
|
||||
));
|
||||
}
|
||||
|
||||
@PostMapping("/resume")
|
||||
public Result<Void> resume(
|
||||
@JsonBody(value = "executeId", required = true) String executeId,
|
||||
@JsonBody("confirmParams") Map<String, Object> confirmParams,
|
||||
HttpServletRequest request
|
||||
) {
|
||||
publicChatService.resume(
|
||||
shareKey(request),
|
||||
visitorId(request),
|
||||
executeId,
|
||||
confirmParams
|
||||
);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@PostMapping(value = "/upload", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public Result<UploadResVo> upload(
|
||||
@RequestParam("file") MultipartFile file,
|
||||
@RequestParam("parameterName") String parameterName,
|
||||
HttpServletRequest request
|
||||
) {
|
||||
return Result.ok(publicChatService.upload(
|
||||
shareKey(request),
|
||||
visitorId(request),
|
||||
parameterName,
|
||||
file
|
||||
));
|
||||
}
|
||||
|
||||
private String shareKey(HttpServletRequest request) {
|
||||
return request.getHeader(WorkflowSharePolicy.CHAT_SHARE_KEY_HEADER);
|
||||
}
|
||||
|
||||
private String visitorId(HttpServletRequest request) {
|
||||
return request.getHeader(WorkflowSharePolicy.CHAT_VISITOR_HEADER);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
package tech.easyflow.admin.controller.ai;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import cn.dev33.satoken.annotation.SaIgnore;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
import tech.easyflow.ai.entity.WorkflowShare;
|
||||
import tech.easyflow.ai.service.KnowledgeShareAuditService;
|
||||
import tech.easyflow.ai.service.WorkflowService;
|
||||
import tech.easyflow.ai.service.WorkflowShareService;
|
||||
import tech.easyflow.ai.share.WorkflowSharePolicy;
|
||||
import tech.easyflow.ai.vo.WorkflowShareCreateResult;
|
||||
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/workflowShare")
|
||||
public class WorkflowShareController {
|
||||
|
||||
@Resource
|
||||
private WorkflowShareService workflowShareService;
|
||||
|
||||
@Resource
|
||||
private WorkflowService workflowService;
|
||||
|
||||
@Resource
|
||||
private ResourceAccessService resourceAccessService;
|
||||
|
||||
@Resource
|
||||
private KnowledgeShareAuditService knowledgeShareAuditService;
|
||||
|
||||
/**
|
||||
* 创建或刷新已发布工作流的对话分享链接。
|
||||
*
|
||||
* @param request HTTP 请求
|
||||
* @param workflowId 工作流 ID
|
||||
* @return 分享创建结果
|
||||
*/
|
||||
@PostMapping("/url/create")
|
||||
@SaCheckPermission("/api/v1/workflow/save")
|
||||
public Result<WorkflowShareCreateResult> createUrlShare(
|
||||
HttpServletRequest request,
|
||||
@JsonBody("workflowId") BigInteger workflowId
|
||||
) {
|
||||
Workflow workflow = workflowService.getById(workflowId);
|
||||
if (workflow == null) {
|
||||
throw new BusinessException("工作流不存在");
|
||||
}
|
||||
resourceAccessService.assertAccess(
|
||||
CategoryResourceType.WORKFLOW,
|
||||
workflow,
|
||||
ResourceAction.MANAGE,
|
||||
"无权限分享工作流"
|
||||
);
|
||||
LoginAccount loginAccount = SaTokenUtil.getLoginAccount();
|
||||
WorkflowShareCreateResult result = workflowShareService.createChatShare(
|
||||
workflowId,
|
||||
loginAccount.getTenantId(),
|
||||
loginAccount.getDeptId(),
|
||||
loginAccount.getId(),
|
||||
buildShareBaseUrl(request)
|
||||
);
|
||||
knowledgeShareAuditService.log(
|
||||
loginAccount.getId(),
|
||||
"创建工作流对话分享",
|
||||
"WORKFLOW_CHAT_SHARE_CREATE",
|
||||
request.getRequestURI(),
|
||||
Map.of("workflowId", workflowId, "shareId", result.getId())
|
||||
);
|
||||
return Result.ok(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析当前对话分享指向的工作流。
|
||||
*
|
||||
* @param request HTTP 请求
|
||||
* @return 工作流标识
|
||||
*/
|
||||
@GetMapping("/resolve")
|
||||
@SaIgnore
|
||||
public Result<Map<String, BigInteger>> resolveUrlShare(HttpServletRequest request) {
|
||||
WorkflowShare share = workflowShareService.resolvePublicChatShare(
|
||||
request.getHeader(WorkflowSharePolicy.CHAT_SHARE_KEY_HEADER)
|
||||
);
|
||||
return Result.ok(Map.of("workflowId", share.getWorkflowId()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据管理端来源构建工作流分享基础 URL。
|
||||
*
|
||||
* @param request HTTP 请求
|
||||
* @return 工作流独立分享页 URL
|
||||
*/
|
||||
private String buildShareBaseUrl(HttpServletRequest request) {
|
||||
String refererBaseUrl = extractFrontendBaseUrl(RequestUtil.getReferer(request));
|
||||
if (refererBaseUrl != null) {
|
||||
return refererBaseUrl + "/share/workflow";
|
||||
}
|
||||
|
||||
String forwardedOrigin = buildForwardedOrigin(request);
|
||||
if (forwardedOrigin != null) {
|
||||
return forwardedOrigin
|
||||
+ normalizeBasePath(firstHeaderValue(request.getHeader("X-Forwarded-Prefix")))
|
||||
+ "/share/workflow";
|
||||
}
|
||||
|
||||
String origin = normalizeOrigin(request.getHeader("Origin"));
|
||||
if (origin != null) {
|
||||
return origin + normalizeBasePath(request.getContextPath()) + "/share/workflow";
|
||||
}
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append(request.getScheme()).append("://").append(request.getServerName());
|
||||
if (request.getServerPort() != 80 && request.getServerPort() != 443) {
|
||||
builder.append(':').append(request.getServerPort());
|
||||
}
|
||||
return builder.append(normalizeBasePath(request.getContextPath()))
|
||||
.append("/share/workflow")
|
||||
.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从来源地址提取前端 origin 与部署基路径。
|
||||
*
|
||||
* @param sourceUrl 来源地址
|
||||
* @return origin 与部署基路径,无法解析时返回 {@code null}
|
||||
*/
|
||||
private String extractFrontendBaseUrl(String sourceUrl) {
|
||||
if (sourceUrl == null || sourceUrl.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
URI uri = new URI(sourceUrl.trim());
|
||||
if (uri.getScheme() == null || uri.getHost() == null) {
|
||||
return null;
|
||||
}
|
||||
String origin = extractOrigin(sourceUrl);
|
||||
return origin == null ? null : origin + inferFrontendBasePath(uri.getPath());
|
||||
} catch (URISyntaxException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从前端页面路径推断部署基路径。
|
||||
*
|
||||
* @param path 页面路径
|
||||
* @return 规范化后的部署基路径
|
||||
*/
|
||||
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 "";
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化部署基路径。
|
||||
*
|
||||
* @param basePath 原始基路径
|
||||
* @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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按反向代理头构建外部访问 origin。
|
||||
*
|
||||
* @param request HTTP 请求
|
||||
* @return 外部 origin,缺少代理头时返回 {@code null}
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 URL 提取 origin。
|
||||
*
|
||||
* @param url 完整 URL
|
||||
* @return origin,无法解析时返回 {@code null}
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化 origin。
|
||||
*
|
||||
* @param origin 原始 origin
|
||||
* @return 规范化结果
|
||||
*/
|
||||
private String normalizeOrigin(String origin) {
|
||||
return extractOrigin(origin);
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取代理头的首个有效值。
|
||||
*
|
||||
* @param value 原始请求头
|
||||
* @return 首个有效值
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
package tech.easyflow.admin.controller.ai.support;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import tech.easyflow.agent.entity.Agent;
|
||||
import tech.easyflow.ai.entity.DocumentCollection;
|
||||
import tech.easyflow.ai.entity.Plugin;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
import tech.easyflow.skill.entity.Skill;
|
||||
import tech.easyflow.system.service.SysAccountService;
|
||||
import tech.easyflow.system.entity.SysAccount;
|
||||
|
||||
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 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量填充 Skill 创建人名称。
|
||||
*
|
||||
* @param skills Skill 集合
|
||||
*/
|
||||
public void fillSkillCreatorNames(Collection<Skill> skills) {
|
||||
if (skills == null || skills.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
LinkedHashSet<BigInteger> creatorIds = skills.stream().map(Skill::getCreatedBy)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new));
|
||||
if (creatorIds.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Map<BigInteger, String> labels = sysAccountService.list(QueryWrapper.create()
|
||||
.select(SysAccount::getId, SysAccount::getNickname, SysAccount::getLoginName)
|
||||
.in(SysAccount::getId, creatorIds)).stream()
|
||||
.collect(java.util.stream.Collectors.toMap(SysAccount::getId, this::skillCreatorLabel,
|
||||
(left, right) -> left, java.util.LinkedHashMap::new));
|
||||
skills.forEach(skill -> skill.setCreatedByName(
|
||||
labels.getOrDefault(skill.getCreatedBy(), String.valueOf(skill.getCreatedBy()))));
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 Skill 创建人格式化为“昵称(账号)”。
|
||||
*
|
||||
* @param account 创建人账号
|
||||
* @return 创建人展示标签
|
||||
*/
|
||||
private String skillCreatorLabel(SysAccount account) {
|
||||
String nickname = account.getNickname() == null ? "" : account.getNickname().trim();
|
||||
String loginName = account.getLoginName() == null ? "" : account.getLoginName().trim();
|
||||
if (nickname.isBlank()) {
|
||||
return loginName.isBlank() ? String.valueOf(account.getId()) : loginName;
|
||||
}
|
||||
return loginName.isBlank() || nickname.equals(loginName)
|
||||
? nickname : nickname + "(" + loginName + ")";
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用的创建人名称填充逻辑。
|
||||
*
|
||||
* @param resources 资源集合
|
||||
* @param createdByGetter 创建人 ID 提取函数
|
||||
* @param createdByNameSetter 创建人名称回填函数
|
||||
* @param <T> 资源类型
|
||||
*/
|
||||
<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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package tech.easyflow.admin.controller.ai.support;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
import tech.easyflow.ai.entity.Bot;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* 为旧 Bot 资源批量补充创建人展示名称。
|
||||
*/
|
||||
@Component
|
||||
public class BotResourceCreatorNameSupport {
|
||||
|
||||
private final AiResourceCreatorNameSupport creatorNameSupport;
|
||||
|
||||
/**
|
||||
* 创建 Bot 创建人名称填充组件。
|
||||
*
|
||||
* @param creatorNameSupport 通用 AI 资源创建人名称组件
|
||||
*/
|
||||
public BotResourceCreatorNameSupport(AiResourceCreatorNameSupport creatorNameSupport) {
|
||||
this.creatorNameSupport = creatorNameSupport;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量填充 Bot 创建人名称。
|
||||
*
|
||||
* @param bots Bot 集合
|
||||
*/
|
||||
public void fillCreatorNames(Collection<Bot> bots) {
|
||||
creatorNameSupport.fillCreatorNames(bots, Bot::getCreatedBy, Bot::setCreatedByName);
|
||||
}
|
||||
}
|
||||
@@ -2,15 +2,22 @@ package tech.easyflow.admin.controller.auth;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaIgnore;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
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.LoginVO;
|
||||
import tech.easyflow.auth.entity.*;
|
||||
import tech.easyflow.auth.service.AuthCredentialKeyService;
|
||||
import tech.easyflow.auth.service.AuthService;
|
||||
import tech.easyflow.common.captcha.tainai.CaptchaVerificationService;
|
||||
import tech.easyflow.common.constant.Constants;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||
import tech.easyflow.log.annotation.LogRecord;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.List;
|
||||
@@ -21,9 +28,23 @@ public class AuthController {
|
||||
|
||||
@Resource
|
||||
private AuthService authService;
|
||||
@Resource
|
||||
private AuthCredentialKeyService credentialKeyService;
|
||||
@Resource
|
||||
private CaptchaVerificationService captchaVerificationService;
|
||||
|
||||
@GetMapping("credential-key")
|
||||
public Result<CredentialKeyVO> credentialKey() {
|
||||
return Result.ok(credentialKeyService.getCurrentCredentialKey());
|
||||
}
|
||||
|
||||
@PostMapping("login")
|
||||
public Result<LoginVO> login(@JsonBody LoginDTO loginDTO) {
|
||||
public Result<LoginVO> login(@JsonBody EncryptedCredentialDTO encryptedCredential) {
|
||||
JSONObject payload = credentialKeyService.decryptPayload(encryptedCredential);
|
||||
if (!captchaVerificationService.verify(payload.getString("validToken"))) {
|
||||
return captchaVerificationService.failureResult();
|
||||
}
|
||||
LoginDTO loginDTO = toLoginDTO(payload);
|
||||
LoginVO res = authService.login(loginDTO);
|
||||
return Result.ok(res);
|
||||
}
|
||||
@@ -45,4 +66,23 @@ public class AuthController {
|
||||
List<String> permissionList = StpUtil.getPermissionList();
|
||||
return Result.ok(permissionList);
|
||||
}
|
||||
|
||||
@PostMapping("credential-key/rotate")
|
||||
@LogRecord("轮换认证传输密钥")
|
||||
public Result<Void> rotateCredentialKey(@JsonBody CredentialKeyRotateDTO rotateDTO) {
|
||||
StpUtil.checkLogin();
|
||||
LoginAccount loginAccount = SaTokenUtil.getLoginAccount();
|
||||
if (loginAccount == null || !Constants.SUPER_ADMIN_ID.equals(loginAccount.getId())) {
|
||||
throw new BusinessException("仅超级管理员可轮换认证密钥");
|
||||
}
|
||||
credentialKeyService.rotate(rotateDTO != null && Boolean.TRUE.equals(rotateDTO.getEmergency()));
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
private LoginDTO toLoginDTO(JSONObject payload) {
|
||||
LoginDTO loginDTO = new LoginDTO();
|
||||
loginDTO.setAccount(payload.getString("account"));
|
||||
loginDTO.setPassword(payload.getString("password"));
|
||||
return loginDTO;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import tech.easyflow.common.dict.Dict;
|
||||
import tech.easyflow.common.dict.DictItem;
|
||||
import tech.easyflow.common.dict.DictLoader;
|
||||
import tech.easyflow.common.dict.DictManager;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
@@ -16,18 +17,30 @@ import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 提供已注册静态字典的查询接口。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/dict/")
|
||||
public class DictController {
|
||||
|
||||
@Resource
|
||||
DictManager dictManager;
|
||||
private DictManager dictManager;
|
||||
|
||||
/**
|
||||
* 查询指定静态字典的选项。
|
||||
*
|
||||
* @param code 字典编码
|
||||
* @param keyword 搜索关键字
|
||||
* @param request HTTP 请求
|
||||
* @return 字典选项
|
||||
* @throws BusinessException 字典未注册时抛出
|
||||
*/
|
||||
@GetMapping("/items/{code}")
|
||||
public Result<List<DictItem>> items(@PathVariable("code") String code, String keyword, HttpServletRequest request) {
|
||||
DictLoader loader = dictManager.getLoader(code);
|
||||
if (loader == null) {
|
||||
return Result.ok(Collections.emptyList());
|
||||
throw new BusinessException("字典不存在或不支持公共查询");
|
||||
}
|
||||
Map<String, String[]> parameterMap = request.getParameterMap();
|
||||
Dict dict = loader.load(keyword, parameterMap);
|
||||
|
||||
@@ -1,15 +1,27 @@
|
||||
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.agent.service.AgentOptionQueryService;
|
||||
import tech.easyflow.agent.vo.AgentOptionView;
|
||||
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;
|
||||
|
||||
/**
|
||||
* 管理端工作台统计接口。
|
||||
*/
|
||||
@@ -18,9 +30,29 @@ import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
public class DashboardController {
|
||||
|
||||
private final DashboardService dashboardService;
|
||||
private final AgentOptionQueryService agentOptionQueryService;
|
||||
|
||||
public DashboardController(DashboardService dashboardService) {
|
||||
/**
|
||||
* 创建管理端工作台控制器。
|
||||
*
|
||||
* @param dashboardService 工作台统计服务
|
||||
* @param agentOptionQueryService Agent 安全选项服务
|
||||
*/
|
||||
public DashboardController(DashboardService dashboardService,
|
||||
AgentOptionQueryService agentOptionQueryService) {
|
||||
this.dashboardService = dashboardService;
|
||||
this.agentOptionQueryService = agentOptionQueryService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询工作台筛选可使用的 Agent。
|
||||
*
|
||||
* @return Agent 安全选项
|
||||
*/
|
||||
@GetMapping("/agentOptions")
|
||||
@SaCheckPermission("/api/v1/dashboard/query")
|
||||
public Result<List<AgentOptionView>> agentOptions() {
|
||||
return Result.ok(agentOptionQueryService.listAgentOptions(false));
|
||||
}
|
||||
|
||||
@GetMapping("/overview")
|
||||
@@ -28,4 +60,26 @@ public class DashboardController {
|
||||
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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,11 @@ import java.util.List;
|
||||
@RequestMapping("/api/v1/datacenterDataset")
|
||||
public class DatacenterDatasetController {
|
||||
|
||||
/** 对外 Schema 接口的默认字段页码。 */
|
||||
private static final long DEFAULT_FIELD_PAGE_NUMBER = 1L;
|
||||
/** 对外 Schema 接口的默认字段页大小。 */
|
||||
private static final long DEFAULT_FIELD_PAGE_SIZE = 200L;
|
||||
|
||||
@Resource
|
||||
private DatacenterDatasetQueryService queryService;
|
||||
@Resource
|
||||
@@ -32,13 +37,18 @@ public class DatacenterDatasetController {
|
||||
@PostMapping("/queryPage")
|
||||
@SaCheckPermission("/api/v1/datacenterSource/query")
|
||||
public Result<Page<Row>> queryPage(@RequestBody DatacenterQueryRequest request) {
|
||||
return Result.ok(queryService.queryPage(request));
|
||||
return Result.ok(queryService.queryPage(
|
||||
request, SaTokenUtil.getLoginAccount()));
|
||||
}
|
||||
|
||||
@GetMapping("/schema")
|
||||
@SaCheckPermission("/api/v1/datacenterSource/query")
|
||||
public Result<DatacenterSchemaResponse> schema(DatasetRef datasetRef) {
|
||||
return Result.ok(queryService.getSchema(datasetRef));
|
||||
public Result<DatacenterSchemaResponse> schema(
|
||||
DatasetRef datasetRef,
|
||||
@RequestParam(defaultValue = "1") Long fieldPageNumber,
|
||||
@RequestParam(defaultValue = "200") Long fieldPageSize) {
|
||||
return Result.ok(queryService.getSchema(
|
||||
datasetRef, fieldPageNumber, fieldPageSize));
|
||||
}
|
||||
|
||||
@GetMapping("/managedTables")
|
||||
@@ -63,6 +73,13 @@ public class DatacenterDatasetController {
|
||||
request == null ? List.of() : request.getFields(),
|
||||
account
|
||||
);
|
||||
return Result.ok(queryService.getSchema(registryService.resolveDatasetRef(table.getId())));
|
||||
return Result.ok(queryService.getSchema(
|
||||
registryService.resolveDatasetRef(table.getId()),
|
||||
request == null || request.getFieldPageNumber() == null
|
||||
? DEFAULT_FIELD_PAGE_NUMBER
|
||||
: request.getFieldPageNumber(),
|
||||
request == null || request.getFieldPageSize() == null
|
||||
? DEFAULT_FIELD_PAGE_SIZE
|
||||
: request.getFieldPageSize()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ 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.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
@@ -32,11 +33,21 @@ public class DatacenterExcelController {
|
||||
@Resource
|
||||
private DatacenterExcelImportService excelImportService;
|
||||
|
||||
/**
|
||||
* 上传并导入 Excel 工作簿。
|
||||
*
|
||||
* @param file Excel 工作簿
|
||||
* @param sourceName 数据源名称,留空时使用文件名
|
||||
* @return 导入任务及新建数据源标识
|
||||
* @throws Exception 文件解析或数据写入失败时抛出
|
||||
*/
|
||||
@PostMapping("/import")
|
||||
@SaCheckPermission("/api/v1/datacenterSource/save")
|
||||
public Result<DatacenterImportJob> importWorkbook(MultipartFile file) throws Exception {
|
||||
public Result<DatacenterImportJob> importWorkbook(
|
||||
@RequestParam("file") MultipartFile file,
|
||||
@RequestParam(value = "sourceName", required = false) String sourceName) throws Exception {
|
||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||
return Result.ok(excelImportService.importWorkbook(file, account));
|
||||
return Result.ok(excelImportService.importWorkbook(file, sourceName, account));
|
||||
}
|
||||
|
||||
@PostMapping("/split")
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package tech.easyflow.admin.controller.datacenter;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.datacenter.execution.model.DatacenterSqlConsoleRequest;
|
||||
import tech.easyflow.datacenter.execution.model.DatacenterSqlConsoleResult;
|
||||
import tech.easyflow.datacenter.execution.model.DatacenterSqlCancelRequest;
|
||||
import tech.easyflow.datacenter.federation.DatacenterFederationQueryService;
|
||||
import tech.easyflow.datacenter.federation.DatacenterFederationQueryCancellationService;
|
||||
import tech.easyflow.datacenter.meta.entity.DatacenterSource;
|
||||
import tech.easyflow.datacenter.meta.service.DatacenterDatasetRegistryService;
|
||||
|
||||
/**
|
||||
* 数据中枢管理端只读 SQL 控制台。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/datacenterQuery")
|
||||
public class DatacenterQueryController {
|
||||
|
||||
private final DatacenterDatasetRegistryService registryService;
|
||||
private final DatacenterFederationQueryService queryService;
|
||||
private final DatacenterFederationQueryCancellationService cancellationService;
|
||||
|
||||
/**
|
||||
* 创建查询 Controller。
|
||||
*
|
||||
* @param registryService 数据集注册服务
|
||||
* @param queryService Federation 查询服务
|
||||
* @param cancellationService 跨节点查询取消服务
|
||||
*/
|
||||
public DatacenterQueryController(
|
||||
DatacenterDatasetRegistryService registryService,
|
||||
DatacenterFederationQueryService queryService,
|
||||
DatacenterFederationQueryCancellationService cancellationService) {
|
||||
this.registryService = registryService;
|
||||
this.queryService = queryService;
|
||||
this.cancellationService = cancellationService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行一条受 Calcite 与业务 Policy 校验的只读 SQL。
|
||||
*
|
||||
* @param request 查询请求
|
||||
* @return 有界查询结果
|
||||
*/
|
||||
@PostMapping("/execute")
|
||||
@SaCheckPermission("/api/v1/datacenterSource/query")
|
||||
public Result<DatacenterSqlConsoleResult> execute(
|
||||
@RequestBody DatacenterSqlConsoleRequest request) {
|
||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||
DatacenterSource source = registryService.getSourceRequired(
|
||||
request == null ? null : request.sourceId());
|
||||
return Result.ok(queryService.execute(
|
||||
source,
|
||||
request == null ? null : request.sql(),
|
||||
java.util.List.of(),
|
||||
request == null ? null : request.maxRows(),
|
||||
account,
|
||||
"MANUAL",
|
||||
account == null || account.getId() == null
|
||||
? null : account.getId().toString(),
|
||||
request == null ? null : request.queryId()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消当前租户在任一节点执行的 SQL 查询。
|
||||
*
|
||||
* @param request 取消请求
|
||||
* @return 是否在本地或集群中接受取消提示
|
||||
*/
|
||||
@PostMapping("/cancel")
|
||||
@SaCheckPermission("/api/v1/datacenterSource/query")
|
||||
public Result<Boolean> cancel(@RequestBody DatacenterSqlCancelRequest request) {
|
||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||
return Result.ok(cancellationService.cancel(
|
||||
request == null ? null : request.queryId(), account));
|
||||
}
|
||||
}
|
||||
@@ -8,10 +8,16 @@ 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.DatacenterMetadataPage;
|
||||
import tech.easyflow.datacenter.meta.model.DatacenterRemoveSourceRequest;
|
||||
import tech.easyflow.datacenter.meta.model.DatacenterSourceActivateRequest;
|
||||
import tech.easyflow.datacenter.meta.model.DatacenterSourceCandidateMetadataRequest;
|
||||
import tech.easyflow.datacenter.meta.model.DatacenterSourceCandidateCatalogRequest;
|
||||
import tech.easyflow.datacenter.meta.model.DatacenterSourceDraftRequest;
|
||||
import tech.easyflow.datacenter.meta.model.DatacenterSourceReconfigureRequest;
|
||||
import tech.easyflow.datacenter.meta.model.DatacenterSourceView;
|
||||
import tech.easyflow.datacenter.meta.model.DatacenterTableDetailMeta;
|
||||
import tech.easyflow.datacenter.meta.service.DatacenterSourceService;
|
||||
|
||||
@@ -19,6 +25,9 @@ import javax.annotation.Resource;
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 数据源绑定、生命周期与元数据浏览接口。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/datacenterSource")
|
||||
public class DatacenterSourceController {
|
||||
@@ -26,23 +35,100 @@ public class DatacenterSourceController {
|
||||
@Resource
|
||||
private DatacenterSourceService sourceService;
|
||||
|
||||
@PostMapping("/testConnection")
|
||||
@SaCheckPermission("/api/v1/datacenterSource/query")
|
||||
public Result<DatacenterConnectionTestResult> testConnection(@RequestBody DatacenterSource source) {
|
||||
@PostMapping("/draft")
|
||||
@SaCheckPermission("/api/v1/datacenterSource/save")
|
||||
public Result<DatacenterSourceView> saveDraft(@RequestBody DatacenterSourceDraftRequest request) {
|
||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||
return Result.ok(sourceService.testConnection(source, account));
|
||||
return Result.ok(sourceService.saveDraft(request, account));
|
||||
}
|
||||
|
||||
@PostMapping("/save")
|
||||
@SaCheckPermission("/api/v1/datacenterSource/save")
|
||||
public Result<DatacenterSource> save(@RequestBody DatacenterSource source) {
|
||||
@PostMapping("/{sourceId}/probe")
|
||||
@SaCheckPermission("/api/v1/datacenterSource/query")
|
||||
public Result<DatacenterConnectionTestResult> probe(@PathVariable BigInteger sourceId) {
|
||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||
return Result.ok(sourceService.saveSource(source, account));
|
||||
return Result.ok(sourceService.probe(sourceId, account));
|
||||
}
|
||||
|
||||
@PostMapping("/activate")
|
||||
@SaCheckPermission("/api/v1/datacenterSource/save")
|
||||
public Result<DatacenterSourceView> activate(@RequestBody DatacenterSourceActivateRequest request) {
|
||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||
return Result.ok(sourceService.activate(request, account));
|
||||
}
|
||||
|
||||
/**
|
||||
* 探测活动数据源的未发布候选配置。
|
||||
*
|
||||
* @param request 候选连接配置
|
||||
* @return 连接探测结果
|
||||
*/
|
||||
@PostMapping("/candidate/probe")
|
||||
@SaCheckPermission("/api/v1/datacenterSource/save")
|
||||
public Result<DatacenterConnectionTestResult> probeCandidate(
|
||||
@RequestBody DatacenterSourceDraftRequest request) {
|
||||
return Result.ok(sourceService.probeCandidate(
|
||||
request, SaTokenUtil.getLoginAccount()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 浏览活动数据源候选配置可访问的命名空间。
|
||||
*
|
||||
* @param request 候选连接配置
|
||||
* @return 命名空间列表
|
||||
*/
|
||||
@PostMapping("/candidate/catalogs")
|
||||
@SaCheckPermission("/api/v1/datacenterSource/save")
|
||||
public Result<List<DatacenterCatalogMeta>> candidateCatalogs(
|
||||
@RequestBody DatacenterSourceDraftRequest request) {
|
||||
return Result.ok(sourceService.listCandidateCatalogs(
|
||||
request, SaTokenUtil.getLoginAccount()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页浏览候选配置可访问的命名空间。
|
||||
*
|
||||
* @param request 候选配置和分页条件
|
||||
* @return 有界命名空间列表
|
||||
*/
|
||||
@PostMapping("/candidate/catalogs/page")
|
||||
@SaCheckPermission("/api/v1/datacenterSource/save")
|
||||
public Result<DatacenterMetadataPage<DatacenterCatalogMeta>> candidateCatalogsPage(
|
||||
@RequestBody DatacenterSourceCandidateCatalogRequest request) {
|
||||
return Result.ok(sourceService.listCandidateCatalogsPage(
|
||||
request, SaTokenUtil.getLoginAccount()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页浏览活动数据源候选配置可访问的表。
|
||||
*
|
||||
* @param request 候选配置与分页条件
|
||||
* @return 有界表列表
|
||||
*/
|
||||
@PostMapping("/candidate/tables")
|
||||
@SaCheckPermission("/api/v1/datacenterSource/save")
|
||||
public Result<DatacenterMetadataPage<DatacenterTable>> candidateTables(
|
||||
@RequestBody DatacenterSourceCandidateMetadataRequest request) {
|
||||
return Result.ok(sourceService.listCandidateTables(
|
||||
request, SaTokenUtil.getLoginAccount()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 原地发布活动数据源的新连接配置和纳管范围。
|
||||
*
|
||||
* @param request 重配置请求
|
||||
* @return 发布后的数据源视图
|
||||
*/
|
||||
@PostMapping("/reconfigure")
|
||||
@SaCheckPermission("/api/v1/datacenterSource/save")
|
||||
public Result<DatacenterSourceView> reconfigure(
|
||||
@RequestBody DatacenterSourceReconfigureRequest request) {
|
||||
return Result.ok(sourceService.reconfigure(
|
||||
request, SaTokenUtil.getLoginAccount()));
|
||||
}
|
||||
|
||||
@GetMapping("/page")
|
||||
@SaCheckPermission("/api/v1/datacenterSource/query")
|
||||
public Result<Page<DatacenterSource>> page(Long pageNumber, Long pageSize) {
|
||||
public Result<Page<DatacenterSourceView>> page(Long pageNumber, Long pageSize) {
|
||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||
return Result.ok(sourceService.pageSources(pageNumber, pageSize, account));
|
||||
}
|
||||
@@ -54,19 +140,53 @@ public class DatacenterSourceController {
|
||||
return Result.ok(sourceService.listCatalogs(sourceId, account));
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页浏览当前数据源的命名空间。
|
||||
*
|
||||
* @param sourceId 数据源 ID
|
||||
* @param keyword 名称搜索词
|
||||
* @param pageNumber 页码
|
||||
* @param pageSize 每页大小
|
||||
* @return 有界命名空间列表
|
||||
*/
|
||||
@GetMapping("/catalogs/page")
|
||||
@SaCheckPermission("/api/v1/datacenterSource/query")
|
||||
public Result<DatacenterMetadataPage<DatacenterCatalogMeta>> catalogsPage(
|
||||
BigInteger sourceId,
|
||||
String keyword,
|
||||
Long pageNumber,
|
||||
Long pageSize) {
|
||||
return Result.ok(sourceService.listCatalogsPage(
|
||||
sourceId, keyword, pageNumber, pageSize,
|
||||
SaTokenUtil.getLoginAccount()));
|
||||
}
|
||||
|
||||
@GetMapping("/tables")
|
||||
@SaCheckPermission("/api/v1/datacenterSource/query")
|
||||
public Result<List<DatacenterTable>> tables(BigInteger sourceId, String catalogName) {
|
||||
public Result<DatacenterMetadataPage<DatacenterTable>> tables(
|
||||
BigInteger sourceId,
|
||||
String catalogName,
|
||||
String keyword,
|
||||
Long pageNumber,
|
||||
Long pageSize) {
|
||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||
return Result.ok(sourceService.listTables(sourceId, catalogName, account));
|
||||
return Result.ok(sourceService.listTables(
|
||||
sourceId, catalogName, keyword, pageNumber, pageSize, account));
|
||||
}
|
||||
|
||||
@GetMapping("/tableDetail")
|
||||
@SaCheckPermission("/api/v1/datacenterSource/query")
|
||||
public Result<DatacenterTableDetailMeta> tableDetail(BigInteger sourceId, String catalogName, String tableName,
|
||||
@RequestParam(defaultValue = "false") boolean register) {
|
||||
public Result<DatacenterTableDetailMeta> tableDetail(
|
||||
BigInteger sourceId,
|
||||
String catalogName,
|
||||
String tableName,
|
||||
@RequestParam(defaultValue = "false") boolean register,
|
||||
Long fieldPageNumber,
|
||||
Long fieldPageSize) {
|
||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||
return Result.ok(sourceService.getTableDetail(sourceId, catalogName, tableName, register, account));
|
||||
return Result.ok(sourceService.getTableDetail(
|
||||
sourceId, catalogName, tableName, register,
|
||||
fieldPageNumber, fieldPageSize, account));
|
||||
}
|
||||
|
||||
@PostMapping("/registerBatch")
|
||||
@@ -83,4 +203,44 @@ public class DatacenterSourceController {
|
||||
sourceService.removeSource(request == null ? null : request.getSourceId(), account);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 停用活动数据源。
|
||||
*
|
||||
* @param sourceId 数据源 ID
|
||||
* @return 停用后的数据源视图
|
||||
*/
|
||||
@PostMapping("/{sourceId}/disable")
|
||||
@SaCheckPermission("/api/v1/datacenterSource/save")
|
||||
public Result<DatacenterSourceView> disable(@PathVariable BigInteger sourceId) {
|
||||
return Result.ok(sourceService.disable(
|
||||
sourceId, SaTokenUtil.getLoginAccount()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新启用已停用数据源。
|
||||
*
|
||||
* @param sourceId 数据源 ID
|
||||
* @return 启用后的数据源视图
|
||||
*/
|
||||
@PostMapping("/{sourceId}/enable")
|
||||
@SaCheckPermission("/api/v1/datacenterSource/save")
|
||||
public Result<DatacenterSourceView> enable(@PathVariable BigInteger sourceId) {
|
||||
return Result.ok(sourceService.enable(
|
||||
sourceId, SaTokenUtil.getLoginAccount()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新已纳管对象的 JDBC 元数据观测状态。
|
||||
*
|
||||
* @param sourceId 数据源 ID
|
||||
* @return 刷新后的数据源视图
|
||||
*/
|
||||
@PostMapping("/{sourceId}/metadata/refresh")
|
||||
@SaCheckPermission("/api/v1/datacenterSource/save")
|
||||
public Result<DatacenterSourceView> refreshMetadata(
|
||||
@PathVariable BigInteger sourceId) {
|
||||
return Result.ok(sourceService.refreshMetadata(
|
||||
sourceId, SaTokenUtil.getLoginAccount()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
package tech.easyflow.admin.controller.dataspace;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
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.web.jsonbody.JsonBody;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.dataspace.model.ConnectionDefinition;
|
||||
import tech.easyflow.dataspace.model.ConnectionView;
|
||||
import tech.easyflow.dataspace.model.ObjectView;
|
||||
import tech.easyflow.dataspace.provider.DataspaceProbe;
|
||||
import tech.easyflow.dataspace.service.DataspaceConnectionService;
|
||||
|
||||
/**
|
||||
* 数据空间物理连接管理端 API。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/dataspaceConnection")
|
||||
public class DataspaceConnectionController {
|
||||
|
||||
private final DataspaceConnectionService connectionService;
|
||||
|
||||
/**
|
||||
* 创建连接控制器。
|
||||
*
|
||||
* @param connectionService 连接服务
|
||||
*/
|
||||
public DataspaceConnectionController(DataspaceConnectionService connectionService) {
|
||||
this.connectionService = connectionService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询当前租户连接列表。
|
||||
*
|
||||
* @param keyword 搜索关键词
|
||||
* @return 连接列表
|
||||
*/
|
||||
@GetMapping("/list")
|
||||
@SaCheckPermission("/api/v1/dataspaceConnection/query")
|
||||
public Result<List<ConnectionView>> list(String keyword) {
|
||||
return Result.ok(connectionService.list(keyword));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取连接详情。
|
||||
*
|
||||
* @param id 连接 ID
|
||||
* @return 连接详情
|
||||
*/
|
||||
@GetMapping("/detail")
|
||||
@SaCheckPermission("/api/v1/dataspaceConnection/query")
|
||||
public Result<ConnectionView> detail(BigInteger id) {
|
||||
return Result.ok(connectionService.detail(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试候选或已保存连接。
|
||||
*
|
||||
* @param definition 连接定义
|
||||
* @return 测试结果
|
||||
*/
|
||||
@PostMapping("/test")
|
||||
@SaCheckPermission("/api/v1/dataspaceConnection/test")
|
||||
public Result<DataspaceProbe> test(
|
||||
@JsonBody(required = true, skipConvertError = false) ConnectionDefinition definition) {
|
||||
return Result.ok(connectionService.test(definition));
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建或更新连接。
|
||||
*
|
||||
* @param definition 连接定义
|
||||
* @return 保存后的连接
|
||||
*/
|
||||
@PostMapping("/save")
|
||||
@SaCheckPermission("/api/v1/dataspaceConnection/save")
|
||||
public Result<ConnectionView> save(
|
||||
@JsonBody(required = true, skipConvertError = false) ConnectionDefinition definition) {
|
||||
return Result.ok(connectionService.save(definition));
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用或禁用连接。
|
||||
*
|
||||
* @param request 状态变更请求
|
||||
* @return 变更后的连接
|
||||
*/
|
||||
@PostMapping("/status")
|
||||
@SaCheckPermission("/api/v1/dataspaceConnection/save")
|
||||
public Result<ConnectionView> status(
|
||||
@JsonBody(required = true, skipConvertError = false) StatusRequest request) {
|
||||
if (request == null || request.enabled() == null) {
|
||||
throw new BusinessException("连接状态不能为空");
|
||||
}
|
||||
return Result.ok(connectionService.setEnabled(request.id(), request.enabled()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询当前连接的对象树数据。
|
||||
*
|
||||
* @param connectionId 连接 ID
|
||||
* @param keyword Schema 或表名关键词
|
||||
* @return 对象列表
|
||||
*/
|
||||
@GetMapping("/objects")
|
||||
@SaCheckPermission("/api/v1/dataspaceConnection/query")
|
||||
public Result<List<ObjectView>> objects(BigInteger connectionId, String keyword) {
|
||||
return Result.ok(connectionService.objects(connectionId, keyword));
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新连接元数据。
|
||||
*
|
||||
* @param request 刷新请求
|
||||
* @return 新 revision 对象列表
|
||||
*/
|
||||
@PostMapping("/refreshMetadata")
|
||||
@SaCheckPermission("/api/v1/dataspaceConnection/metadata")
|
||||
public Result<List<ObjectView>> refreshMetadata(
|
||||
@JsonBody(required = true, skipConvertError = false) RefreshRequest request) {
|
||||
return Result.ok(connectionService.refreshMetadata(
|
||||
request.connectionId(), request.expectedRevision()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除未被引用的连接。
|
||||
*
|
||||
* @param id 连接 ID
|
||||
* @return 成功结果
|
||||
*/
|
||||
@PostMapping("/remove")
|
||||
@SaCheckPermission("/api/v1/dataspaceConnection/remove")
|
||||
public Result<Void> remove(
|
||||
@JsonBody(value = "id", required = true, skipConvertError = false) BigInteger id) {
|
||||
connectionService.remove(id);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 元数据刷新请求。
|
||||
*
|
||||
* @param connectionId 连接 ID
|
||||
* @param expectedRevision 期望 revision
|
||||
*/
|
||||
public record RefreshRequest(BigInteger connectionId, long expectedRevision) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接状态变更请求。
|
||||
*
|
||||
* @param id 连接 ID
|
||||
* @param enabled 是否启用
|
||||
*/
|
||||
public record StatusRequest(BigInteger id, Boolean enabled) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package tech.easyflow.admin.controller.dataspace;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
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.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.dataspace.model.DataspaceDefinition;
|
||||
import tech.easyflow.dataspace.model.DataspaceSummary;
|
||||
import tech.easyflow.dataspace.model.DataspaceView;
|
||||
import tech.easyflow.dataspace.service.DataspaceService;
|
||||
|
||||
/**
|
||||
* 虚拟数据空间管理端 API。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/dataspace")
|
||||
public class DataspaceController {
|
||||
|
||||
private final DataspaceService dataspaceService;
|
||||
|
||||
/**
|
||||
* 创建数据空间控制器。
|
||||
*
|
||||
* @param dataspaceService 数据空间服务
|
||||
*/
|
||||
public DataspaceController(DataspaceService dataspaceService) {
|
||||
this.dataspaceService = dataspaceService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询数据空间列表。
|
||||
*
|
||||
* @param keyword 搜索关键词
|
||||
* @return 数据空间摘要
|
||||
*/
|
||||
@GetMapping("/list")
|
||||
@SaCheckPermission("/api/v1/dataspace/query")
|
||||
public Result<List<DataspaceSummary>> list(String keyword) {
|
||||
return Result.ok(dataspaceService.list(keyword));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数据空间当前 revision 详情。
|
||||
*
|
||||
* @param id 数据空间 ID
|
||||
* @return 数据空间详情
|
||||
*/
|
||||
@GetMapping("/detail")
|
||||
@SaCheckPermission("/api/v1/dataspace/detail")
|
||||
public Result<DataspaceView> detail(BigInteger id) {
|
||||
return Result.ok(dataspaceService.detail(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存数据空间并生成新 revision。
|
||||
*
|
||||
* @param definition 数据空间定义
|
||||
* @return 保存后的详情
|
||||
*/
|
||||
@PostMapping("/save")
|
||||
@SaCheckPermission("/api/v1/dataspace/save")
|
||||
public Result<DataspaceView> save(
|
||||
@RequestBody DataspaceDefinition definition) {
|
||||
return Result.ok(dataspaceService.save(definition));
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用或禁用数据空间。
|
||||
*
|
||||
* @param request 状态变更请求
|
||||
* @return 成功结果
|
||||
*/
|
||||
@PostMapping("/status")
|
||||
@SaCheckPermission("/api/v1/dataspace/save")
|
||||
public Result<Void> status(
|
||||
@JsonBody(required = true, skipConvertError = false) StatusRequest request) {
|
||||
if (request == null || request.enabled() == null) {
|
||||
throw new BusinessException("数据空间状态不能为空");
|
||||
}
|
||||
dataspaceService.setEnabled(request.id(), request.enabled());
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 逻辑删除数据空间。
|
||||
*
|
||||
* @param id 数据空间 ID
|
||||
* @return 成功结果
|
||||
*/
|
||||
@PostMapping("/remove")
|
||||
@SaCheckPermission("/api/v1/dataspace/remove")
|
||||
public Result<Void> remove(
|
||||
@JsonBody(value = "id", required = true, skipConvertError = false) BigInteger id) {
|
||||
dataspaceService.remove(id);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据空间状态变更请求。
|
||||
*
|
||||
* @param id 数据空间 ID
|
||||
* @param enabled 是否启用
|
||||
*/
|
||||
public record StatusRequest(BigInteger id, Boolean enabled) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package tech.easyflow.admin.controller.dataspace;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
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.web.jsonbody.JsonBody;
|
||||
import tech.easyflow.dataspace.model.DataspaceExplainResult;
|
||||
import tech.easyflow.dataspace.model.DataspaceQueryRequest;
|
||||
import tech.easyflow.dataspace.model.DataspaceQueryResult;
|
||||
import tech.easyflow.dataspace.model.DataspaceSqlCompletionRequest;
|
||||
import tech.easyflow.dataspace.model.DataspaceSqlCompletionResult;
|
||||
import tech.easyflow.dataspace.service.DataspaceQueryService;
|
||||
|
||||
/**
|
||||
* 数据空间 SQL 工作台 Query、Explain、Complete 与 Cancel API。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/dataspaceSql")
|
||||
public class DataspaceSqlController {
|
||||
|
||||
private final DataspaceQueryService queryService;
|
||||
|
||||
/**
|
||||
* 创建 SQL 控制器。
|
||||
*
|
||||
* @param queryService 查询服务
|
||||
*/
|
||||
public DataspaceSqlController(DataspaceQueryService queryService) {
|
||||
this.queryService = queryService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行只读 SQL。
|
||||
*
|
||||
* @param request 查询请求
|
||||
* @return 查询结果与指标
|
||||
*/
|
||||
@PostMapping("/query")
|
||||
@SaCheckPermission("/api/v1/dataspaceSql/query")
|
||||
public Result<DataspaceQueryResult> query(
|
||||
@JsonBody(required = true, skipConvertError = false) DataspaceQueryRequest request) {
|
||||
return Result.ok(queryService.query(request));
|
||||
}
|
||||
|
||||
/**
|
||||
* 显式执行非 ANALYZE Explain。
|
||||
*
|
||||
* @param request Explain 请求
|
||||
* @return Explain 与索引信息
|
||||
*/
|
||||
@PostMapping("/explain")
|
||||
@SaCheckPermission("/api/v1/dataspaceSql/explain")
|
||||
public Result<DataspaceExplainResult> explain(
|
||||
@JsonBody(required = true, skipConvertError = false) DataspaceQueryRequest request) {
|
||||
return Result.ok(queryService.explain(request));
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回当前数据空间内的 Calcite SQL 补全候选。
|
||||
*
|
||||
* @param request 补全请求
|
||||
* @return 补全替换区间与候选
|
||||
*/
|
||||
@PostMapping("/complete")
|
||||
@SaCheckPermission("/api/v1/dataspaceSql/query")
|
||||
public Result<DataspaceSqlCompletionResult> complete(
|
||||
@JsonBody(required = true, skipConvertError = false)
|
||||
DataspaceSqlCompletionRequest request) {
|
||||
return Result.ok(queryService.complete(request));
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试取消当前节点查询。
|
||||
*
|
||||
* @param queryId 查询 ID
|
||||
* @return 是否找到并发起取消
|
||||
*/
|
||||
@PostMapping("/cancel")
|
||||
@SaCheckPermission("/api/v1/dataspaceSql/query")
|
||||
public Result<Boolean> cancel(
|
||||
@JsonBody(value = "queryId", required = true, skipConvertError = false) String queryId) {
|
||||
return Result.ok(queryService.cancel(queryId));
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,49 @@
|
||||
package tech.easyflow.admin.controller.job;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import org.quartz.CronExpression;
|
||||
import com.easyagents.flow.core.chain.Parameter;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import tech.easyflow.ai.easyagentsflow.service.WorkflowRunningParameterResolver;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
import tech.easyflow.ai.enums.PublishStatus;
|
||||
import tech.easyflow.ai.service.WorkflowService;
|
||||
import tech.easyflow.ai.service.WorkflowUsageAuthorizationService;
|
||||
import tech.easyflow.admin.model.SysJobWorkflowOptionView;
|
||||
import tech.easyflow.common.constant.enums.EnumJobStatus;
|
||||
import tech.easyflow.common.constant.enums.EnumJobType;
|
||||
import tech.easyflow.common.constant.enums.EnumMisfirePolicy;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||
import tech.easyflow.job.entity.SysJob;
|
||||
import tech.easyflow.job.job.JobConstant;
|
||||
import tech.easyflow.job.service.SysJobService;
|
||||
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.job.support.SysJobWorkflowReferenceSupport;
|
||||
import tech.easyflow.log.annotation.LogRecord;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceAction;
|
||||
import tech.easyflow.system.service.ResourceAccessService;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 系统任务表 控制层。
|
||||
@@ -30,52 +54,356 @@ import java.util.List;
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/sysJob")
|
||||
public class SysJobController extends BaseCurdController<SysJobService, SysJob> {
|
||||
public SysJobController(SysJobService service) {
|
||||
|
||||
/** 工作流服务。 */
|
||||
private final WorkflowService workflowService;
|
||||
|
||||
/** 工作流使用权限校验服务。 */
|
||||
private final WorkflowUsageAuthorizationService workflowUsageAuthorizationService;
|
||||
|
||||
/** 资源访问控制服务。 */
|
||||
private final ResourceAccessService resourceAccessService;
|
||||
|
||||
/** 工作流运行参数解析器。 */
|
||||
private final WorkflowRunningParameterResolver workflowRunningParameterResolver;
|
||||
|
||||
/** 与调度计算一致的 Cron 预览格式化器。 */
|
||||
private final DateTimeFormatter jobTimeFormatter;
|
||||
|
||||
/**
|
||||
* 创建定时任务控制器。
|
||||
*
|
||||
* @param service 定时任务服务
|
||||
* @param workflowService 工作流服务
|
||||
* @param workflowUsageAuthorizationService 工作流使用权限校验服务
|
||||
* @param resourceAccessService 资源访问控制服务
|
||||
* @param workflowRunningParameterResolver 工作流运行参数解析器
|
||||
* @param jobTimezone 定时任务业务时区
|
||||
*/
|
||||
public SysJobController(SysJobService service,
|
||||
WorkflowService workflowService,
|
||||
WorkflowUsageAuthorizationService workflowUsageAuthorizationService,
|
||||
ResourceAccessService resourceAccessService,
|
||||
WorkflowRunningParameterResolver workflowRunningParameterResolver,
|
||||
@Value("${easyflow.job.timezone:Asia/Shanghai}") String jobTimezone) {
|
||||
super(service);
|
||||
this.workflowService = workflowService;
|
||||
this.workflowUsageAuthorizationService = workflowUsageAuthorizationService;
|
||||
this.resourceAccessService = resourceAccessService;
|
||||
this.workflowRunningParameterResolver = workflowRunningParameterResolver;
|
||||
this.jobTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
|
||||
.withZone(ZoneId.of(jobTimezone));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取定时任务关键字搜索字段。
|
||||
*
|
||||
* @return 任务名称和备注属性
|
||||
*/
|
||||
@Override
|
||||
protected String[] getKeywordSearchProperties() {
|
||||
return new String[]{"jobName", "remark"};
|
||||
}
|
||||
|
||||
@GetMapping("/start")
|
||||
@SaCheckPermission("/api/v1/sysJob/save")
|
||||
@LogRecord("启动定时任务")
|
||||
public Result<Void> start(BigInteger id) {
|
||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||
SysJob job = requireExistingJob(id);
|
||||
validateWorkflowReference(job, account);
|
||||
service.startJob(id);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@GetMapping("/stop")
|
||||
@SaCheckPermission("/api/v1/sysJob/save")
|
||||
@LogRecord("停止定时任务")
|
||||
public Result<Void> stop(BigInteger id) {
|
||||
service.stopJob(id);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@GetMapping("/getNextTimes")
|
||||
public Result<List<String>> getNextTimes(String cronExpression) throws Exception{
|
||||
CronExpression ex = new CronExpression(cronExpression);
|
||||
List<String> times = new ArrayList<>();
|
||||
Date date = new Date();
|
||||
for (int i = 0; i < 5; i++) {
|
||||
Date next = ex.getNextValidTimeAfter(date);
|
||||
times.add(DateUtil.formatDateTime(next));
|
||||
date = next;
|
||||
@GetMapping("/trigger")
|
||||
@SaCheckPermission("/api/v1/sysJob/save")
|
||||
@LogRecord("立即执行定时任务")
|
||||
public Result<String> trigger(BigInteger id) {
|
||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||
SysJob job = requireExistingJob(id);
|
||||
validateWorkflowReference(job, account);
|
||||
return Result.ok(service.triggerNow(id));
|
||||
}
|
||||
return Result.ok(times);
|
||||
|
||||
@GetMapping("/getNextTimes")
|
||||
@SaCheckPermission("/api/v1/sysJob/save")
|
||||
public Result<List<String>> getNextTimes(String cronExpression) {
|
||||
return Result.ok(service.nextFireTimes(cronExpression, 5).stream()
|
||||
.map(Date::toInstant)
|
||||
.map(jobTimeFormatter::format)
|
||||
.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
@PostMapping("remove")
|
||||
@Transactional(propagation = Propagation.NOT_SUPPORTED)
|
||||
public Result<?> remove(@JsonBody(value = "id", required = true) Serializable id) {
|
||||
service.deleteJob(List.of(id));
|
||||
return Result.ok(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
@PostMapping("removeBatch")
|
||||
@Transactional(propagation = Propagation.NOT_SUPPORTED)
|
||||
public Result<?> removeBatch(
|
||||
@JsonBody(value = "ids", required = true) Collection<Serializable> ids) {
|
||||
if (ids == null || ids.isEmpty()) {
|
||||
return Result.fail("id不能为空");
|
||||
}
|
||||
service.deleteJob(ids);
|
||||
return Result.ok(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询当前账号可用于定时任务的工作流。
|
||||
*
|
||||
* @return 工作流安全选项
|
||||
*/
|
||||
@GetMapping("/workflowOptions")
|
||||
@SaCheckPermission("/api/v1/sysJob/save")
|
||||
public Result<List<SysJobWorkflowOptionView>> workflowOptions() {
|
||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||
List<SysJobWorkflowOptionView> options = workflowService.list(QueryWrapper.create()
|
||||
.eq(Workflow::getTenantId, account.getTenantId())
|
||||
.eq(Workflow::getPublishStatus, PublishStatus.PUBLISHED.getCode())
|
||||
.orderBy(Workflow::getModified, false))
|
||||
.stream()
|
||||
.filter(workflow -> Objects.equals(workflow.getTenantId(), account.getTenantId()))
|
||||
.filter(workflow -> workflow.getPublishedSnapshotJson() != null
|
||||
&& !workflow.getPublishedSnapshotJson().isEmpty())
|
||||
.filter(workflow -> resourceAccessService.canAccess(
|
||||
account,
|
||||
CategoryResourceType.WORKFLOW,
|
||||
workflow,
|
||||
ResourceAction.USE))
|
||||
.map(workflowService::toPublishedView)
|
||||
.filter(Objects::nonNull)
|
||||
.map(workflow -> new SysJobWorkflowOptionView(
|
||||
workflow.getId(),
|
||||
workflow.getTitle(),
|
||||
workflow.getDescription()))
|
||||
.toList();
|
||||
return Result.ok(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询定时任务所选工作流的运行参数。
|
||||
*
|
||||
* @param id 工作流 ID
|
||||
* @return 工作流运行参数
|
||||
* @throws BusinessException 工作流不存在或无运行权限时抛出
|
||||
*/
|
||||
@GetMapping("/workflowRunningParameters")
|
||||
@SaCheckPermission("/api/v1/sysJob/save")
|
||||
public Result<Map<String, Object>> workflowRunningParameters(BigInteger id) {
|
||||
Workflow workflow = workflowUsageAuthorizationService.requireUsableWorkflow(
|
||||
id,
|
||||
SaTokenUtil.getLoginAccount(),
|
||||
"工作流不存在、未发布或无权运行");
|
||||
Map<String, Object> result = workflowRunningParameterResolver.buildRunningParametersView(workflow);
|
||||
if (result == null) {
|
||||
throw new BusinessException("工作流参数配置无效,请检查工作流后重试");
|
||||
}
|
||||
return Result.ok(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Result onSaveOrUpdateBefore(SysJob entity, boolean isSave) {
|
||||
if (entity == null) {
|
||||
throw new BusinessException("定时任务不能为空");
|
||||
}
|
||||
LoginAccount loginUser = SaTokenUtil.getLoginAccount();
|
||||
SysJob effectiveEntity = entity;
|
||||
if (isSave) {
|
||||
// 新任务固定从 STOP 和第 0 代开始,禁止请求绕过启动协议。
|
||||
entity.setStatus(EnumJobStatus.STOP.getCode());
|
||||
entity.setScheduleGeneration(0L);
|
||||
commonFiled(entity,loginUser.getId(),loginUser.getTenantId(), loginUser.getDeptId());
|
||||
} else {
|
||||
SysJob existing = requireExistingJob(entity.getId());
|
||||
preserveServerControlledFields(entity, existing);
|
||||
effectiveEntity = mergeForValidation(entity, existing);
|
||||
entity.setModified(new Date());
|
||||
entity.setModifiedBy(loginUser.getId());
|
||||
}
|
||||
validateWorkflowReference(effectiveEntity, loginUser);
|
||||
validateCronExpression(effectiveEntity.getCronExpression());
|
||||
validateMisfirePolicy(effectiveEntity.getMisfirePolicy());
|
||||
return super.onSaveOrUpdateBefore(entity, isSave);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Result onRemoveBefore(Collection<Serializable> ids) {
|
||||
service.deleteJob(ids);
|
||||
return super.onRemoveBefore(ids);
|
||||
protected void onSaveOrUpdateAfter(SysJob entity, boolean isSave) {
|
||||
service.syncJob(entity.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
@PostMapping("update")
|
||||
public Result<?> update(@JsonBody SysJob entity) {
|
||||
Result<?> result = onSaveOrUpdateBefore(entity, false);
|
||||
if (result != null) return result;
|
||||
service.updateJobDefinition(entity);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验工作流类型任务引用的工作流可被当前用户运行。
|
||||
*
|
||||
* @param entity 待保存的定时任务
|
||||
* @param account 当前账号
|
||||
* @throws BusinessException 工作流不存在、参数非法或无运行权限时抛出
|
||||
*/
|
||||
private void validateWorkflowReference(SysJob entity, LoginAccount account) {
|
||||
if (entity == null
|
||||
|| !Integer.valueOf(EnumJobType.TINY_FLOW.getCode()).equals(entity.getJobType())) {
|
||||
return;
|
||||
}
|
||||
BigInteger workflowId = SysJobWorkflowReferenceSupport.requireWorkflowId(entity);
|
||||
Workflow workflow = workflowUsageAuthorizationService.requireUsableWorkflow(
|
||||
workflowId,
|
||||
account,
|
||||
"工作流不存在、未发布或无权运行");
|
||||
validateRequiredWorkflowParams(entity, workflow);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前租户内存在的定时任务。
|
||||
*
|
||||
* @param id 定时任务 ID
|
||||
* @return 已存在的定时任务
|
||||
* @throws BusinessException ID 缺失或任务不存在时抛出
|
||||
*/
|
||||
private SysJob requireExistingJob(BigInteger id) {
|
||||
if (id == null) {
|
||||
throw new BusinessException("定时任务ID不能为空");
|
||||
}
|
||||
SysJob existing = service.getById(id);
|
||||
if (existing == null) {
|
||||
throw new BusinessException("定时任务不存在");
|
||||
}
|
||||
return existing;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保留更新请求不能修改的服务端控制字段。
|
||||
*
|
||||
* @param entity 更新请求
|
||||
* @param existing 数据库中的定时任务
|
||||
*/
|
||||
private void preserveServerControlledFields(SysJob entity, SysJob existing) {
|
||||
entity.setTenantId(existing.getTenantId());
|
||||
entity.setDeptId(existing.getDeptId());
|
||||
entity.setCreated(existing.getCreated());
|
||||
entity.setCreatedBy(existing.getCreatedBy());
|
||||
entity.setStatus(existing.getStatus());
|
||||
entity.setScheduleGeneration(existing.getScheduleGeneration());
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并部分更新请求与原记录,生成用于权限和参数校验的有效任务状态。
|
||||
*
|
||||
* @param entity 更新请求
|
||||
* @param existing 数据库中的定时任务
|
||||
* @return 合并后的校验对象
|
||||
*/
|
||||
private SysJob mergeForValidation(SysJob entity, SysJob existing) {
|
||||
SysJob effective = new SysJob();
|
||||
effective.setJobType(entity.getJobType() == null
|
||||
? existing.getJobType()
|
||||
: entity.getJobType());
|
||||
effective.setJobParams(entity.getJobParams() == null
|
||||
? existing.getJobParams()
|
||||
: entity.getJobParams());
|
||||
effective.setCronExpression(entity.getCronExpression() == null
|
||||
? existing.getCronExpression()
|
||||
: entity.getCronExpression());
|
||||
effective.setMisfirePolicy(entity.getMisfirePolicy() == null
|
||||
? existing.getMisfirePolicy()
|
||||
: entity.getMisfirePolicy());
|
||||
return effective;
|
||||
}
|
||||
|
||||
private void validateMisfirePolicy(Integer misfirePolicy) {
|
||||
if (!Integer.valueOf(EnumMisfirePolicy.FIRE_ONCE_NOW.getCode()).equals(misfirePolicy)
|
||||
&& !Integer.valueOf(EnumMisfirePolicy.SKIP.getCode()).equals(misfirePolicy)) {
|
||||
throw new BusinessException("错过策略只支持恢复后补执行一次或跳过本次");
|
||||
}
|
||||
}
|
||||
|
||||
private void validateCronExpression(String cronExpression) {
|
||||
try {
|
||||
service.nextFireTimes(cronExpression, 1);
|
||||
} catch (RuntimeException exception) {
|
||||
throw new BusinessException(400, 1, "Cron 表达式无效", exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验定时任务已填写工作流的全部必填运行参数。
|
||||
*
|
||||
* @param entity 待保存定时任务
|
||||
* @param workflow 关联工作流
|
||||
* @throws BusinessException 工作流参数配置无效或必填值缺失时抛出
|
||||
*/
|
||||
private void validateRequiredWorkflowParams(SysJob entity, Workflow workflow) {
|
||||
List<Parameter> parameters =
|
||||
workflowRunningParameterResolver.resolveStartParameters(workflow.getContent());
|
||||
if (parameters == null) {
|
||||
throw new BusinessException("工作流参数配置无效,请检查工作流后重试");
|
||||
}
|
||||
Map<String, Object> jobParams = entity.getJobParams();
|
||||
Object rawWorkflowParams = jobParams == null
|
||||
? null
|
||||
: jobParams.get(JobConstant.WORKFLOW_PARAMS_KEY);
|
||||
Map<?, ?> workflowParams = rawWorkflowParams instanceof Map<?, ?> map
|
||||
? map
|
||||
: Map.of();
|
||||
for (Parameter parameter : parameters) {
|
||||
if (parameter == null || !parameter.isRequired()) {
|
||||
continue;
|
||||
}
|
||||
String name = parameter.getName();
|
||||
if (!StringUtils.hasText(name)) {
|
||||
throw new BusinessException("工作流存在无效必填参数配置,请检查工作流后重试");
|
||||
}
|
||||
if (!hasRequiredValue(workflowParams.get(name))) {
|
||||
String label = StringUtils.hasText(parameter.getFormLabel())
|
||||
? parameter.getFormLabel()
|
||||
: name;
|
||||
throw new BusinessException("工作流必填参数“" + label + "”不能为空");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断必填参数值是否有效。
|
||||
*
|
||||
* @param value 参数值
|
||||
* @return 非空时为 true
|
||||
*/
|
||||
private boolean hasRequiredValue(Object value) {
|
||||
if (value == null) {
|
||||
return false;
|
||||
}
|
||||
if (value instanceof CharSequence sequence) {
|
||||
return StringUtils.hasText(sequence);
|
||||
}
|
||||
if (value instanceof Collection<?> collection) {
|
||||
return !collection.isEmpty();
|
||||
}
|
||||
if (value instanceof Map<?, ?> map) {
|
||||
return !map.isEmpty();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,15 +1,29 @@
|
||||
package tech.easyflow.admin.controller.job;
|
||||
|
||||
import tech.easyflow.common.annotation.UsePermission;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import com.mybatisflex.core.paginate.Page;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.annotation.UsePermission;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.util.StringUtil;
|
||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.job.entity.SysJobLog;
|
||||
import tech.easyflow.job.service.SysJobLogService;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 系统任务日志 控制层。
|
||||
*
|
||||
@@ -20,16 +34,112 @@ import tech.easyflow.job.service.SysJobLogService;
|
||||
@RequestMapping("/api/v1/sysJobLog")
|
||||
@UsePermission(moduleName = "/api/v1/sysJob")
|
||||
public class SysJobLogController extends BaseCurdController<SysJobLogService, SysJobLog> {
|
||||
public SysJobLogController(SysJobLogService service) {
|
||||
private static final long DEFAULT_PAGE_SIZE = 10L;
|
||||
private static final long MAX_PAGE_SIZE = 100L;
|
||||
private static final DateTimeFormatter QUERY_TIME_FORMATTER =
|
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
private final ZoneId jobZoneId;
|
||||
|
||||
public SysJobLogController(
|
||||
SysJobLogService service,
|
||||
@Value("${easyflow.job.timezone:Asia/Shanghai}") String jobTimezone) {
|
||||
super(service);
|
||||
this.jobZoneId = ZoneId.of(jobTimezone);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造日志筛选条件,并追加计划触发时间和实际触发时间范围。
|
||||
*/
|
||||
@Override
|
||||
protected QueryWrapper buildQueryWrapper(HttpServletRequest request) {
|
||||
QueryWrapper queryWrapper = super.buildQueryWrapper(request);
|
||||
Date scheduledStart = parseQueryTime(
|
||||
request.getParameter("scheduledStart"), "计划触发开始时间");
|
||||
Date scheduledEnd = parseQueryTime(
|
||||
request.getParameter("scheduledEnd"), "计划触发结束时间");
|
||||
Date actualStart = parseQueryTime(
|
||||
request.getParameter("actualStart"), "实际触发开始时间");
|
||||
Date actualEnd = parseQueryTime(
|
||||
request.getParameter("actualEnd"), "实际触发结束时间");
|
||||
|
||||
validateTimeRange(scheduledStart, scheduledEnd, "计划触发时间");
|
||||
validateTimeRange(actualStart, actualEnd, "实际触发时间");
|
||||
if (scheduledStart != null) {
|
||||
queryWrapper.ge(SysJobLog::getScheduledFireTime, scheduledStart);
|
||||
}
|
||||
if (scheduledEnd != null) {
|
||||
queryWrapper.le(SysJobLog::getScheduledFireTime, scheduledEnd);
|
||||
}
|
||||
if (actualStart != null) {
|
||||
queryWrapper.ge(SysJobLog::getActualFireTime, actualStart);
|
||||
}
|
||||
if (actualEnd != null) {
|
||||
queryWrapper.le(SysJobLog::getActualFireTime, actualEnd);
|
||||
}
|
||||
return queryWrapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动刷新只读取当前第一页,不执行分页总数统计。
|
||||
*/
|
||||
@GetMapping("refresh")
|
||||
public Result<List<SysJobLog>> refresh(HttpServletRequest request, Long pageSize) {
|
||||
QueryWrapper queryWrapper = buildQueryWrapper(request);
|
||||
queryWrapper.orderBy(buildOrderBy(null, null, getDefaultOrderBy()));
|
||||
queryWrapper.limit(resolvePageSize(pageSize));
|
||||
return Result.ok(service.list(queryWrapper));
|
||||
}
|
||||
|
||||
/**
|
||||
* 最新计划触发记录优先,并用主键保证毫秒时间相同时顺序稳定。
|
||||
*/
|
||||
@Override
|
||||
protected String getDefaultOrderBy() {
|
||||
return "scheduled_fire_time desc, id desc";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Page<SysJobLog> queryPage(
|
||||
Page<SysJobLog> page, QueryWrapper queryWrapper) {
|
||||
page.setPageSize(resolvePageSize(page.getPageSize()));
|
||||
return super.queryPage(page, queryWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Result onSaveOrUpdateBefore(SysJobLog entity, boolean isSave) {
|
||||
LoginAccount loginUser = SaTokenUtil.getLoginAccount();
|
||||
if (isSave) {
|
||||
commonFiled(entity,loginUser.getId(),loginUser.getTenantId(), loginUser.getDeptId());
|
||||
throw new IllegalStateException("定时任务执行记录由系统维护,禁止外部写入");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Result onRemoveBefore(Collection<Serializable> ids) {
|
||||
service.requireTerminal(ids);
|
||||
return super.onRemoveBefore(ids);
|
||||
}
|
||||
|
||||
private long resolvePageSize(Long pageSize) {
|
||||
if (pageSize == null || pageSize < 1) {
|
||||
return DEFAULT_PAGE_SIZE;
|
||||
}
|
||||
return Math.min(pageSize, MAX_PAGE_SIZE);
|
||||
}
|
||||
|
||||
private Date parseQueryTime(String value, String fieldName) {
|
||||
if (!StringUtil.hasText(value)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
LocalDateTime dateTime = LocalDateTime.parse(value, QUERY_TIME_FORMATTER);
|
||||
return Date.from(dateTime.atZone(jobZoneId).toInstant());
|
||||
} catch (DateTimeParseException exception) {
|
||||
throw new BusinessException(
|
||||
400, 400, fieldName + "格式不正确", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateTimeRange(Date start, Date end, String fieldName) {
|
||||
if (start != null && end != null && start.after(end)) {
|
||||
throw new BusinessException(400, 400, fieldName + "范围不正确");
|
||||
}
|
||||
return super.onSaveOrUpdateBefore(entity, isSave);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
package tech.easyflow.admin.controller.skill;
|
||||
|
||||
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.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import tech.easyflow.common.annotation.UsePermission;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.util.SearchKeywordUtil;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||
import tech.easyflow.skill.entity.SkillCategory;
|
||||
import tech.easyflow.skill.service.SkillCategoryService;
|
||||
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.service.CategoryPermissionService;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Skill 分类管理控制器。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/skill/category")
|
||||
@UsePermission(moduleName = "/api/v1/skill")
|
||||
public class SkillCategoryController {
|
||||
|
||||
private static final Set<String> SORT_COLUMNS = Set.of(
|
||||
"id", "category_name", "parent_id", "level_no", "sort_no", "status", "created", "modified");
|
||||
|
||||
private final SkillCategoryService service;
|
||||
@javax.annotation.Resource
|
||||
private CategoryPermissionService categoryPermissionService;
|
||||
|
||||
/**
|
||||
* 创建 Skill 分类管理控制器。
|
||||
*
|
||||
* @param service Skill 分类服务
|
||||
*/
|
||||
public SkillCategoryController(SkillCategoryService service) {
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询当前用户可见的 Skill 分类。
|
||||
*
|
||||
* @param entity 查询条件
|
||||
* @param asTree 是否转树
|
||||
* @param sortKey 排序字段
|
||||
* @param sortType 排序方式
|
||||
* @return 可见分类列表
|
||||
*/
|
||||
@GetMapping("visibleList")
|
||||
@SaCheckPermission("/api/v1/skill/query")
|
||||
public Result<List<SkillCategory>> visibleList(SkillCategory entity, Boolean asTree, String sortKey, String sortType) {
|
||||
QueryWrapper queryWrapper = QueryWrapper.create()
|
||||
.eq(SkillCategory::getTenantId, currentAccount().getTenantId());
|
||||
if (entity != null) {
|
||||
queryWrapper.eq(SkillCategory::getId, entity.getId(), entity.getId() != null)
|
||||
.eq(SkillCategory::getParentId, entity.getParentId(), entity.getParentId() != null)
|
||||
.eq(SkillCategory::getLevelNo, entity.getLevelNo(), entity.getLevelNo() != null)
|
||||
.eq(SkillCategory::getStatus, entity.getStatus(), entity.getStatus() != null);
|
||||
if (entity.getCategoryName() != null && !entity.getCategoryName().isBlank()) {
|
||||
queryWrapper.and("category_name LIKE ?",
|
||||
SearchKeywordUtil.literalContainsPattern(entity.getCategoryName()));
|
||||
}
|
||||
}
|
||||
RoleCategoryAccessSnapshot access = categoryPermissionService.getCurrentAccess(CategoryResourceType.SKILL.getCode());
|
||||
queryWrapper.orderBy(resolveOrderBy(sortKey, sortType));
|
||||
List<SkillCategory> categories = service.list(queryWrapper);
|
||||
if (access.isRestricted()) {
|
||||
Set<BigInteger> visibleIds = new java.util.LinkedHashSet<>(access.getCategoryIds());
|
||||
categories.stream().filter(category -> access.getCategoryIds().contains(category.getId()))
|
||||
.map(SkillCategory::getAncestors).filter(value -> value != null && !value.isBlank())
|
||||
.flatMap(value -> java.util.Arrays.stream(value.split(",")))
|
||||
.map(String::trim).filter(value -> !value.isBlank() && !"0".equals(value))
|
||||
.map(BigInteger::new).forEach(visibleIds::add);
|
||||
categories = categories.stream().filter(category -> visibleIds.contains(category.getId())).toList();
|
||||
}
|
||||
return Result.ok(Boolean.FALSE.equals(asTree) ? categories : toTree(categories));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询当前租户完整分类管理树,包含停用分类。
|
||||
*
|
||||
* @return 分类树
|
||||
*/
|
||||
@GetMapping("tree")
|
||||
@SaCheckPermission("/api/v1/skill/category")
|
||||
public Result<List<SkillCategory>> tree() {
|
||||
List<SkillCategory> categories = service.list(QueryWrapper.create()
|
||||
.eq(SkillCategory::getTenantId, currentAccount().getTenantId())
|
||||
.orderBy("sort_no asc, id asc"));
|
||||
return Result.ok(toTree(categories));
|
||||
}
|
||||
|
||||
/**
|
||||
* 移动 Skill 分类到新的父级。
|
||||
*
|
||||
* @param id 分类 ID
|
||||
* @param parentId 新父级 ID,根分类为空
|
||||
* @return 更新结果
|
||||
*/
|
||||
@PostMapping("move")
|
||||
@SaCheckPermission("/api/v1/skill/category")
|
||||
public Result<?> move(
|
||||
@JsonBody(value = "id", required = true, skipConvertError = false) BigInteger id,
|
||||
@JsonBody(value = "parentId", skipConvertError = false) BigInteger parentId) {
|
||||
SkillCategory category = service.getOne(QueryWrapper.create()
|
||||
.eq(SkillCategory::getId, id)
|
||||
.eq(SkillCategory::getTenantId, currentAccount().getTenantId()));
|
||||
if (category == null) {
|
||||
throw new BusinessException(404, 404, "Skill 分类不存在");
|
||||
}
|
||||
category.setParentId(parentId);
|
||||
if (!service.updateById(category)) {
|
||||
throw new BusinessException(500, 500, "移动 Skill 分类失败,请稍后重试");
|
||||
}
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 Skill 分类。
|
||||
*
|
||||
* @param entity 分类
|
||||
* @return 保存结果
|
||||
*/
|
||||
@PostMapping("save")
|
||||
@SaCheckPermission("/api/v1/skill/category")
|
||||
public Result<?> save(@JsonBody(required = true, skipConvertError = false) SkillCategory entity) {
|
||||
if (entity != null) {
|
||||
entity.setId(null);
|
||||
entity.setTenantId(null);
|
||||
entity.setAncestors(null);
|
||||
entity.setLevelNo(null);
|
||||
entity.setCreated(null);
|
||||
entity.setCreatedBy(null);
|
||||
entity.setModified(null);
|
||||
entity.setModifiedBy(null);
|
||||
}
|
||||
if (!service.save(entity)) {
|
||||
throw new BusinessException(500, 500, "创建 Skill 分类失败,请稍后重试");
|
||||
}
|
||||
return Result.ok(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 Skill 分类。
|
||||
*
|
||||
* @param entity 分类
|
||||
* @return 更新结果
|
||||
*/
|
||||
@PostMapping("update")
|
||||
@SaCheckPermission("/api/v1/skill/category")
|
||||
public Result<?> update(@JsonBody(required = true, skipConvertError = false) SkillCategory entity) {
|
||||
if (entity != null) {
|
||||
entity.setTenantId(null);
|
||||
entity.setAncestors(null);
|
||||
entity.setLevelNo(null);
|
||||
entity.setCreated(null);
|
||||
entity.setCreatedBy(null);
|
||||
entity.setModified(null);
|
||||
entity.setModifiedBy(null);
|
||||
}
|
||||
if (entity == null || entity.getId() == null) {
|
||||
throw new BusinessException("Skill 分类 ID 不能为空");
|
||||
}
|
||||
if (!service.updateById(entity)) {
|
||||
throw new BusinessException(500, 500, "更新 Skill 分类失败,请稍后重试");
|
||||
}
|
||||
return Result.ok(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除 Skill 分类。
|
||||
*
|
||||
* @param id 分类 ID
|
||||
* @return 删除结果
|
||||
*/
|
||||
@PostMapping("remove")
|
||||
@SaCheckPermission("/api/v1/skill/category")
|
||||
public Result<?> remove(
|
||||
@JsonBody(value = "id", required = true, skipConvertError = false) Serializable id) {
|
||||
if (!service.removeById(id)) {
|
||||
throw new BusinessException(500, 500, "删除 Skill 分类失败,请稍后重试");
|
||||
}
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
private LoginAccount currentAccount() {
|
||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||
if (account == null || account.getId() == null || account.getTenantId() == null) {
|
||||
throw new BusinessException(401, 401, "未登录或登录态无效");
|
||||
}
|
||||
return account;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将分类排序参数收敛到固定字段白名单,禁止原始 SQL 片段进入查询。
|
||||
*
|
||||
* @param sortKey 排序字段
|
||||
* @param sortType 排序方向
|
||||
* @return 安全排序表达式
|
||||
*/
|
||||
String resolveOrderBy(String sortKey, String sortType) {
|
||||
String snake = sortKey == null ? "" : sortKey
|
||||
.replaceAll("([a-z0-9])([A-Z])", "$1_$2")
|
||||
.toLowerCase(Locale.ROOT);
|
||||
String column = SORT_COLUMNS.contains(snake) ? snake : "sort_no";
|
||||
String direction = "desc".equalsIgnoreCase(sortType) ? "desc" : "asc";
|
||||
return column + " " + direction + ("id".equals(column) ? "" : ", id asc");
|
||||
}
|
||||
|
||||
private List<SkillCategory> toTree(List<SkillCategory> categories) {
|
||||
Map<java.math.BigInteger, SkillCategory> byId = new LinkedHashMap<>();
|
||||
categories.forEach(category -> {
|
||||
category.setChildren(null);
|
||||
byId.put(category.getId(), category);
|
||||
});
|
||||
List<SkillCategory> roots = new java.util.ArrayList<>();
|
||||
for (SkillCategory category : categories) {
|
||||
SkillCategory parent = category.getParentId() == null ? null : byId.get(category.getParentId());
|
||||
if (parent == null) {
|
||||
roots.add(category);
|
||||
} else {
|
||||
parent.getChildren().add(category);
|
||||
}
|
||||
}
|
||||
return roots;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,755 @@
|
||||
package tech.easyflow.admin.controller.skill;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import cn.dev33.satoken.annotation.SaMode;
|
||||
import com.mybatisflex.core.paginate.Page;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.util.StreamUtils;
|
||||
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.bind.annotation.RequestPart;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import tech.easyflow.admin.controller.ai.support.AiResourceCreatorNameSupport;
|
||||
import tech.easyflow.admin.controller.skill.vo.SkillCopyRequest;
|
||||
import tech.easyflow.admin.controller.skill.vo.SkillDraftRequest;
|
||||
import tech.easyflow.admin.controller.skill.vo.SkillGitRepositoryPrepareRequest;
|
||||
import tech.easyflow.admin.controller.skill.vo.SkillGitRepositoryScanRequest;
|
||||
import tech.easyflow.admin.controller.skill.vo.SkillImportBatchResultView;
|
||||
import tech.easyflow.admin.controller.skill.vo.SkillView;
|
||||
import tech.easyflow.admin.controller.skill.vo.SkillToolBindingUpdateRequest;
|
||||
import tech.easyflow.admin.controller.skill.vo.SkillPublishStatusView;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalActionResult;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.util.SearchKeywordUtil;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||
import tech.easyflow.skill.entity.Skill;
|
||||
import tech.easyflow.skill.file.SkillFileContent;
|
||||
import tech.easyflow.skill.file.SkillFileNode;
|
||||
import tech.easyflow.skill.file.SkillFileRenameRequest;
|
||||
import tech.easyflow.skill.file.SkillFileSaveRequest;
|
||||
import tech.easyflow.skill.file.SkillFileService;
|
||||
import tech.easyflow.skill.imports.SkillExportRequest;
|
||||
import tech.easyflow.skill.imports.SkillExportArtifact;
|
||||
import tech.easyflow.skill.imports.SkillExportService;
|
||||
import tech.easyflow.skill.imports.SkillImportConfirmRequest;
|
||||
import tech.easyflow.skill.imports.SkillImportPreview;
|
||||
import tech.easyflow.skill.imports.SkillImportService;
|
||||
import tech.easyflow.skill.gitimport.SkillGitImportService;
|
||||
import tech.easyflow.skill.gitimport.SkillGitScanResult;
|
||||
import tech.easyflow.skill.publish.SkillPublishAppService;
|
||||
import tech.easyflow.skill.security.SkillVisibilityQueryHelper;
|
||||
import tech.easyflow.skill.service.SkillApprovalStateService;
|
||||
import tech.easyflow.skill.service.SkillService;
|
||||
import tech.easyflow.skill.service.SkillToolBindingService;
|
||||
import tech.easyflow.skill.service.SkillToolOptionQueryService;
|
||||
import tech.easyflow.skill.vo.SkillMcpToolManifestView;
|
||||
import tech.easyflow.skill.vo.SkillToolOptionPage;
|
||||
import tech.easyflow.skill.validation.SkillValidationResult;
|
||||
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 java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.math.BigInteger;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Skill 管理端 API,统一负责轻量查询、白名单写入、文件工作台和标准包导入导出。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/skill")
|
||||
public class SkillController {
|
||||
|
||||
private static final Set<String> PAGE_SORT_COLUMNS = Set.of(
|
||||
"id", "name", "display_name", "created", "modified", "publish_status");
|
||||
|
||||
private final SkillService skillService;
|
||||
private final SkillApprovalStateService skillApprovalStateService;
|
||||
private final SkillPublishAppService skillPublishAppService;
|
||||
private final SkillImportService skillImportService;
|
||||
private final SkillGitImportService skillGitImportService;
|
||||
private final SkillExportService skillExportService;
|
||||
private final SkillFileService skillFileService;
|
||||
private final SkillToolBindingService skillToolBindingService;
|
||||
private final SkillToolOptionQueryService skillToolOptionQueryService;
|
||||
private final ResourceAccessService resourceAccessService;
|
||||
private final CategoryPermissionService categoryPermissionService;
|
||||
private final SkillVisibilityQueryHelper visibilityQueryHelper;
|
||||
private final AiResourceCreatorNameSupport creatorNameSupport;
|
||||
|
||||
/**
|
||||
* 创建 Skill 管理控制器。
|
||||
*
|
||||
* @param skillService Skill 服务
|
||||
* @param skillApprovalStateService 审批状态服务
|
||||
* @param skillPublishAppService 发布服务
|
||||
* @param skillImportService 导入服务
|
||||
* @param skillGitImportService Git 仓库导入服务
|
||||
* @param skillExportService 导出服务
|
||||
* @param skillFileService 文件服务
|
||||
* @param skillToolBindingService Skill Tool 绑定服务
|
||||
* @param skillToolOptionQueryService Skill Tool 候选查询服务
|
||||
* @param resourceAccessService 资源权限服务
|
||||
* @param categoryPermissionService 分类权限服务
|
||||
* @param visibilityQueryHelper 可见性查询助手
|
||||
* @param creatorNameSupport 创建人名称助手
|
||||
*/
|
||||
public SkillController(SkillService skillService,
|
||||
SkillApprovalStateService skillApprovalStateService,
|
||||
SkillPublishAppService skillPublishAppService,
|
||||
SkillImportService skillImportService,
|
||||
SkillGitImportService skillGitImportService,
|
||||
SkillExportService skillExportService,
|
||||
SkillFileService skillFileService,
|
||||
SkillToolBindingService skillToolBindingService,
|
||||
SkillToolOptionQueryService skillToolOptionQueryService,
|
||||
ResourceAccessService resourceAccessService,
|
||||
CategoryPermissionService categoryPermissionService,
|
||||
SkillVisibilityQueryHelper visibilityQueryHelper,
|
||||
AiResourceCreatorNameSupport creatorNameSupport) {
|
||||
this.skillService = skillService;
|
||||
this.skillApprovalStateService = skillApprovalStateService;
|
||||
this.skillPublishAppService = skillPublishAppService;
|
||||
this.skillImportService = skillImportService;
|
||||
this.skillGitImportService = skillGitImportService;
|
||||
this.skillExportService = skillExportService;
|
||||
this.skillFileService = skillFileService;
|
||||
this.skillToolBindingService = skillToolBindingService;
|
||||
this.skillToolOptionQueryService = skillToolOptionQueryService;
|
||||
this.resourceAccessService = resourceAccessService;
|
||||
this.categoryPermissionService = categoryPermissionService;
|
||||
this.visibilityQueryHelper = visibilityQueryHelper;
|
||||
this.creatorNameSupport = creatorNameSupport;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询当前用户可读的 Skill 描述信息。
|
||||
*
|
||||
* @param pageNumber 页码
|
||||
* @param pageSize 每页数量
|
||||
* @param categoryId 分类 ID
|
||||
* @param categoryScope 分类范围,UNCATEGORIZED 表示未分类
|
||||
* @param name 名称关键词
|
||||
* @param displayName 展示名称关键词
|
||||
* @param keyword 名称、用途或创建人模糊关键词
|
||||
* @param publishStatus 发布状态
|
||||
* @param visibilityScope 使用范围
|
||||
* @param sortKey 排序字段
|
||||
* @param sortType 排序方向
|
||||
* @return 轻量分页结果
|
||||
*/
|
||||
@GetMapping("/page")
|
||||
@SaCheckPermission("/api/v1/skill/query")
|
||||
public Result<Page<SkillView>> page(Long pageNumber, Long pageSize, BigInteger categoryId, String categoryScope,
|
||||
String name, String displayName, String keyword, String publishStatus,
|
||||
String visibilityScope, String sortKey, String sortType) {
|
||||
long normalizedPage = pageNumber == null || pageNumber < 1 ? 1 : pageNumber;
|
||||
long normalizedSize = pageSize == null || pageSize < 1 ? 10 : Math.min(pageSize, 100);
|
||||
QueryWrapper query = descriptorQuery();
|
||||
visibilityQueryHelper.applyReadableAccess(query);
|
||||
if ("UNCATEGORIZED".equalsIgnoreCase(categoryScope)) {
|
||||
query.isNull("category_id");
|
||||
} else {
|
||||
query.eq("category_id", categoryId, categoryId != null);
|
||||
}
|
||||
query.eq("publish_status", publishStatus, hasText(publishStatus))
|
||||
.eq("visibility_scope", visibilityScope, hasText(visibilityScope));
|
||||
String effectiveKeyword = hasText(keyword) ? keyword : hasText(displayName) ? displayName : name;
|
||||
if (hasText(effectiveKeyword)) {
|
||||
String pattern = SearchKeywordUtil.literalContainsPattern(effectiveKeyword);
|
||||
query.and("(name LIKE ? ESCAPE '\\\\' OR display_name LIKE ? ESCAPE '\\\\' "
|
||||
+ "OR description LIKE ? ESCAPE '\\\\' OR EXISTS (SELECT 1 FROM tb_sys_account a "
|
||||
+ "WHERE a.id = tb_skill.created_by AND a.tenant_id = tb_skill.tenant_id "
|
||||
+ "AND (a.nickname LIKE ? ESCAPE '\\\\' OR a.login_name LIKE ? ESCAPE '\\\\'))) ",
|
||||
pattern, pattern, pattern, pattern, pattern);
|
||||
}
|
||||
query.orderBy(resolveSortColumn(sortKey) + ("asc".equalsIgnoreCase(sortType) ? " asc" : " desc"));
|
||||
Page<Skill> source = skillService.page(new Page<>(normalizedPage, normalizedSize), query);
|
||||
fillListState(source.getRecords());
|
||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||
boolean superAdmin = account != null && categoryPermissionService.isSuperAdmin(account);
|
||||
List<SkillView> records = source.getRecords().stream()
|
||||
.map(skill -> toPageView(skill, account, superAdmin)).toList();
|
||||
return Result.ok(new Page<>(records, source.getPageNumber(), source.getPageSize(), source.getTotalRow()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Skill 完整管理详情。
|
||||
*
|
||||
* @param id Skill ID
|
||||
* @return Skill 详情
|
||||
*/
|
||||
@GetMapping("/detail")
|
||||
@SaCheckPermission("/api/v1/skill/getDetail")
|
||||
public Result<SkillView> detail(BigInteger id) {
|
||||
Skill skill = skillService.getManagementDetail(id);
|
||||
fillListState(List.of(skill));
|
||||
return Result.ok(toView(skill));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询 Skill 可绑定的 Tool 候选。
|
||||
*
|
||||
* @param keyword 名称或描述关键词
|
||||
* @param toolType 类型过滤
|
||||
* @param pageNum 页码
|
||||
* @param pageSize 每页数量
|
||||
* @return 安全候选分页
|
||||
*/
|
||||
@GetMapping("/toolOptions")
|
||||
@SaCheckPermission(value = {"/api/v1/skill/save", "/api/v1/skill/update"}, mode = SaMode.OR)
|
||||
public Result<SkillToolOptionPage> toolOptions(String keyword, String toolType,
|
||||
Long pageNum, Long pageSize) {
|
||||
return Result.ok(skillToolOptionQueryService.page(keyword, toolType,
|
||||
pageNum == null ? 1 : pageNum, pageSize == null ? 20 : pageSize));
|
||||
}
|
||||
|
||||
/**
|
||||
* 按需读取指定 MCP 的脱敏 Tool 清单。
|
||||
*
|
||||
* @param mcpId MCP ID
|
||||
* @return MCP Tool 清单
|
||||
*/
|
||||
@GetMapping("/mcpTools")
|
||||
@SaCheckPermission(value = {"/api/v1/skill/save", "/api/v1/skill/update"}, mode = SaMode.OR)
|
||||
public Result<SkillMcpToolManifestView> mcpTools(BigInteger mcpId) {
|
||||
return Result.ok(skillToolOptionQueryService.mcpTools(mcpId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 原子替换 Skill 的全部平台 Tool 草稿绑定。
|
||||
*
|
||||
* @param request 白名单绑定请求
|
||||
* @return 服务端规范化的安全绑定摘要
|
||||
*/
|
||||
@PostMapping("/toolBinding/update")
|
||||
@SaCheckPermission(value = {"/api/v1/skill/save", "/api/v1/skill/update"}, mode = SaMode.OR)
|
||||
public Result<List<SkillView.ToolBindingView>> updateToolBindings(
|
||||
@JsonBody(required = true, skipConvertError = false) SkillToolBindingUpdateRequest request) {
|
||||
if (request == null || request.getSkillId() == null) {
|
||||
throw new BusinessException("Skill ID 不能为空");
|
||||
}
|
||||
List<tech.easyflow.skill.entity.SkillToolBinding> bindings = request.getBindings() == null
|
||||
? List.of() : request.getBindings().stream().map(SkillToolBindingUpdateRequest.Binding::toEntity).toList();
|
||||
return Result.ok(skillToolBindingService.replaceBindings(request.getSkillId(), bindings)
|
||||
.stream().map(SkillView.ToolBindingView::from).toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 Skill 草稿。
|
||||
*
|
||||
* @param request 草稿白名单请求
|
||||
* @return 创建后的 Skill
|
||||
*/
|
||||
@PostMapping("/save")
|
||||
@SaCheckPermission("/api/v1/skill/save")
|
||||
public Result<SkillView> save(@JsonBody(required = true, skipConvertError = false) SkillDraftRequest request) {
|
||||
if (request == null || request.id() != null) {
|
||||
throw new BusinessException("创建 Skill 时不能指定 ID");
|
||||
}
|
||||
return Result.ok(toView(skillService.saveDraft(request.toEntity())));
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 Skill 草稿。
|
||||
*
|
||||
* @param request 草稿白名单请求
|
||||
* @return 更新后的 Skill
|
||||
*/
|
||||
@PostMapping("/update")
|
||||
@SaCheckPermission("/api/v1/skill/update")
|
||||
public Result<SkillView> update(@JsonBody(required = true, skipConvertError = false) SkillDraftRequest request) {
|
||||
if (request == null || request.id() == null) {
|
||||
throw new BusinessException("Skill ID 不能为空");
|
||||
}
|
||||
return Result.ok(toView(skillService.updateDraft(request.toUpdateEntity())));
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制已有 Skill 为当前用户拥有的新草稿。
|
||||
*
|
||||
* @param request 复制请求
|
||||
* @return 新建的 Skill 草稿
|
||||
*/
|
||||
@PostMapping("/copy")
|
||||
@SaCheckPermission("/api/v1/skill/save")
|
||||
public Result<SkillView> copy(@JsonBody(required = true, skipConvertError = false) SkillCopyRequest request) {
|
||||
if (request == null) {
|
||||
throw new BusinessException("复制参数不能为空");
|
||||
}
|
||||
return Result.ok(toView(skillService.copyDraft(request.sourceId(), request.name(),
|
||||
request.displayName(), request.categoryId())));
|
||||
}
|
||||
|
||||
/**
|
||||
* 在展示发布确认前执行发布级全量校验。
|
||||
*
|
||||
* @param id Skill ID
|
||||
* @return 标准包结构化校验结果
|
||||
*/
|
||||
@PostMapping("/validatePublish")
|
||||
@SaCheckPermission("/api/v1/skill/submitPublishApproval")
|
||||
public Result<SkillValidationResult> validatePublish(
|
||||
@JsonBody(value = "id", required = true, skipConvertError = false) BigInteger id) {
|
||||
return Result.ok(skillService.validateSkill(id, true));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Skill 文件树。
|
||||
*
|
||||
* @param skillId Skill ID
|
||||
* @return 文件树
|
||||
*/
|
||||
@GetMapping("/file/tree")
|
||||
@SaCheckPermission("/api/v1/skill/getDetail")
|
||||
public Result<List<SkillFileNode>> fileTree(BigInteger skillId) {
|
||||
return Result.ok(skillFileService.tree(skillId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Skill 文本文件内容或二进制摘要。
|
||||
*
|
||||
* @param skillId Skill ID
|
||||
* @param path 包内路径
|
||||
* @return 文件内容
|
||||
*/
|
||||
@GetMapping("/file/content")
|
||||
@SaCheckPermission("/api/v1/skill/getDetail")
|
||||
public Result<SkillFileContent> fileContent(BigInteger skillId, String path) {
|
||||
return Result.ok(skillFileService.getContent(skillId, path));
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存已有文本文件。
|
||||
*
|
||||
* @param request 保存请求
|
||||
* @return 最新文件内容
|
||||
*/
|
||||
@PostMapping("/file/save")
|
||||
@SaCheckPermission("/api/v1/skill/file")
|
||||
public Result<SkillFileContent> saveFile(
|
||||
@JsonBody(required = true, skipConvertError = false) SkillFileSaveRequest request) {
|
||||
return Result.ok(skillFileService.saveContent(request));
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建文本文件。
|
||||
*
|
||||
* @param request 创建请求
|
||||
* @return 文件内容
|
||||
*/
|
||||
@PostMapping("/file/create")
|
||||
@SaCheckPermission("/api/v1/skill/file")
|
||||
public Result<SkillFileContent> createFile(
|
||||
@JsonBody(required = true, skipConvertError = false) SkillFileSaveRequest request) {
|
||||
return Result.ok(skillFileService.createTextFile(request));
|
||||
}
|
||||
|
||||
/**
|
||||
* 重命名文件。
|
||||
*
|
||||
* @param request 重命名请求
|
||||
* @return 最新文件内容
|
||||
*/
|
||||
@PostMapping("/file/rename")
|
||||
@SaCheckPermission("/api/v1/skill/file")
|
||||
public Result<SkillFileContent> renameFile(
|
||||
@JsonBody(required = true, skipConvertError = false) SkillFileRenameRequest request) {
|
||||
return Result.ok(skillFileService.renameFile(request));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除包内文件。
|
||||
*
|
||||
* @param skillId Skill ID
|
||||
* @param path 文件路径
|
||||
* @return 空结果
|
||||
*/
|
||||
@PostMapping("/file/delete")
|
||||
@SaCheckPermission("/api/v1/skill/file")
|
||||
public Result<Void> deleteFile(
|
||||
@JsonBody(value = "skillId", required = true, skipConvertError = false) BigInteger skillId,
|
||||
@JsonBody(value = "path", required = true, skipConvertError = false) String path,
|
||||
@JsonBody(value = "expectedContentHash", required = true, skipConvertError = false)
|
||||
String expectedContentHash) {
|
||||
skillFileService.deleteFile(skillId, path, expectedContentHash);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传任意包内二进制资源。
|
||||
*
|
||||
* @param skillId Skill ID
|
||||
* @param path 文件路径
|
||||
* @param expectedContentHash 目标文件预期内容哈希,替换时必填
|
||||
* @param file 上传文件
|
||||
* @return 文件摘要
|
||||
*/
|
||||
@PostMapping(value = "/file/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@SaCheckPermission("/api/v1/skill/file")
|
||||
public Result<SkillFileContent> uploadFile(BigInteger skillId,
|
||||
String path,
|
||||
String expectedContentHash,
|
||||
MultipartFile file) {
|
||||
return Result.ok(skillFileService.uploadResource(skillId, path, file, expectedContentHash));
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载包内文件。
|
||||
*
|
||||
* @param skillId Skill ID
|
||||
* @param path 文件路径
|
||||
* @param response HTTP 响应
|
||||
* @throws IOException 响应写入失败
|
||||
*/
|
||||
@GetMapping("/file/download")
|
||||
@SaCheckPermission("/api/v1/skill/getDetail")
|
||||
public void downloadFile(BigInteger skillId, String path, HttpServletResponse response) throws IOException {
|
||||
transferFile(skillId, path, response, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全预览包内文件;主动内容强制下载。
|
||||
*
|
||||
* @param skillId Skill ID
|
||||
* @param path 文件路径
|
||||
* @param response HTTP 响应
|
||||
* @throws IOException 响应写入失败
|
||||
*/
|
||||
@GetMapping("/file/preview")
|
||||
@SaCheckPermission("/api/v1/skill/getDetail")
|
||||
public void previewFile(BigInteger skillId, String path, HttpServletResponse response) throws IOException {
|
||||
transferFile(skillId, path, response, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量预览标准 ZIP 导入内容。
|
||||
*
|
||||
* @param files 导入文件
|
||||
* @param file 兼容单文件字段
|
||||
* @return 每个文件的 token 化预览
|
||||
*/
|
||||
@PostMapping(value = "/import/preview", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@SaCheckPermission("/api/v1/skill/import")
|
||||
public Result<List<SkillImportPreview>> importPreview(
|
||||
@RequestPart(value = "files", required = false) List<MultipartFile> files,
|
||||
@RequestPart(value = "file", required = false) MultipartFile file) {
|
||||
List<MultipartFile> uploads = new java.util.ArrayList<>(files == null ? List.of() : files);
|
||||
if (file != null) {
|
||||
uploads.add(file);
|
||||
}
|
||||
if (uploads.isEmpty()) {
|
||||
throw new BusinessException("请选择要导入的标准 Skill ZIP");
|
||||
}
|
||||
if (uploads.size() > SkillImportService.MAX_BATCH_SKILL_COUNT) {
|
||||
throw new BusinessException("单次最多选择 "
|
||||
+ SkillImportService.MAX_BATCH_SKILL_COUNT + " 个 Skill ZIP");
|
||||
}
|
||||
return Result.ok(skillImportService.previewBatch(uploads));
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫描 HTTPS Git 仓库中的标准 Skill 候选。
|
||||
*
|
||||
* @param request 仓库地址请求
|
||||
* @return 固定提交的候选列表
|
||||
*/
|
||||
@PostMapping("/import/repository/scan")
|
||||
@SaCheckPermission("/api/v1/skill/import")
|
||||
public Result<SkillGitScanResult> scanGitRepository(
|
||||
@JsonBody(required = true, skipConvertError = false) SkillGitRepositoryScanRequest request) {
|
||||
if (request == null) {
|
||||
throw new BusinessException("请输入 Git 仓库地址");
|
||||
}
|
||||
return Result.ok(skillGitImportService.scan(request.repositoryUrl()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 将选中的 Git Skill 候选转换为既有标准 ZIP 导入预览。
|
||||
*
|
||||
* @param request 扫描令牌与候选 ID
|
||||
* @return 与本地 ZIP 导入一致的预览列表
|
||||
*/
|
||||
@PostMapping("/import/repository/prepare")
|
||||
@SaCheckPermission("/api/v1/skill/import")
|
||||
public Result<List<SkillImportPreview>> prepareGitRepositoryImport(
|
||||
@JsonBody(required = true, skipConvertError = false) SkillGitRepositoryPrepareRequest request) {
|
||||
if (request == null) {
|
||||
throw new BusinessException("请选择要导入的 Git Skill");
|
||||
}
|
||||
return Result.ok(skillGitImportService.prepare(request.scanToken(), request.candidateIds()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用一次性 token 确认导入。
|
||||
*
|
||||
* @param request 导入确认请求
|
||||
* @return 导入后的 Skill
|
||||
*/
|
||||
@PostMapping("/import/confirm")
|
||||
@SaCheckPermission("/api/v1/skill/import")
|
||||
public Result<List<SkillView>> importConfirm(
|
||||
@JsonBody(required = true, skipConvertError = false) SkillImportConfirmRequest request) {
|
||||
return Result.ok(skillImportService.confirm(request).stream().map(this::toView).toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 独立确认多个已预检的标准 Skill ZIP;单包业务失败不回滚其他包。
|
||||
*
|
||||
* @param requests 导入确认请求,按预检 token 一一对应
|
||||
* @return 各包独立导入结果
|
||||
*/
|
||||
@PostMapping("/import/confirmBatch")
|
||||
@SaCheckPermission("/api/v1/skill/import")
|
||||
public Result<List<SkillImportBatchResultView>> importConfirmBatch(
|
||||
@JsonBody(required = true, skipConvertError = false) List<SkillImportConfirmRequest> requests) {
|
||||
if (requests == null || requests.isEmpty()) {
|
||||
throw new BusinessException("请选择要确认导入的 Skill");
|
||||
}
|
||||
if (requests.size() > SkillImportService.MAX_BATCH_SKILL_COUNT) {
|
||||
throw new BusinessException("单次最多确认导入 "
|
||||
+ SkillImportService.MAX_BATCH_SKILL_COUNT + " 个 Skill");
|
||||
}
|
||||
List<SkillImportBatchResultView> results = new java.util.ArrayList<>(requests.size());
|
||||
for (SkillImportConfirmRequest request : requests) {
|
||||
String token = request == null ? null : request.getImportToken();
|
||||
try {
|
||||
List<SkillView> skills = skillImportService.confirm(request).stream().map(this::toView).toList();
|
||||
results.add(SkillImportBatchResultView.succeeded(token, skills));
|
||||
} catch (BusinessException exception) {
|
||||
results.add(SkillImportBatchResultView.failed(token, exception.getMessage()));
|
||||
}
|
||||
}
|
||||
return Result.ok(results);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消导入并清理临时包。
|
||||
*
|
||||
* @param importToken 导入 token
|
||||
* @return 空结果
|
||||
*/
|
||||
@PostMapping("/import/cancel")
|
||||
@SaCheckPermission("/api/v1/skill/import")
|
||||
public Result<Void> importCancel(
|
||||
@JsonBody(value = "importToken", required = true, skipConvertError = false) String importToken) {
|
||||
skillImportService.cancel(importToken);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出标准 Skill ZIP。
|
||||
*
|
||||
* @param request 导出请求
|
||||
* @param response HTTP 响应
|
||||
*/
|
||||
@PostMapping("/export")
|
||||
@SaCheckPermission("/api/v1/skill/export")
|
||||
public void export(@JsonBody(required = true, skipConvertError = false) SkillExportRequest request,
|
||||
HttpServletResponse response) {
|
||||
if (request == null || request.getIds().isEmpty()) {
|
||||
throw new BusinessException("请选择要导出的 Skill");
|
||||
}
|
||||
if (request.getIds().size() > 100) {
|
||||
throw new BusinessException("单次最多导出 100 个 Skill");
|
||||
}
|
||||
try (SkillExportArtifact artifact = skillExportService.prepare(request.getIds())) {
|
||||
response.setContentType(artifact.getMediaType());
|
||||
response.setHeader("Content-Disposition", attachment(artifact.getFileName()));
|
||||
artifact.transferTo(output(response));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出单个标准 Skill 包。
|
||||
*
|
||||
* @param id Skill ID
|
||||
* @param response HTTP 响应
|
||||
*/
|
||||
@GetMapping("/export")
|
||||
@SaCheckPermission("/api/v1/skill/export")
|
||||
public void exportOne(BigInteger id, HttpServletResponse response) {
|
||||
if (id == null) {
|
||||
throw new BusinessException("Skill ID 不能为空");
|
||||
}
|
||||
writeExport(List.of(id), response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交发布审批。
|
||||
*
|
||||
* @param id Skill ID
|
||||
* @param applicationReason 发布说明
|
||||
* @return 审批实例 ID
|
||||
*/
|
||||
@PostMapping("/submitPublishApproval")
|
||||
@SaCheckPermission("/api/v1/skill/submitPublishApproval")
|
||||
public Result<BigInteger> submitPublishApproval(
|
||||
@JsonBody(value = "id", required = true, skipConvertError = false) BigInteger id,
|
||||
@JsonBody(value = "applicationReason", required = true, skipConvertError = false)
|
||||
String applicationReason) {
|
||||
return approvalResult(skillPublishAppService.submitPublishApproval(id, applicationReason),
|
||||
"已提交发布审批", "已直接发布");
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交下线审批。
|
||||
*
|
||||
* @param id Skill ID
|
||||
* @return 审批实例 ID
|
||||
*/
|
||||
@PostMapping("/submitOfflineApproval")
|
||||
@SaCheckPermission("/api/v1/skill/submitOfflineApproval")
|
||||
public Result<BigInteger> submitOfflineApproval(
|
||||
@JsonBody(value = "id", required = true, skipConvertError = false) BigInteger id) {
|
||||
return approvalResult(skillPublishAppService.submitOfflineApproval(id), "已提交下线审批", "已直接下线");
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交删除审批。
|
||||
*
|
||||
* @param id Skill ID
|
||||
* @return 审批实例 ID
|
||||
*/
|
||||
@PostMapping("/submitDeleteApproval")
|
||||
@SaCheckPermission("/api/v1/skill/submitDeleteApproval")
|
||||
public Result<BigInteger> submitDeleteApproval(
|
||||
@JsonBody(value = "id", required = true, skipConvertError = false) BigInteger id) {
|
||||
return approvalResult(skillPublishAppService.submitDeleteApproval(id), "已提交删除审批", "已直接删除");
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询 Skill 发布和审批派生状态。
|
||||
*
|
||||
* @param id Skill ID
|
||||
* @return 发布状态
|
||||
*/
|
||||
@GetMapping("/publish/status")
|
||||
@SaCheckPermission("/api/v1/skill/getDetail")
|
||||
public Result<SkillPublishStatusView> publishStatus(BigInteger id) {
|
||||
QueryWrapper query = descriptorQuery().eq(Skill::getId, id);
|
||||
visibilityQueryHelper.applyReadableAccess(query);
|
||||
Skill skill = skillService.getOne(query);
|
||||
if (skill == null) {
|
||||
throw new BusinessException(404, 404, "Skill 不存在");
|
||||
}
|
||||
fillListState(List.of(skill));
|
||||
return Result.ok(new SkillPublishStatusView(skill.getId(), skill.getPublishStatus(),
|
||||
skill.getApprovalPending(), skill.getCurrentApprovalActionType(), skill.getDisplayPublishStatus(),
|
||||
skill.getCurrentApprovalInstanceId()));
|
||||
}
|
||||
|
||||
private QueryWrapper descriptorQuery() {
|
||||
return QueryWrapper.create().select("id", "tenant_id", "dept_id", "category_id", "name", "display_name", "description",
|
||||
"visibility_scope", "package_hash", "snapshot_hash",
|
||||
"publish_status", "current_approval_instance_id", "created", "created_by", "modified", "modified_by");
|
||||
}
|
||||
|
||||
private void writeExport(List<BigInteger> ids, HttpServletResponse response) {
|
||||
try (SkillExportArtifact artifact = skillExportService.prepare(ids)) {
|
||||
response.setContentType(artifact.getMediaType());
|
||||
response.setHeader("Content-Disposition", attachment(artifact.getFileName()));
|
||||
artifact.transferTo(output(response));
|
||||
}
|
||||
}
|
||||
|
||||
private void fillListState(List<Skill> skills) {
|
||||
skillApprovalStateService.fillSkillApprovalState(skills);
|
||||
creatorNameSupport.fillSkillCreatorNames(skills);
|
||||
}
|
||||
|
||||
private SkillView toView(Skill skill) {
|
||||
boolean readable = resourceAccessService.canAccess(CategoryResourceType.SKILL, skill, ResourceAction.READ);
|
||||
boolean manageable = resourceAccessService.canAccess(CategoryResourceType.SKILL, skill, ResourceAction.MANAGE);
|
||||
return SkillView.from(skill, readable, manageable);
|
||||
}
|
||||
|
||||
private SkillView toPageView(Skill skill, LoginAccount account, boolean superAdmin) {
|
||||
boolean sameTenant = account != null && account.getTenantId() != null
|
||||
&& Objects.equals(account.getTenantId(), skill.getTenantId());
|
||||
boolean manageable = sameTenant && (superAdmin || Objects.equals(account.getId(), skill.getCreatedBy()));
|
||||
return SkillView.from(skill, sameTenant, manageable);
|
||||
}
|
||||
|
||||
private void transferFile(BigInteger skillId, String path, HttpServletResponse response, boolean preview) throws IOException {
|
||||
SkillFileContent content = skillFileService.getContent(skillId, path);
|
||||
String mediaType = content.getMediaType() == null ? MediaType.APPLICATION_OCTET_STREAM_VALUE : content.getMediaType();
|
||||
boolean inline = preview && isSafeInline(mediaType);
|
||||
response.setContentType(inline ? mediaType : MediaType.APPLICATION_OCTET_STREAM_VALUE);
|
||||
response.setHeader("X-Content-Type-Options", "nosniff");
|
||||
response.setHeader("Content-Security-Policy", "sandbox; default-src 'none'");
|
||||
response.setHeader("Content-Disposition", (inline ? "inline" : "attachment") + filenameParameter(fileName(path)));
|
||||
if (Boolean.TRUE.equals(content.getIsText())) {
|
||||
response.getOutputStream().write((content.getContent() == null ? "" : content.getContent())
|
||||
.getBytes(StandardCharsets.UTF_8));
|
||||
return;
|
||||
}
|
||||
try (InputStream inputStream = skillFileService.openResource(skillId, path)) {
|
||||
StreamUtils.copy(inputStream, response.getOutputStream());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isSafeInline(String mediaType) {
|
||||
String normalized = mediaType.toLowerCase(Locale.ROOT).split(";", 2)[0];
|
||||
return normalized.equals("application/pdf") || normalized.equals("text/plain")
|
||||
|| normalized.equals("text/markdown") || normalized.equals("image/png")
|
||||
|| normalized.equals("image/jpeg") || normalized.equals("image/gif")
|
||||
|| normalized.equals("image/webp") || normalized.equals("image/avif");
|
||||
}
|
||||
|
||||
private String resolveSortColumn(String sortKey) {
|
||||
if (!hasText(sortKey)) {
|
||||
return "modified";
|
||||
}
|
||||
String snake = sortKey.replaceAll("([a-z0-9])([A-Z])", "$1_$2").toLowerCase(Locale.ROOT);
|
||||
return PAGE_SORT_COLUMNS.contains(snake) ? snake : "modified";
|
||||
}
|
||||
|
||||
private String attachment(String fileName) {
|
||||
return "attachment" + filenameParameter(fileName);
|
||||
}
|
||||
|
||||
private String filenameParameter(String fileName) {
|
||||
String encoded = URLEncoder.encode(fileName, StandardCharsets.UTF_8).replace("+", "%20");
|
||||
return "; filename*=UTF-8''" + encoded;
|
||||
}
|
||||
|
||||
private String fileName(String path) {
|
||||
if (!hasText(path)) {
|
||||
return "resource.bin";
|
||||
}
|
||||
int index = path.lastIndexOf('/');
|
||||
return index < 0 ? path : path.substring(index + 1);
|
||||
}
|
||||
|
||||
private Result<BigInteger> approvalResult(ApprovalActionResult result, String approvalMessage, String directMessage) {
|
||||
return Result.ok(result.isApprovalRequired() ? approvalMessage : directMessage, result.getInstanceId());
|
||||
}
|
||||
|
||||
private java.io.OutputStream output(HttpServletResponse response) {
|
||||
try {
|
||||
return response.getOutputStream();
|
||||
} catch (IOException exception) {
|
||||
throw new BusinessException(500, 500, "创建 Skill 导出响应失败", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasText(String value) {
|
||||
return value != null && !value.isBlank();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package tech.easyflow.admin.controller.skill.vo;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
/**
|
||||
* Skill 复制请求白名单。
|
||||
*
|
||||
* @param sourceId 源 Skill ID
|
||||
* @param name 新 Skill 标准名称
|
||||
* @param displayName 新 Skill 展示名称
|
||||
* @param categoryId 目标分类 ID,可为空
|
||||
*/
|
||||
public record SkillCopyRequest(BigInteger sourceId,
|
||||
String name,
|
||||
String displayName,
|
||||
BigInteger categoryId) {
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package tech.easyflow.admin.controller.skill.vo;
|
||||
|
||||
import tech.easyflow.skill.entity.Skill;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
/**
|
||||
* Skill 草稿写入白名单,拒绝客户端覆盖租户、归属人、发布态、快照和 hash 等服务端字段。
|
||||
*
|
||||
* @param id Skill ID,创建时为空
|
||||
* @param categoryId 分类 ID
|
||||
* @param displayName 展示名称
|
||||
* @param skillContent SKILL.md 内容,仅创建时使用;已有草稿正文通过文件接口原子保存
|
||||
* @param visibilityScope 可见范围
|
||||
*/
|
||||
public record SkillDraftRequest(BigInteger id,
|
||||
BigInteger categoryId,
|
||||
String displayName,
|
||||
String skillContent,
|
||||
String visibilityScope) {
|
||||
|
||||
/**
|
||||
* 转换为仅包含可写字段的业务实体。
|
||||
*
|
||||
* @return Skill 草稿实体
|
||||
*/
|
||||
public Skill toEntity() {
|
||||
Skill skill = new Skill();
|
||||
skill.setId(id);
|
||||
skill.setCategoryId(categoryId);
|
||||
skill.setDisplayName(displayName);
|
||||
skill.setSkillContent(skillContent);
|
||||
skill.setVisibilityScope(visibilityScope);
|
||||
return skill;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为不包含 SKILL.md 正文的基础配置更新实体。
|
||||
*
|
||||
* @return Skill 基础配置实体
|
||||
*/
|
||||
public Skill toUpdateEntity() {
|
||||
Skill skill = toEntity();
|
||||
skill.setSkillContent(null);
|
||||
return skill;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package tech.easyflow.admin.controller.skill.vo;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 选中 Git Skill 候选的标准导入预览准备请求。
|
||||
*
|
||||
* @param scanToken 短期扫描令牌
|
||||
* @param candidateIds 选中的候选 ID
|
||||
*/
|
||||
public record SkillGitRepositoryPrepareRequest(String scanToken, List<String> candidateIds) {
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package tech.easyflow.admin.controller.skill.vo;
|
||||
|
||||
/**
|
||||
* Git 仓库 Skill 扫描请求。
|
||||
*
|
||||
* @param repositoryUrl HTTPS Git 仓库地址,可省略 .git 后缀
|
||||
*/
|
||||
public record SkillGitRepositoryScanRequest(String repositoryUrl) {
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package tech.easyflow.admin.controller.skill.vo;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 单个标准 Skill ZIP 的独立导入结果。
|
||||
*
|
||||
* @param importToken 预检 token
|
||||
* @param success 是否成功
|
||||
* @param message 失败原因,成功时为空
|
||||
* @param skills 导入成功的 Skill
|
||||
*/
|
||||
public record SkillImportBatchResultView(
|
||||
String importToken,
|
||||
boolean success,
|
||||
String message,
|
||||
List<SkillView> skills
|
||||
) {
|
||||
|
||||
/**
|
||||
* 构造成功结果。
|
||||
*
|
||||
* @param importToken 预检 token
|
||||
* @param skills 导入的 Skill
|
||||
* @return 成功结果
|
||||
*/
|
||||
public static SkillImportBatchResultView succeeded(String importToken, List<SkillView> skills) {
|
||||
return new SkillImportBatchResultView(importToken, true, null, List.copyOf(skills));
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造失败结果。
|
||||
*
|
||||
* @param importToken 预检 token
|
||||
* @param message 失败原因
|
||||
* @return 失败结果
|
||||
*/
|
||||
public static SkillImportBatchResultView failed(String importToken, String message) {
|
||||
return new SkillImportBatchResultView(importToken, false, message, List.of());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package tech.easyflow.admin.controller.skill.vo;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
/**
|
||||
* Skill 发布和审批派生状态。
|
||||
*
|
||||
* @param id Skill ID
|
||||
* @param publishStatus 真实发布状态
|
||||
* @param approvalPending 是否存在进行中审批
|
||||
* @param currentApprovalActionType 当前审批动作
|
||||
* @param displayPublishStatus 前端展示状态
|
||||
* @param currentApprovalInstanceId 当前审批实例 ID
|
||||
*/
|
||||
public record SkillPublishStatusView(BigInteger id,
|
||||
String publishStatus,
|
||||
Boolean approvalPending,
|
||||
String currentApprovalActionType,
|
||||
String displayPublishStatus,
|
||||
BigInteger currentApprovalInstanceId) {
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package tech.easyflow.admin.controller.skill.vo;
|
||||
|
||||
import tech.easyflow.skill.entity.SkillToolBinding;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Skill Tool 整组替换请求。
|
||||
*
|
||||
* <p>{@code @JsonBody} 当前由 Fastjson 1 完成转换,使用标准 JavaBean 可确保嵌套列表元素
|
||||
* 按声明类型转换,避免嵌套 record 被保留为 {@code JSONObject}。</p>
|
||||
*/
|
||||
public class SkillToolBindingUpdateRequest {
|
||||
|
||||
private BigInteger skillId;
|
||||
private List<Binding> bindings;
|
||||
|
||||
/** 创建空请求。 */
|
||||
public SkillToolBindingUpdateRequest() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 Skill Tool 绑定请求。
|
||||
*
|
||||
* @param skillId Skill ID
|
||||
* @param bindings 绑定引用
|
||||
*/
|
||||
public SkillToolBindingUpdateRequest(BigInteger skillId, List<Binding> bindings) {
|
||||
this.skillId = skillId;
|
||||
this.bindings = bindings;
|
||||
}
|
||||
|
||||
/** @return Skill ID */
|
||||
public BigInteger getSkillId() { return skillId; }
|
||||
/** @param skillId Skill ID */
|
||||
public void setSkillId(BigInteger skillId) { this.skillId = skillId; }
|
||||
/** @return 绑定引用 */
|
||||
public List<Binding> getBindings() { return bindings; }
|
||||
/** @param bindings 绑定引用 */
|
||||
public void setBindings(List<Binding> bindings) { this.bindings = bindings; }
|
||||
|
||||
/** 客户端允许提交的最小绑定字段。 */
|
||||
public static class Binding {
|
||||
|
||||
private String toolType;
|
||||
private BigInteger targetId;
|
||||
private Boolean hitlEnabled;
|
||||
private Integer sortNo;
|
||||
private String mcpToolManifestHash;
|
||||
|
||||
/** 创建空绑定。 */
|
||||
public Binding() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建最小 Tool 绑定。
|
||||
*
|
||||
* @param toolType Tool 类型
|
||||
* @param targetId 目标资源 ID
|
||||
* @param hitlEnabled 是否调用前确认
|
||||
* @param sortNo 排序号
|
||||
* @param mcpToolManifestHash MCP Tool 清单 hash
|
||||
*/
|
||||
public Binding(String toolType, BigInteger targetId, Boolean hitlEnabled,
|
||||
Integer sortNo, String mcpToolManifestHash) {
|
||||
this.toolType = toolType;
|
||||
this.targetId = targetId;
|
||||
this.hitlEnabled = hitlEnabled;
|
||||
this.sortNo = sortNo;
|
||||
this.mcpToolManifestHash = mcpToolManifestHash;
|
||||
}
|
||||
|
||||
/** @return Tool 类型 */
|
||||
public String getToolType() { return toolType; }
|
||||
/** @param toolType Tool 类型 */
|
||||
public void setToolType(String toolType) { this.toolType = toolType; }
|
||||
/** @return 目标资源 ID */
|
||||
public BigInteger getTargetId() { return targetId; }
|
||||
/** @param targetId 目标资源 ID */
|
||||
public void setTargetId(BigInteger targetId) { this.targetId = targetId; }
|
||||
/** @return 是否调用前确认 */
|
||||
public Boolean getHitlEnabled() { return hitlEnabled; }
|
||||
/** @param hitlEnabled 是否调用前确认 */
|
||||
public void setHitlEnabled(Boolean hitlEnabled) { this.hitlEnabled = hitlEnabled; }
|
||||
/** @return 排序号 */
|
||||
public Integer getSortNo() { return sortNo; }
|
||||
/** @param sortNo 排序号 */
|
||||
public void setSortNo(Integer sortNo) { this.sortNo = sortNo; }
|
||||
/** @return MCP Tool 清单 hash */
|
||||
public String getMcpToolManifestHash() { return mcpToolManifestHash; }
|
||||
/** @param mcpToolManifestHash MCP Tool 清单 hash */
|
||||
public void setMcpToolManifestHash(String mcpToolManifestHash) {
|
||||
this.mcpToolManifestHash = mcpToolManifestHash;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为领域绑定引用。
|
||||
*
|
||||
* @return 最小 Tool 绑定
|
||||
*/
|
||||
public SkillToolBinding toEntity() {
|
||||
SkillToolBinding value = new SkillToolBinding();
|
||||
value.setToolType(toolType);
|
||||
value.setTargetId(targetId);
|
||||
value.setHitlEnabled(hitlEnabled);
|
||||
value.setSortNo(sortNo);
|
||||
value.setMcpToolManifestHash(mcpToolManifestHash);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package tech.easyflow.admin.controller.skill.vo;
|
||||
|
||||
import com.easyagents.skill.util.SkillResources;
|
||||
import tech.easyflow.skill.entity.Skill;
|
||||
import tech.easyflow.skill.entity.SkillResource;
|
||||
import tech.easyflow.skill.entity.SkillToolBinding;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 管理端 Skill 安全视图。
|
||||
*
|
||||
* @param id Skill ID
|
||||
* @param categoryId 分类 ID
|
||||
* @param name 标准名称
|
||||
* @param displayName 展示名称
|
||||
* @param description 用途描述
|
||||
* @param visibilityScope 使用范围
|
||||
* @param packageHash 标准包哈希
|
||||
* @param snapshotHash 发布快照哈希
|
||||
* @param publishStatus 发布状态
|
||||
* @param currentApprovalInstanceId 当前审批实例 ID
|
||||
* @param approvalPending 是否审批中
|
||||
* @param currentApprovalActionType 当前审批动作
|
||||
* @param displayPublishStatus 展示状态
|
||||
* @param created 创建时间
|
||||
* @param modified 修改时间
|
||||
* @param createdByName 创建人昵称与账号
|
||||
* @param readable 是否可读
|
||||
* @param manageable 是否可管理
|
||||
* @param resources 资源摘要
|
||||
* @param toolBindings 平台 Tool 草稿绑定摘要
|
||||
* @param toolCount 实际 Tool 数
|
||||
* @param hasToolUpdate Tool 草稿是否与线上快照不同
|
||||
*/
|
||||
public record SkillView(BigInteger id,
|
||||
BigInteger categoryId,
|
||||
String name,
|
||||
String displayName,
|
||||
String description,
|
||||
String visibilityScope,
|
||||
String packageHash,
|
||||
String snapshotHash,
|
||||
String publishStatus,
|
||||
BigInteger currentApprovalInstanceId,
|
||||
Boolean approvalPending,
|
||||
String currentApprovalActionType,
|
||||
String displayPublishStatus,
|
||||
Date created,
|
||||
Date modified,
|
||||
String createdByName,
|
||||
boolean readable,
|
||||
boolean manageable,
|
||||
List<ResourceView> resources,
|
||||
List<ToolBindingView> toolBindings,
|
||||
int toolCount,
|
||||
boolean hasToolUpdate) {
|
||||
|
||||
/**
|
||||
* 从领域实体构造管理端视图。
|
||||
*
|
||||
* @param skill Skill 实体
|
||||
* @param readable 是否可读
|
||||
* @param manageable 是否可管理
|
||||
* @return 管理端视图
|
||||
*/
|
||||
public static SkillView from(Skill skill, boolean readable, boolean manageable) {
|
||||
List<ResourceView> resources = skill.getResources() == null ? null
|
||||
: skill.getResources().stream().map(ResourceView::from).toList();
|
||||
List<ToolBindingView> toolBindings = skill.getToolBindings() == null ? null
|
||||
: skill.getToolBindings().stream().map(ToolBindingView::from).toList();
|
||||
int toolCount = skill.getToolBindings() == null ? 0 : skill.getToolBindings().stream()
|
||||
.mapToInt(binding -> "MCP".equalsIgnoreCase(binding.getToolType())
|
||||
? Math.max(0, binding.getMcpToolCount() == null ? 0 : binding.getMcpToolCount()) : 1)
|
||||
.sum();
|
||||
return new SkillView(skill.getId(), skill.getCategoryId(), skill.getName(), skill.getDisplayName(),
|
||||
skill.getDescription(), skill.getVisibilityScope(), skill.getPackageHash(),
|
||||
skill.getSnapshotHash(), skill.getPublishStatus(), skill.getCurrentApprovalInstanceId(),
|
||||
skill.getApprovalPending(), skill.getCurrentApprovalActionType(), skill.getDisplayPublishStatus(),
|
||||
skill.getCreated(), skill.getModified(), skill.getCreatedByName(), readable, manageable, resources,
|
||||
toolBindings, toolCount, hasToolUpdate(skill));
|
||||
}
|
||||
|
||||
/**
|
||||
* Skill 包内资源摘要。
|
||||
*
|
||||
* @param id 资源 ID
|
||||
* @param path 标准相对路径
|
||||
* @param kind 按路径派生的语义类型
|
||||
* @param mediaType 媒体类型
|
||||
* @param isText 是否文本
|
||||
* @param contentHash 内容哈希
|
||||
* @param size 字节数
|
||||
*/
|
||||
public record ResourceView(BigInteger id, String path, String kind, String mediaType,
|
||||
Boolean isText, String contentHash, Long size) {
|
||||
|
||||
/**
|
||||
* 转换资源实体。
|
||||
*
|
||||
* @param resource 资源实体
|
||||
* @return 资源摘要
|
||||
*/
|
||||
public static ResourceView from(SkillResource resource) {
|
||||
String path = resource.getNormalizedPath();
|
||||
return new ResourceView(resource.getId(), path, SkillResources.classify(path).name(),
|
||||
resource.getMediaType(), resource.getIsText(), resource.getContentHash(), resource.getSize());
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean hasToolUpdate(Skill skill) {
|
||||
if (skill.getToolBindings() == null) {
|
||||
return false;
|
||||
}
|
||||
Object published = skill.getPublishedToolBindingsJson() == null
|
||||
? null : skill.getPublishedToolBindingsJson().get("bindings");
|
||||
List<String> currentKeys = skill.getToolBindings().stream().map(SkillView::bindingKey).toList();
|
||||
if (!(published instanceof List<?> list)) {
|
||||
return !currentKeys.isEmpty();
|
||||
}
|
||||
List<String> publishedKeys = list.stream().map(item -> {
|
||||
if (!(item instanceof java.util.Map<?, ?> map)) {
|
||||
return "INVALID";
|
||||
}
|
||||
return String.valueOf(map.get("toolType")) + ":" + map.get("targetId") + ":"
|
||||
+ Boolean.TRUE.equals(map.get("hitlEnabled")) + ":" + map.get("mcpToolManifestHash");
|
||||
}).toList();
|
||||
return !currentKeys.equals(publishedKeys);
|
||||
}
|
||||
|
||||
private static String bindingKey(SkillToolBinding binding) {
|
||||
return binding.getToolType() + ":" + binding.getTargetId() + ":"
|
||||
+ Boolean.TRUE.equals(binding.getHitlEnabled()) + ":" + binding.getMcpToolManifestHash();
|
||||
}
|
||||
|
||||
/**
|
||||
* Skill 平台 Tool 草稿绑定安全摘要。
|
||||
*/
|
||||
public record ToolBindingView(BigInteger id, String toolType, BigInteger targetId,
|
||||
Boolean hitlEnabled, Integer mcpToolCount,
|
||||
String mcpToolManifestHash, Integer sortNo,
|
||||
java.util.Map<String, Object> resourceSummary) {
|
||||
/** @param binding 绑定实体 @return 安全摘要 */
|
||||
public static ToolBindingView from(SkillToolBinding binding) {
|
||||
return new ToolBindingView(binding.getId(), binding.getToolType(), binding.getTargetId(),
|
||||
binding.getHitlEnabled(), binding.getMcpToolCount(), binding.getMcpToolManifestHash(),
|
||||
binding.getSortNo(), binding.getResourceSummary());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,8 @@ 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.admin.model.SystemFormOptionsView;
|
||||
import tech.easyflow.admin.service.system.SystemFormOptionService;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalAssigneeOptionVo;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalFlowDetailVo;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalFlowPageVo;
|
||||
@@ -36,6 +38,20 @@ public class ApprovalFlowController {
|
||||
|
||||
@Resource
|
||||
private ApprovalAssigneeService approvalAssigneeService;
|
||||
@Resource
|
||||
private SystemFormOptionService systemFormOptionService;
|
||||
|
||||
/**
|
||||
* 查询审批流程配置所需的资源范围选项。
|
||||
*
|
||||
* @return 非 Bot 分类和部门树
|
||||
*/
|
||||
@GetMapping("/resourceScopeOptions")
|
||||
@SaCheckPermission("/api/v1/approvalFlow/save")
|
||||
public Result<SystemFormOptionsView.ApprovalResourceScopeOptions> resourceScopeOptions() {
|
||||
assertSuperAdmin();
|
||||
return Result.ok(systemFormOptionService.approvalResourceScopeOptions());
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询审批流程。
|
||||
@@ -102,6 +118,9 @@ public class ApprovalFlowController {
|
||||
@SaCheckPermission("/api/v1/approvalFlow/save")
|
||||
public Result<BigInteger> save(@JsonBody ApprovalFlowDetailVo request) {
|
||||
assertSuperAdmin();
|
||||
systemFormOptionService.validateApprovalScopes(
|
||||
request == null ? null : request.getResourceType(),
|
||||
request == null ? null : request.getScopes());
|
||||
BigInteger operatorId = SaTokenUtil.getLoginAccount().getId();
|
||||
return Result.ok(approvalFlowService.saveFlow(request, operatorId));
|
||||
}
|
||||
@@ -116,6 +135,9 @@ public class ApprovalFlowController {
|
||||
@SaCheckPermission("/api/v1/approvalFlow/save")
|
||||
public Result<Void> update(@JsonBody ApprovalFlowDetailVo request) {
|
||||
assertSuperAdmin();
|
||||
systemFormOptionService.validateApprovalScopes(
|
||||
request == null ? null : request.getResourceType(),
|
||||
request == null ? null : request.getScopes());
|
||||
BigInteger operatorId = SaTokenUtil.getLoginAccount().getId();
|
||||
approvalFlowService.updateFlow(request, operatorId);
|
||||
return Result.ok();
|
||||
|
||||
@@ -3,15 +3,24 @@ package tech.easyflow.admin.controller.system;
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.hutool.crypto.digest.BCrypt;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.mybatisflex.core.paginate.Page;
|
||||
import com.mybatisflex.core.query.QueryCondition;
|
||||
import com.mybatisflex.core.query.QueryMethods;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import tech.easyflow.auth.entity.EncryptedCredentialDTO;
|
||||
import tech.easyflow.auth.service.AuthCredentialKeyService;
|
||||
import tech.easyflow.auth.service.AuthService;
|
||||
import tech.easyflow.common.constant.Constants;
|
||||
import tech.easyflow.common.constant.enums.EnumAccountType;
|
||||
import tech.easyflow.common.constant.enums.EnumDataStatus;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
@@ -21,18 +30,33 @@ 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.admin.controller.system.vo.SysAccountProfileVo;
|
||||
import tech.easyflow.admin.model.SystemFormOptionsView;
|
||||
import tech.easyflow.admin.service.system.SystemFormOptionService;
|
||||
import tech.easyflow.system.entity.SysAccount;
|
||||
import tech.easyflow.system.entity.SysRole;
|
||||
import tech.easyflow.system.entity.vo.SysAccountBatchActionResultVo;
|
||||
import tech.easyflow.system.entity.vo.SysAccountImportResultVo;
|
||||
import tech.easyflow.system.service.SysAccountService;
|
||||
import tech.easyflow.system.service.SysRoleService;
|
||||
import tech.easyflow.system.util.SysPasswordPolicy;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import javax.annotation.Resource;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigInteger;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static tech.easyflow.system.entity.table.SysAccountRoleTableDef.SYS_ACCOUNT_ROLE;
|
||||
import static tech.easyflow.system.entity.table.SysAccountTableDef.SYS_ACCOUNT;
|
||||
import static tech.easyflow.system.entity.table.SysRoleTableDef.SYS_ROLE;
|
||||
|
||||
/**
|
||||
* 用户表 控制层。
|
||||
@@ -43,22 +67,123 @@ import java.util.List;
|
||||
@RestController("sysAccountController")
|
||||
@RequestMapping("/api/v1/sysAccount")
|
||||
public class SysAccountController extends BaseCurdController<SysAccountService, SysAccount> {
|
||||
public SysAccountController(SysAccountService service) {
|
||||
private static final String ACCOUNT_SEARCH_KEYWORD_PARAM = "keyword";
|
||||
private static final String SUPER_ADMIN_HOME_PATH = "/dashboard/workspace";
|
||||
private static final String USER_HOME_PATH = "/ai/agent-chat";
|
||||
|
||||
private final AuthCredentialKeyService credentialKeyService;
|
||||
private final SysRoleService sysRoleService;
|
||||
private final SystemFormOptionService systemFormOptionService;
|
||||
@Resource
|
||||
private AuthService authService;
|
||||
|
||||
/**
|
||||
* 创建用户管理控制器。
|
||||
*
|
||||
* @param service 用户服务
|
||||
* @param credentialKeyService 凭证密钥服务
|
||||
* @param sysRoleService 角色服务
|
||||
* @param systemFormOptionService 用户表单安全选项服务
|
||||
*/
|
||||
public SysAccountController(SysAccountService service,
|
||||
AuthCredentialKeyService credentialKeyService,
|
||||
SysRoleService sysRoleService,
|
||||
SystemFormOptionService systemFormOptionService) {
|
||||
super(service);
|
||||
this.credentialKeyService = credentialKeyService;
|
||||
this.sysRoleService = sysRoleService;
|
||||
this.systemFormOptionService = systemFormOptionService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造账号列表查询条件。
|
||||
*
|
||||
* <p>统一关键字同时匹配账号、昵称、电话、邮件和已关联角色名称。角色条件使用相关
|
||||
* {@code EXISTS} 子查询,避免多角色关联导致分页记录重复。</p>
|
||||
*
|
||||
* @param request 当前 HTTP 请求
|
||||
* @return 账号列表查询条件
|
||||
*/
|
||||
@Override
|
||||
protected QueryWrapper buildQueryWrapper(HttpServletRequest request) {
|
||||
String keyword = request.getParameter(ACCOUNT_SEARCH_KEYWORD_PARAM);
|
||||
if (!StringUtil.hasText(keyword)) {
|
||||
return super.buildQueryWrapper(request);
|
||||
}
|
||||
|
||||
String likePattern = buildLiteralContainsPattern(normalizeSearchKeyword(keyword));
|
||||
QueryWrapper roleExistsQuery = QueryMethods.selectOne()
|
||||
.from(SYS_ACCOUNT_ROLE)
|
||||
.innerJoin(SYS_ROLE)
|
||||
.on(SYS_ACCOUNT_ROLE.ROLE_ID.eq(SYS_ROLE.ID))
|
||||
.where(SYS_ACCOUNT_ROLE.ACCOUNT_ID.eq(SYS_ACCOUNT.ID))
|
||||
// 关联角色必须与账号属于同一租户,避免异常关系数据跨租户命中。
|
||||
.and(SYS_ROLE.TENANT_ID.eq(SYS_ACCOUNT.TENANT_ID))
|
||||
.and(SYS_ROLE.ROLE_NAME.likeRaw(likePattern));
|
||||
QueryCondition keywordCondition = SYS_ACCOUNT.LOGIN_NAME.likeRaw(likePattern)
|
||||
.or(SYS_ACCOUNT.NICKNAME.likeRaw(likePattern))
|
||||
.or(SYS_ACCOUNT.MOBILE.likeRaw(likePattern))
|
||||
.or(SYS_ACCOUNT.EMAIL.likeRaw(likePattern))
|
||||
.or(QueryMethods.exists(roleExistsQuery));
|
||||
|
||||
return super.buildQueryWrapper(request).and(keywordCondition);
|
||||
}
|
||||
|
||||
@Override
|
||||
@LogRecord("分页查询")
|
||||
protected Page<SysAccount> queryPage(Page<SysAccount> page, QueryWrapper queryWrapper) {
|
||||
return service.getMapper().paginateWithRelations(page, queryWrapper);
|
||||
Page<SysAccount> result = service.getMapper().paginateWithRelations(page, queryWrapper);
|
||||
fillRoleNames(result.getRecords());
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按当前分页内的角色 ID 批量补全角色名称。
|
||||
*
|
||||
* @param accounts 当前页账号
|
||||
*/
|
||||
private void fillRoleNames(List<SysAccount> accounts) {
|
||||
if (accounts == null || accounts.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Set<BigInteger> roleIds = accounts.stream()
|
||||
.map(SysAccount::getRoleIds)
|
||||
.filter(java.util.Objects::nonNull)
|
||||
.flatMap(Collection::stream)
|
||||
.filter(java.util.Objects::nonNull)
|
||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
if (roleIds.isEmpty()) {
|
||||
accounts.forEach(account -> account.setRoleNames(List.of()));
|
||||
return;
|
||||
}
|
||||
|
||||
Map<BigInteger, String> roleNameMap = sysRoleService.listByIds(roleIds).stream()
|
||||
.filter(role -> role.getId() != null && StringUtil.hasText(role.getRoleName()))
|
||||
.collect(Collectors.toMap(
|
||||
SysRole::getId,
|
||||
SysRole::getRoleName,
|
||||
(first, ignored) -> first
|
||||
));
|
||||
accounts.forEach(account -> {
|
||||
List<BigInteger> accountRoleIds = account.getRoleIds();
|
||||
if (accountRoleIds == null || accountRoleIds.isEmpty()) {
|
||||
account.setRoleNames(List.of());
|
||||
return;
|
||||
}
|
||||
List<String> roleNames = accountRoleIds.stream()
|
||||
.map(roleNameMap::get)
|
||||
.filter(StringUtil::hasText)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
account.setRoleNames(roleNames);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Result onSaveOrUpdateBefore(SysAccount entity, boolean isSave) {
|
||||
systemFormOptionService.validateAccountReferences(entity);
|
||||
LoginAccount loginUser = SaTokenUtil.getLoginAccount();
|
||||
BigInteger tenantId = loginUser.getTenantId();
|
||||
if (isSave) {
|
||||
commonFiled(entity, loginUser.getId(), tenantId, loginUser.getDeptId());
|
||||
// 查询用户名是否存在
|
||||
// long count = Db.selectCount(SqlPrepare.COUNT_ACCOUNT_BY_UNI_KEY, entity.getLoginName(), tenantId);
|
||||
QueryWrapper w = QueryWrapper.create();
|
||||
@@ -67,7 +192,11 @@ public class SysAccountController extends BaseCurdController<SysAccountService,
|
||||
if (count > 0) {
|
||||
return Result.fail(1, "用户名已存在");
|
||||
}
|
||||
String password = entity.getPassword();
|
||||
Result<?> roleValidation = validateCreateRoles(entity);
|
||||
if (roleValidation != null) {
|
||||
return roleValidation;
|
||||
}
|
||||
String password = decryptInitialPassword(entity.getPasswordCredential());
|
||||
if (!StringUtil.hasText(password)) {
|
||||
return Result.fail(1, "密码不能为空");
|
||||
}
|
||||
@@ -98,6 +227,34 @@ public class SysAccountController extends BaseCurdController<SysAccountService,
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询账号表单所需的部门、角色和岗位选项。
|
||||
*
|
||||
* @return 账号表单安全选项
|
||||
*/
|
||||
@GetMapping("/formOptions")
|
||||
@SaCheckPermission("/api/v1/sysAccount/save")
|
||||
public Result<SystemFormOptionsView.AccountFormOptions> formOptions() {
|
||||
return Result.ok(systemFormOptionService.accountFormOptions());
|
||||
}
|
||||
|
||||
/**
|
||||
* 填充账号创建的公共字段。
|
||||
*
|
||||
* <p>账号部门由管理端表单指定;只有未提交部门时才沿用通用创建流程的默认部门。</p>
|
||||
*
|
||||
* @param entity 待创建的账号
|
||||
* @param loginAccount 当前登录账号
|
||||
*/
|
||||
@Override
|
||||
protected void fillCreateCommonFields(SysAccount entity, LoginAccount loginAccount) {
|
||||
BigInteger selectedDeptId = entity.getDeptId();
|
||||
super.fillCreateCommonFields(entity, loginAccount);
|
||||
if (selectedDeptId != null) {
|
||||
entity.setDeptId(selectedDeptId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveOrUpdateAfter(SysAccount entity, boolean isSave) {
|
||||
service.syncRelations(entity);
|
||||
@@ -118,11 +275,24 @@ public class SysAccountController extends BaseCurdController<SysAccountService,
|
||||
return super.onRemoveBefore(ids);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前账号资料、角色标识与默认首页。
|
||||
*
|
||||
* @return 当前账号资料视图
|
||||
*/
|
||||
@GetMapping("/myProfile")
|
||||
public Result<SysAccount> myProfile() {
|
||||
public Result<SysAccountProfileVo> myProfile() {
|
||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||
SysAccount sysAccount = service.getById(account.getId());
|
||||
return Result.ok(sysAccount);
|
||||
List<String> roles = sysRoleService.getRolesByAccountId(account.getId()).stream()
|
||||
.map(SysRole::getRoleKey)
|
||||
.filter(StringUtil::hasText)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
String homePath = roles.contains(Constants.SUPER_ADMIN_ROLE_CODE)
|
||||
? SUPER_ADMIN_HOME_PATH
|
||||
: USER_HOME_PATH;
|
||||
return Result.ok(SysAccountProfileVo.from(sysAccount, roles, homePath));
|
||||
}
|
||||
|
||||
@PostMapping("/updateProfile")
|
||||
@@ -143,42 +313,59 @@ public class SysAccountController extends BaseCurdController<SysAccountService,
|
||||
/**
|
||||
* 修改密码,用于修改用户自己的密码
|
||||
*
|
||||
* @param password 用户的旧密码
|
||||
* @param newPassword 新密码
|
||||
* @param confirmPassword 确认密码
|
||||
* @param encryptedCredential 加密后的当前密码、新密码与确认密码
|
||||
* @return 密码修改结果
|
||||
*/
|
||||
@PostMapping("/updatePassword")
|
||||
public Result<Void> updatePassword(@JsonBody(value = "password", required = true) String password,
|
||||
@JsonBody(value = "newPassword", required = true) String newPassword,
|
||||
@JsonBody(value = "confirmPassword", required = true) String confirmPassword) {
|
||||
public Result<Void> updatePassword(@JsonBody EncryptedCredentialDTO encryptedCredential) {
|
||||
JSONObject payload = credentialKeyService.decryptPayload(encryptedCredential);
|
||||
String password = payload.getString("password");
|
||||
String newPassword = payload.getString("newPassword");
|
||||
String confirmPassword = payload.getString("confirmPassword");
|
||||
BigInteger loginAccountId = SaTokenUtil.getLoginAccount().getId();
|
||||
SysAccount record = service.getById(loginAccountId);
|
||||
if (record == null) {
|
||||
return Result.fail("修改失败");
|
||||
}
|
||||
String pwdDb = record.getPassword();
|
||||
if (!BCrypt.checkpw(password, pwdDb)) {
|
||||
return Result.fail(1, "密码不正确");
|
||||
}
|
||||
if (!newPassword.equals(confirmPassword)) {
|
||||
return Result.fail(2, "两次密码不一致");
|
||||
}
|
||||
SysPasswordPolicy.validateStrongPassword(newPassword);
|
||||
SysAccount update = new SysAccount();
|
||||
update.setId(loginAccountId);
|
||||
update.setPassword(BCrypt.hashpw(newPassword));
|
||||
update.setPasswordResetRequired(false);
|
||||
update.setModified(new Date());
|
||||
update.setModifiedBy(loginAccountId);
|
||||
service.updateById(update);
|
||||
authService.updateOwnPassword(
|
||||
loginAccountId,
|
||||
password,
|
||||
newPassword,
|
||||
confirmPassword,
|
||||
StpUtil.getLoginDevice()
|
||||
);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
private String decryptInitialPassword(Map<String, Object> passwordCredential) {
|
||||
if (passwordCredential == null || passwordCredential.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
JSONObject payload = credentialKeyService.decryptPayload(toEncryptedCredential(passwordCredential));
|
||||
return payload.getString("password");
|
||||
}
|
||||
|
||||
private EncryptedCredentialDTO toEncryptedCredential(Map<String, Object> passwordCredential) {
|
||||
EncryptedCredentialDTO encryptedCredential = new EncryptedCredentialDTO();
|
||||
encryptedCredential.setKeyId(asString(passwordCredential.get("keyId")));
|
||||
encryptedCredential.setEncryptedKey(asString(passwordCredential.get("encryptedKey")));
|
||||
encryptedCredential.setIv(asString(passwordCredential.get("iv")));
|
||||
encryptedCredential.setCiphertext(asString(passwordCredential.get("ciphertext")));
|
||||
encryptedCredential.setNonce(asString(passwordCredential.get("nonce")));
|
||||
return encryptedCredential;
|
||||
}
|
||||
|
||||
private String asString(Object value) {
|
||||
return value == null ? null : String.valueOf(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将指定账号密码重置为系统默认强密码。
|
||||
*
|
||||
* @param id 账号 ID
|
||||
* @return 本次重置后使用的明文密码
|
||||
*/
|
||||
@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();
|
||||
public Result<String> resetPassword(@JsonBody(value = "id", required = true) BigInteger id) {
|
||||
String password = service.resetPassword(id, SaTokenUtil.getLoginAccount().getId());
|
||||
return Result.ok(password);
|
||||
}
|
||||
|
||||
@PostMapping("/removeBatchWithResult")
|
||||
@@ -219,8 +406,12 @@ public class SysAccountController extends BaseCurdController<SysAccountService,
|
||||
service.writeImportTemplate(response.getOutputStream());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
@PostMapping("save")
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Result<?> save(@JsonBody SysAccount entity) {
|
||||
try {
|
||||
return super.save(entity);
|
||||
@@ -228,4 +419,33 @@ public class SysAccountController extends BaseCurdController<SysAccountService,
|
||||
return Result.fail(1, "用户名已存在");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验创建账号时提交的角色,并归一化角色 ID。
|
||||
*
|
||||
* @param entity 待创建账号
|
||||
* @return 校验失败结果,校验通过返回 null
|
||||
*/
|
||||
private Result<?> validateCreateRoles(SysAccount entity) {
|
||||
List<BigInteger> roleIds = entity == null ? null : entity.getRoleIds();
|
||||
Set<BigInteger> uniqueRoleIds = new LinkedHashSet<>();
|
||||
if (roleIds != null) {
|
||||
roleIds.stream()
|
||||
.filter(java.util.Objects::nonNull)
|
||||
.forEach(uniqueRoleIds::add);
|
||||
}
|
||||
if (uniqueRoleIds.isEmpty()) {
|
||||
return Result.fail(1, "角色不能为空");
|
||||
}
|
||||
|
||||
List<SysRole> roles = sysRoleService.listByIds(uniqueRoleIds);
|
||||
boolean valid = roles.size() == uniqueRoleIds.size()
|
||||
&& roles.stream().allMatch(role ->
|
||||
EnumDataStatus.AVAILABLE.getCode().equals(role.getStatus()));
|
||||
if (!valid) {
|
||||
return Result.fail(1, "角色不存在或已禁用");
|
||||
}
|
||||
entity.setRoleIds(new ArrayList<>(uniqueRoleIds));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,16 +6,21 @@ import com.mybatisflex.core.query.QueryWrapper;
|
||||
import com.mybatisflex.core.table.TableInfo;
|
||||
import com.mybatisflex.core.table.TableInfoFactory;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import tech.easyflow.ai.service.KnowledgeSharePermissionService;
|
||||
import tech.easyflow.ai.service.WorkflowApiPermissionService;
|
||||
import tech.easyflow.ai.enums.KnowledgeApiPermissionScope;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.util.IdUtil;
|
||||
import tech.easyflow.common.vo.PkVo;
|
||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||
import tech.easyflow.system.entity.SysApiKey;
|
||||
import tech.easyflow.system.entity.SysApiKeyResourceMapping;
|
||||
import tech.easyflow.system.service.SysApiKeyResourceMappingService;
|
||||
@@ -27,6 +32,7 @@ import java.time.LocalDate;
|
||||
import java.time.ZoneId;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 控制层。
|
||||
@@ -37,22 +43,53 @@ import java.util.List;
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/sysApiKey")
|
||||
public class SysApiKeyController extends BaseCurdController<SysApiKeyService, SysApiKey> {
|
||||
|
||||
/**
|
||||
* 访问令牌名称最大长度。
|
||||
*/
|
||||
private static final int API_KEY_NAME_MAX_LENGTH = 100;
|
||||
|
||||
/**
|
||||
* 兼容旧客户端时使用的默认名称前缀。
|
||||
*/
|
||||
private static final String DEFAULT_API_KEY_NAME_PREFIX = "访问令牌-";
|
||||
|
||||
public SysApiKeyController(SysApiKeyService service) {
|
||||
super(service);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 API Key 列表关键字搜索字段。
|
||||
*
|
||||
* @return 名称和 Key 属性
|
||||
*/
|
||||
@Override
|
||||
protected String[] getKeywordSearchProperties() {
|
||||
return new String[]{"name", "apiKey"};
|
||||
}
|
||||
|
||||
@Resource
|
||||
private SysApiKeyResourceMappingService sysApiKeyResourceMappingService;
|
||||
@Resource
|
||||
private KnowledgeSharePermissionService knowledgeSharePermissionService;
|
||||
@Resource
|
||||
private WorkflowApiPermissionService workflowApiPermissionService;
|
||||
/**
|
||||
* 添加(保存)数据
|
||||
*
|
||||
* @param name 访问令牌名称
|
||||
* @return {@code Result.errorCode == 0} 添加成功,否则添加失败
|
||||
*/
|
||||
@PostMapping("/key/save")
|
||||
@SaCheckPermission("/api/v1/sysApiKey/save")
|
||||
public Result<PkVo> save() {
|
||||
public Result<PkVo> save(@JsonBody(value = "name", required = false) String name) {
|
||||
String apiKey = IdUtil.generateUUID();
|
||||
String normalizedName = normalizeCreateName(name, apiKey);
|
||||
if (normalizedName.length() > API_KEY_NAME_MAX_LENGTH) {
|
||||
return Result.fail("访问令牌名称不能超过100个字符", null);
|
||||
}
|
||||
SysApiKey entity = new SysApiKey();
|
||||
entity.setName(normalizedName);
|
||||
entity.setApiKey(apiKey);
|
||||
entity.setCreated(new Date());
|
||||
entity.setStatus(1);
|
||||
@@ -77,12 +114,74 @@ public class SysApiKeyController extends BaseCurdController<SysApiKeyService, Sy
|
||||
return Result.ok(new PkVo(pkArgs));
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新访问令牌基础信息与授权。
|
||||
*
|
||||
* <p>权限开关不映射数据库列,权限更新请求可能只包含主键与权限字段。
|
||||
* 此时跳过主表更新,避免 MyBatis-Flex 生成空的 {@code SET} 子句。</p>
|
||||
*
|
||||
* @param entity 待更新的访问令牌
|
||||
* @return 更新结果
|
||||
*/
|
||||
@Override
|
||||
@PostMapping("/update")
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Result<?> update(@JsonBody SysApiKey entity) {
|
||||
if (entity == null || entity.getId() == null) {
|
||||
return Result.fail("访问令牌 ID 不能为空");
|
||||
}
|
||||
Result<?> nameValidationResult = normalizeAndValidateUpdateName(entity);
|
||||
if (nameValidationResult != null) {
|
||||
return nameValidationResult;
|
||||
}
|
||||
if (!hasPersistentUpdateFields(entity) && !hasPermissionUpdateFields(entity)) {
|
||||
return Result.fail("没有可更新的访问令牌字段");
|
||||
}
|
||||
if (hasNewKnowledgePermissionFields(entity)
|
||||
&& !hasCompleteKnowledgePermissionFields(entity)) {
|
||||
return Result.fail("知识库读取、导入、维护权限必须同时提交");
|
||||
}
|
||||
if (service.getById(entity.getId()) == null) {
|
||||
return Result.fail("访问令牌不存在");
|
||||
}
|
||||
Result<?> beforeResult = onSaveOrUpdateBefore(entity, false);
|
||||
if (beforeResult != null) {
|
||||
return beforeResult;
|
||||
}
|
||||
if (hasPersistentUpdateFields(entity)) {
|
||||
service.updateById(entity);
|
||||
}
|
||||
onSaveOrUpdateAfter(entity, false);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveOrUpdateAfter(SysApiKey entity, boolean isSave) {
|
||||
if (!isSave && entity.getPermissionIds() != null && !entity.getPermissionIds().isEmpty()) {
|
||||
// 修改的时候绑定授权接口
|
||||
if (entity.getPermissionIds() != null) {
|
||||
sysApiKeyResourceMappingService.authInterface(entity);
|
||||
}
|
||||
if (hasNewKnowledgePermissionFields(entity)) {
|
||||
knowledgeSharePermissionService.replaceApiPermissions(
|
||||
entity.getId(),
|
||||
Boolean.TRUE.equals(entity.getKnowledgeReadEnabled()),
|
||||
Boolean.TRUE.equals(entity.getKnowledgeImportEnabled()),
|
||||
Boolean.TRUE.equals(entity.getKnowledgeMaintenanceEnabled())
|
||||
);
|
||||
} else 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
|
||||
@@ -91,11 +190,135 @@ public class SysApiKeyController extends BaseCurdController<SysApiKeyService, Sy
|
||||
Result<Page<SysApiKey>> pageResult = (Result<Page<SysApiKey>>) super.page(request, sortKey, sortType, pageNumber, pageSize);
|
||||
Page<SysApiKey> data = pageResult.getData();
|
||||
List<SysApiKey> records = data.getRecords();
|
||||
records.forEach(record -> {
|
||||
QueryWrapper queryWrapper = QueryWrapper.create().select(SysApiKeyResourceMapping::getApiKeyResourceId).eq(SysApiKeyResourceMapping::getApiKeyId, record.getId());
|
||||
List<BigInteger> resourceIds = sysApiKeyResourceMappingService.listAs(queryWrapper, BigInteger.class);
|
||||
record.setPermissionIds(resourceIds);
|
||||
});
|
||||
records.forEach(this::fillApiKeyPermissions);
|
||||
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);
|
||||
|
||||
Set<String> knowledgeScopes =
|
||||
knowledgeSharePermissionService.getApiPermissionScopes(entity.getId());
|
||||
boolean readEnabled =
|
||||
knowledgeScopes.contains(KnowledgeApiPermissionScope.KNOWLEDGE_READ.name());
|
||||
boolean importEnabled =
|
||||
knowledgeScopes.contains(KnowledgeApiPermissionScope.KNOWLEDGE_IMPORT.name());
|
||||
boolean maintenanceEnabled =
|
||||
knowledgeScopes.contains(KnowledgeApiPermissionScope.KNOWLEDGE_MAINTENANCE.name());
|
||||
entity.setKnowledgeReadEnabled(readEnabled);
|
||||
entity.setKnowledgeImportEnabled(importEnabled);
|
||||
entity.setKnowledgeMaintenanceEnabled(maintenanceEnabled);
|
||||
entity.setKnowledgeShareEnabled(readEnabled || importEnabled || maintenanceEnabled);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断请求是否提交了任一新版知识库权限字段。
|
||||
*
|
||||
* @param entity 访问令牌
|
||||
* @return 是否提交新版字段
|
||||
*/
|
||||
private boolean hasNewKnowledgePermissionFields(SysApiKey entity) {
|
||||
return entity.getKnowledgeReadEnabled() != null
|
||||
|| entity.getKnowledgeImportEnabled() != null
|
||||
|| entity.getKnowledgeMaintenanceEnabled() != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断请求是否完整提交三个新版知识库权限字段。
|
||||
*
|
||||
* @param entity 访问令牌
|
||||
* @return 三个字段是否均已提交
|
||||
*/
|
||||
private boolean hasCompleteKnowledgePermissionFields(SysApiKey entity) {
|
||||
return entity.getKnowledgeReadEnabled() != null
|
||||
&& entity.getKnowledgeImportEnabled() != null
|
||||
&& entity.getKnowledgeMaintenanceEnabled() != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断请求是否包含主表可持久化字段。
|
||||
*
|
||||
* @param entity 访问令牌
|
||||
* @return 是否需要更新访问令牌主表
|
||||
*/
|
||||
private boolean hasPersistentUpdateFields(SysApiKey entity) {
|
||||
return entity.getName() != null
|
||||
|| entity.getApiKey() != null
|
||||
|| entity.getCreated() != null
|
||||
|| entity.getStatus() != null
|
||||
|| entity.getDeptId() != null
|
||||
|| entity.getTenantId() != null
|
||||
|| entity.getExpiredAt() != null
|
||||
|| entity.getCreatedBy() != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断请求是否包含任一非主表权限字段。
|
||||
*
|
||||
* @param entity 访问令牌
|
||||
* @return 是否需要更新权限映射
|
||||
*/
|
||||
private boolean hasPermissionUpdateFields(SysApiKey entity) {
|
||||
return entity.getPermissionIds() != null
|
||||
|| entity.getKnowledgeShareEnabled() != null
|
||||
|| hasNewKnowledgePermissionFields(entity)
|
||||
|| entity.getWorkflowApiEnabled() != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 标准化新建访问令牌的名称。
|
||||
*
|
||||
* <p>未传名称时生成可识别的兼容名称,避免旧客户端在升级期间创建空名称记录。</p>
|
||||
*
|
||||
* @param name 客户端提交的名称
|
||||
* @param apiKey 新生成的访问令牌
|
||||
* @return 标准化后的名称
|
||||
*/
|
||||
private String normalizeCreateName(String name, String apiKey) {
|
||||
if (name != null && !name.trim().isEmpty()) {
|
||||
return name.trim();
|
||||
}
|
||||
int suffixStart = Math.max(0, apiKey.length() - 6);
|
||||
return DEFAULT_API_KEY_NAME_PREFIX + apiKey.substring(suffixStart);
|
||||
}
|
||||
|
||||
/**
|
||||
* 标准化并校验更新请求中的访问令牌名称。
|
||||
*
|
||||
* @param entity 待更新的访问令牌
|
||||
* @return 校验失败结果;无需校验或校验成功时返回 {@code null}
|
||||
*/
|
||||
private Result<?> normalizeAndValidateUpdateName(SysApiKey entity) {
|
||||
if (entity.getName() == null) {
|
||||
return null;
|
||||
}
|
||||
String normalizedName = entity.getName().trim();
|
||||
if (normalizedName.isEmpty()) {
|
||||
return Result.fail("访问令牌名称不能为空");
|
||||
}
|
||||
if (normalizedName.length() > API_KEY_NAME_MAX_LENGTH) {
|
||||
return Result.fail("访问令牌名称不能超过100个字符");
|
||||
}
|
||||
entity.setName(normalizedName);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,17 @@
|
||||
package tech.easyflow.admin.controller.system;
|
||||
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import tech.easyflow.common.annotation.UsePermission;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import tech.easyflow.system.entity.SysApiKeyResource;
|
||||
import tech.easyflow.system.service.SysApiKeyResourceService;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 请求接口表 控制层。
|
||||
*
|
||||
@@ -20,4 +25,36 @@ public class SysApiKeyResourceController extends BaseCurdController<SysApiKeyRes
|
||||
public SysApiKeyResourceController(SysApiKeyResourceService service) {
|
||||
super(service);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取接口授权资源关键字搜索字段。
|
||||
*
|
||||
* @return 请求接口和标题属性
|
||||
*/
|
||||
@Override
|
||||
protected String[] getKeywordSearchProperties() {
|
||||
return new String[]{"requestInterface", "title"};
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询普通 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));
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,39 @@
|
||||
package tech.easyflow.admin.controller.system;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import com.mybatisflex.core.query.QueryColumn;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import tech.easyflow.approval.entity.ApprovalFlowScope;
|
||||
import tech.easyflow.approval.entity.ApprovalFlowStepAssignee;
|
||||
import tech.easyflow.approval.enums.ApprovalAssigneeType;
|
||||
import tech.easyflow.approval.enums.ApprovalScopeType;
|
||||
import tech.easyflow.approval.mapper.ApprovalFlowScopeMapper;
|
||||
import tech.easyflow.approval.mapper.ApprovalFlowStepAssigneeMapper;
|
||||
import tech.easyflow.common.constant.Constants;
|
||||
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.tree.Tree;
|
||||
|
||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||
import tech.easyflow.system.entity.SysAccount;
|
||||
import tech.easyflow.system.entity.SysDept;
|
||||
import tech.easyflow.system.service.SysAccountService;
|
||||
import tech.easyflow.system.service.SysDeptService;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 部门表 控制层。
|
||||
@@ -33,30 +45,154 @@ import java.util.List;
|
||||
@RequestMapping("/api/v1/sysDept")
|
||||
public class SysDeptController extends BaseCurdController<SysDeptService, SysDept> {
|
||||
|
||||
@Resource
|
||||
private SysAccountService sysAccountService;
|
||||
private final SysAccountService sysAccountService;
|
||||
private final ApprovalFlowStepAssigneeMapper approvalFlowStepAssigneeMapper;
|
||||
private final ApprovalFlowScopeMapper approvalFlowScopeMapper;
|
||||
|
||||
public SysDeptController(SysDeptService service) {
|
||||
/**
|
||||
* 创建部门管理控制器。
|
||||
*
|
||||
* @param service 部门服务
|
||||
* @param sysAccountService 用户服务
|
||||
* @param approvalFlowStepAssigneeMapper 审批步骤对象 Mapper
|
||||
* @param approvalFlowScopeMapper 审批范围 Mapper
|
||||
*/
|
||||
public SysDeptController(SysDeptService service,
|
||||
SysAccountService sysAccountService,
|
||||
ApprovalFlowStepAssigneeMapper approvalFlowStepAssigneeMapper,
|
||||
ApprovalFlowScopeMapper approvalFlowScopeMapper) {
|
||||
super(service);
|
||||
this.sysAccountService = sysAccountService;
|
||||
this.approvalFlowStepAssigneeMapper = approvalFlowStepAssigneeMapper;
|
||||
this.approvalFlowScopeMapper = approvalFlowScopeMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取部门列表默认排序规则。
|
||||
*
|
||||
* @return 默认排序表达式
|
||||
*/
|
||||
@Override
|
||||
protected String getDefaultOrderBy() {
|
||||
return "sort_no asc";
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询部门列表并组装为树形结构。
|
||||
*
|
||||
* @param entity 查询条件
|
||||
* @param asTree 是否返回树形结构
|
||||
* @param sortKey 排序字段
|
||||
* @param sortType 排序方向
|
||||
* @return 部门树
|
||||
*/
|
||||
@Override
|
||||
@GetMapping("list")
|
||||
public Result<List<SysDept>> list(SysDept entity, Boolean asTree, String sortKey, String sortType) {
|
||||
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
|
||||
String keyword = entity == null ? "" : normalizeSearchKeyword(entity.getKeyword());
|
||||
if (tech.easyflow.common.util.StringUtil.hasText(keyword)) {
|
||||
queryWrapper.and(buildLiteralContainsCondition(
|
||||
keyword, new QueryColumn("dept_name"), new QueryColumn("dept_code")));
|
||||
}
|
||||
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||
List<SysDept> sysMenus = service.list(queryWrapper);
|
||||
return Result.ok(Tree.tryToTree(sysMenus, "id", "parentId"));
|
||||
List<SysDept> matchedDepartments = service.list(queryWrapper);
|
||||
if (!tech.easyflow.common.util.StringUtil.hasText(keyword)) {
|
||||
return Result.ok(Tree.tryToTree(matchedDepartments, "id", "parentId"));
|
||||
}
|
||||
if (matchedDepartments.isEmpty()) {
|
||||
return Result.ok(List.of());
|
||||
}
|
||||
|
||||
// 搜索结果保留所有重名命中项,并补齐各自祖先节点以维持可定位的树结构。
|
||||
Set<BigInteger> visibleIds = new LinkedHashSet<>();
|
||||
for (SysDept department : matchedDepartments) {
|
||||
visibleIds.add(department.getId());
|
||||
addAncestorIds(visibleIds, department.getAncestors());
|
||||
}
|
||||
QueryWrapper visibleDepartmentQuery = QueryWrapper.create()
|
||||
.in(SysDept::getId, visibleIds)
|
||||
.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||
List<SysDept> visibleDepartments = service.list(visibleDepartmentQuery);
|
||||
return Result.ok(Tree.tryToTree(visibleDepartments, "id", "parentId"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 将逗号分隔的祖先 ID 加入可见集合。
|
||||
*
|
||||
* @param visibleIds 可见部门 ID 集合
|
||||
* @param ancestors 祖先路径
|
||||
*/
|
||||
private void addAncestorIds(Set<BigInteger> visibleIds, String ancestors) {
|
||||
if (!tech.easyflow.common.util.StringUtil.hasText(ancestors)) {
|
||||
return;
|
||||
}
|
||||
for (String ancestor : ancestors.split(",")) {
|
||||
String normalized = ancestor.trim();
|
||||
if (!normalized.isEmpty() && !"0".equals(normalized)) {
|
||||
visibleIds.add(new BigInteger(normalized));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量修改部门状态。
|
||||
*
|
||||
* @param ids 部门主键集合
|
||||
* @param status 目标状态
|
||||
* @return 实际更新数量
|
||||
*/
|
||||
@PostMapping("changeStatusBatch")
|
||||
@SaCheckPermission("/api/v1/sysDept/save")
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Result<Integer> changeStatusBatch(
|
||||
@JsonBody(value = "ids", required = true) List<BigInteger> ids,
|
||||
@JsonBody(value = "status", required = true) Integer status) {
|
||||
Set<BigInteger> uniqueIds = normalizeIds(ids);
|
||||
if (uniqueIds.isEmpty()) {
|
||||
return Result.fail("请选择需要操作的部门", null);
|
||||
}
|
||||
if (!EnumDataStatus.AVAILABLE.getCode().equals(status)
|
||||
&& !EnumDataStatus.UNAVAILABLE.getCode().equals(status)) {
|
||||
return Result.fail("部门状态不合法", null);
|
||||
}
|
||||
|
||||
List<SysDept> records = service.listByIds(uniqueIds);
|
||||
if (records.size() != uniqueIds.size()) {
|
||||
return Result.fail("部分部门不存在或已删除,请刷新后重试", null);
|
||||
}
|
||||
if (EnumDataStatus.UNAVAILABLE.getCode().equals(status) && containsRootDept(records)) {
|
||||
return Result.fail("根部门不能禁用", null);
|
||||
}
|
||||
if (EnumDataStatus.UNAVAILABLE.getCode().equals(status)
|
||||
&& isUsedByApprovalFlow(uniqueIds)) {
|
||||
return Result.fail("所选部门已被审批流程使用,不能禁用", null);
|
||||
}
|
||||
|
||||
LoginAccount loginUser = SaTokenUtil.getLoginAccount();
|
||||
SysDept update = new SysDept();
|
||||
update.setStatus(status);
|
||||
update.setModified(new Date());
|
||||
update.setModifiedBy(loginUser.getId());
|
||||
|
||||
QueryWrapper updateWrapper = QueryWrapper.create();
|
||||
updateWrapper.in(SysDept::getId, uniqueIds);
|
||||
int updated = service.getMapper().updateByQuery(update, updateWrapper);
|
||||
if (updated <= 0) {
|
||||
return Result.fail("部门状态修改失败", null);
|
||||
}
|
||||
return Result.ok(updated);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
protected Result onSaveOrUpdateBefore(SysDept entity, boolean isSave) {
|
||||
LoginAccount loginUser = SaTokenUtil.getLoginAccount();
|
||||
if (isSave && entity.getStatus() == null) {
|
||||
entity.setStatus(EnumDataStatus.AVAILABLE.getCode());
|
||||
}
|
||||
BigInteger parentId = entity.getParentId();
|
||||
if (parentId.equals(BigInteger.ZERO)) {
|
||||
entity.setAncestors(parentId.toString());
|
||||
@@ -73,20 +209,87 @@ public class SysDeptController extends BaseCurdController<SysDeptService, SysDep
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
protected Result onRemoveBefore(Collection<Serializable> ids) {
|
||||
List<SysDept> records = service.listByIds(ids);
|
||||
for (SysDept dept : records) {
|
||||
if (Constants.ROOT_DEPT.equals(dept.getDeptCode())) {
|
||||
if (records.size() != ids.size()) {
|
||||
return Result.fail(1, "部分部门不存在或已删除,请刷新后重试");
|
||||
}
|
||||
if (containsRootDept(records)) {
|
||||
return Result.fail(1, "无法删除根部门");
|
||||
}
|
||||
|
||||
QueryWrapper childQuery = QueryWrapper.create();
|
||||
childQuery.in(SysDept::getParentId, ids);
|
||||
childQuery.notIn(SysDept::getId, ids);
|
||||
if (service.count(childQuery) > 0) {
|
||||
return Result.fail(1, "所选部门包含未选中的下级部门,不能删除");
|
||||
}
|
||||
QueryWrapper w = QueryWrapper.create();
|
||||
w.in(SysAccount::getDeptId, ids);
|
||||
long count = sysAccountService.count(w);
|
||||
if (isUsedByApprovalFlow(ids)) {
|
||||
return Result.fail(1, "所选部门已被审批流程使用,请先调整审批配置");
|
||||
}
|
||||
|
||||
QueryWrapper accountQuery = QueryWrapper.create();
|
||||
accountQuery.in(SysAccount::getDeptId, ids);
|
||||
long count = sysAccountService.count(accountQuery);
|
||||
if (count > 0) {
|
||||
return Result.fail(1, "该部门下有员工,不能删除");
|
||||
return Result.fail(1, "所选部门下有员工,不能删除");
|
||||
}
|
||||
return super.onRemoveBefore(ids);
|
||||
}
|
||||
|
||||
/**
|
||||
* 去重并过滤无效部门主键。
|
||||
*
|
||||
* @param ids 原始部门主键集合
|
||||
* @return 有效且去重后的主键集合
|
||||
*/
|
||||
private Set<BigInteger> normalizeIds(Collection<BigInteger> ids) {
|
||||
Set<BigInteger> uniqueIds = new LinkedHashSet<>();
|
||||
if (ids == null) {
|
||||
return uniqueIds;
|
||||
}
|
||||
for (BigInteger id : ids) {
|
||||
if (id != null) {
|
||||
uniqueIds.add(id);
|
||||
}
|
||||
}
|
||||
return uniqueIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断部门集合中是否包含根部门。
|
||||
*
|
||||
* @param records 部门集合
|
||||
* @return 包含根部门时返回 {@code true}
|
||||
*/
|
||||
private boolean containsRootDept(Collection<SysDept> records) {
|
||||
return records.stream()
|
||||
.anyMatch(dept -> Constants.ROOT_DEPT.equals(dept.getDeptCode()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断部门是否仍被审批步骤或审批范围引用。
|
||||
*
|
||||
* @param ids 部门主键集合
|
||||
* @return 存在审批流程引用时返回 {@code true}
|
||||
*/
|
||||
private boolean isUsedByApprovalFlow(Collection<? extends Serializable> ids) {
|
||||
QueryWrapper assigneeQuery = QueryWrapper.create();
|
||||
assigneeQuery.eq(
|
||||
ApprovalFlowStepAssignee::getAssigneeType,
|
||||
ApprovalAssigneeType.DEPT.getCode());
|
||||
assigneeQuery.in(ApprovalFlowStepAssignee::getTargetId, ids);
|
||||
if (approvalFlowStepAssigneeMapper.selectCountByQuery(assigneeQuery) > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
QueryWrapper scopeQuery = QueryWrapper.create();
|
||||
scopeQuery.eq(ApprovalFlowScope::getScopeType, ApprovalScopeType.DEPT.getCode());
|
||||
scopeQuery.in(ApprovalFlowScope::getScopeValue, ids);
|
||||
return approvalFlowScopeMapper.selectCountByQuery(scopeQuery) > 0;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,31 @@
|
||||
package tech.easyflow.admin.controller.system;
|
||||
|
||||
import com.mybatisflex.core.paginate.Page;
|
||||
import com.mybatisflex.core.query.QueryCondition;
|
||||
import com.mybatisflex.core.query.QueryMethods;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import com.mybatisflex.core.relation.RelationManager;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import tech.easyflow.common.util.StringUtil;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.log.annotation.LogRecord;
|
||||
import tech.easyflow.system.entity.SysLog;
|
||||
import tech.easyflow.system.service.SysLogService;
|
||||
import com.mybatisflex.core.paginate.Page;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import com.mybatisflex.core.relation.RelationManager;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
|
||||
import static tech.easyflow.system.entity.table.SysAccountTableDef.SYS_ACCOUNT;
|
||||
import static tech.easyflow.system.entity.table.SysLogTableDef.SYS_LOG;
|
||||
|
||||
/**
|
||||
* 操作日志表 控制层。
|
||||
@@ -21,14 +36,102 @@ import java.util.Collections;
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/sysLog")
|
||||
public class SysLogController extends BaseCurdController<SysLogService, SysLog> {
|
||||
|
||||
private static final long MAX_PAGE_SIZE = 100L;
|
||||
private static final DateTimeFormatter QUERY_TIME_FORMATTER =
|
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
/**
|
||||
* 创建操作日志控制器。
|
||||
*
|
||||
* @param service 操作日志服务
|
||||
*/
|
||||
public SysLogController(SysLogService service) {
|
||||
super(service);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造通用查询条件,并追加操作时间范围。
|
||||
*
|
||||
* @param request 当前 HTTP 请求
|
||||
* @return 操作日志查询条件
|
||||
* @throws BusinessException 时间格式不正确或开始时间晚于结束时间时抛出
|
||||
*/
|
||||
@Override
|
||||
protected QueryWrapper buildQueryWrapper(HttpServletRequest request) {
|
||||
QueryWrapper queryWrapper = super.buildQueryWrapper(request);
|
||||
String keyword = normalizeSearchKeyword(request.getParameter("keyword"));
|
||||
if (StringUtil.hasText(keyword)) {
|
||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||
String pattern = buildLiteralContainsPattern(keyword);
|
||||
QueryCondition accountKeyword = SYS_ACCOUNT.LOGIN_NAME.likeRaw(pattern)
|
||||
.or(SYS_ACCOUNT.NICKNAME.likeRaw(pattern));
|
||||
QueryWrapper accountExists = QueryMethods.selectOne()
|
||||
.from(SYS_ACCOUNT)
|
||||
.where(SYS_ACCOUNT.ID.eq(SYS_LOG.ACCOUNT_ID))
|
||||
.and(SYS_ACCOUNT.TENANT_ID.eq(account.getTenantId()))
|
||||
.and(accountKeyword);
|
||||
QueryCondition keywordCondition = SYS_LOG.ACTION_NAME.likeRaw(pattern)
|
||||
.or(SYS_LOG.ACTION_IP.likeRaw(pattern))
|
||||
.or(QueryMethods.exists(accountExists));
|
||||
queryWrapper.and(keywordCondition);
|
||||
}
|
||||
Date createdStart = parseQueryTime(request.getParameter("createdStart"));
|
||||
Date createdEnd = parseQueryTime(request.getParameter("createdEnd"));
|
||||
if (createdStart != null && createdEnd != null && createdStart.after(createdEnd)) {
|
||||
throw new BusinessException(400, 400, "操作时间范围不正确");
|
||||
}
|
||||
if (createdStart != null) {
|
||||
queryWrapper.ge(SysLog::getCreated, createdStart);
|
||||
}
|
||||
if (createdEnd != null) {
|
||||
queryWrapper.le(SysLog::getCreated, createdEnd);
|
||||
}
|
||||
return queryWrapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按操作时间稳定倒序展示最新日志。
|
||||
*
|
||||
* @return 默认排序表达式
|
||||
*/
|
||||
@Override
|
||||
protected String getDefaultOrderBy() {
|
||||
return "created desc, id desc";
|
||||
}
|
||||
|
||||
/**
|
||||
* 限制日志单页记录数并加载操作账号关系。
|
||||
*
|
||||
* @param page 分页参数
|
||||
* @param queryWrapper 查询条件
|
||||
* @return 操作日志分页结果
|
||||
*/
|
||||
@Override
|
||||
@LogRecord("分页查询")
|
||||
protected Page<SysLog> queryPage(Page<SysLog> page, QueryWrapper queryWrapper) {
|
||||
page.setPageSize(Math.min(page.getPageSize(), MAX_PAGE_SIZE));
|
||||
RelationManager.setQueryRelations(Collections.singleton("account"));
|
||||
return service.getMapper().paginateWithRelations(page, queryWrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析日志查询时间。
|
||||
*
|
||||
* @param value 格式为 yyyy-MM-dd HH:mm:ss 的时间文本
|
||||
* @return 解析后的时间;空文本返回 {@code null}
|
||||
* @throws BusinessException 时间格式不正确时抛出
|
||||
*/
|
||||
private Date parseQueryTime(String value) {
|
||||
if (!StringUtil.hasText(value)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
LocalDateTime dateTime = LocalDateTime.parse(value, QUERY_TIME_FORMATTER);
|
||||
return Date.from(dateTime.atZone(ZoneId.systemDefault()).toInstant());
|
||||
} catch (DateTimeParseException exception) {
|
||||
throw new BusinessException(
|
||||
400, 400, "操作时间格式不正确", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,7 @@ public class SysPositionController extends BaseCurdController<SysPositionService
|
||||
/**
|
||||
* 分页查询岗位列表
|
||||
* <p>
|
||||
* 支持按岗位名称模糊查询,按状态、编码精确查询。
|
||||
* 支持按岗位名称、岗位编码统一模糊查询,状态保持精确查询。
|
||||
* </p>
|
||||
*
|
||||
* @param request 请求对象
|
||||
@@ -67,18 +67,23 @@ public class SysPositionController extends BaseCurdController<SysPositionService
|
||||
.from(SYS_POSITION);
|
||||
|
||||
// 获取查询参数
|
||||
String keyword = normalizeSearchKeyword(request.getParameter("keyword"));
|
||||
String positionName = request.getParameter("positionName");
|
||||
String positionCode = request.getParameter("positionCode");
|
||||
String status = request.getParameter("status");
|
||||
|
||||
// 岗位名称 - 模糊查询
|
||||
if (StringUtil.hasText(keyword)) {
|
||||
queryWrapper.and(buildLiteralContainsCondition(
|
||||
keyword, SYS_POSITION.POSITION_NAME, SYS_POSITION.POSITION_CODE));
|
||||
} else {
|
||||
// 兼容仍按旧参数调用的客户端。
|
||||
if (StringUtil.hasText(positionName)) {
|
||||
queryWrapper.where(SYS_POSITION.POSITION_NAME.like(positionName));
|
||||
queryWrapper.and(buildLiteralContainsCondition(positionName, SYS_POSITION.POSITION_NAME));
|
||||
}
|
||||
// 岗位编码 - 精确查询
|
||||
if (StringUtil.hasText(positionCode)) {
|
||||
queryWrapper.where(SYS_POSITION.POSITION_CODE.eq(positionCode));
|
||||
}
|
||||
}
|
||||
// 状态 - 精确查询
|
||||
if (StringUtil.hasText(status)) {
|
||||
queryWrapper.where(SYS_POSITION.STATUS.eq(status));
|
||||
|
||||
@@ -9,6 +9,7 @@ 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.admin.service.system.SystemFormOptionService;
|
||||
import tech.easyflow.system.entity.vo.SysRoleCategoryScopeDetailVo;
|
||||
import tech.easyflow.system.service.CategoryPermissionService;
|
||||
import tech.easyflow.system.service.SysRoleCategoryScopeService;
|
||||
@@ -25,6 +26,8 @@ public class SysRoleCategoryScopeController {
|
||||
|
||||
@Resource
|
||||
private CategoryPermissionService categoryPermissionService;
|
||||
@Resource
|
||||
private SystemFormOptionService systemFormOptionService;
|
||||
|
||||
@GetMapping("/detail")
|
||||
@SaCheckPermission("/api/v1/sysRole/query")
|
||||
@@ -41,6 +44,7 @@ public class SysRoleCategoryScopeController {
|
||||
if (request == null || request.getRoleId() == null) {
|
||||
throw new BusinessException("角色ID不能为空");
|
||||
}
|
||||
systemFormOptionService.validateCategoryScopes(request.getScopes());
|
||||
BigInteger operatorId = SaTokenUtil.getLoginAccount().getId();
|
||||
sysRoleCategoryScopeService.saveRoleScopes(request.getRoleId(), request.getScopes(), operatorId);
|
||||
return Result.ok();
|
||||
|
||||
@@ -9,6 +9,8 @@ import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||
import tech.easyflow.admin.model.SystemFormOptionsView;
|
||||
import tech.easyflow.admin.service.system.SystemFormOptionService;
|
||||
import tech.easyflow.system.entity.SysRole;
|
||||
import tech.easyflow.system.entity.SysRoleDept;
|
||||
import tech.easyflow.system.entity.SysRoleMenu;
|
||||
@@ -38,11 +40,34 @@ public class SysRoleController extends BaseCurdController<SysRoleService, SysRol
|
||||
private SysRoleMenuService sysRoleMenuService;
|
||||
@Resource
|
||||
private SysRoleDeptService sysRoleDeptService;
|
||||
@Resource
|
||||
private SystemFormOptionService systemFormOptionService;
|
||||
|
||||
public SysRoleController(SysRoleService service) {
|
||||
super(service);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取角色列表关键字搜索字段。
|
||||
*
|
||||
* @return 角色名称和角色标识属性
|
||||
*/
|
||||
@Override
|
||||
protected String[] getKeywordSearchProperties() {
|
||||
return new String[]{"roleName", "roleKey"};
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询角色表单所需的菜单和非 Bot 分类选项。
|
||||
*
|
||||
* @return 角色表单安全选项
|
||||
*/
|
||||
@GetMapping("formOptions")
|
||||
@SaCheckPermission("/api/v1/sysRole/query")
|
||||
public Result<SystemFormOptionsView.RoleFormOptions> formOptions() {
|
||||
return Result.ok(systemFormOptionService.roleFormOptions());
|
||||
}
|
||||
|
||||
@PostMapping("saveRoleMenu/{roleId}")
|
||||
@SaCheckPermission("/api/v1/sysRole/save")
|
||||
@Deprecated
|
||||
@@ -85,6 +110,7 @@ public class SysRoleController extends BaseCurdController<SysRoleService, SysRol
|
||||
if (entity.getId() == null) {
|
||||
commonFiled(entity, loginUser.getId(), loginUser.getTenantId(), loginUser.getDeptId());
|
||||
}
|
||||
systemFormOptionService.validateRoleReferences(entity);
|
||||
service.saveRole(entity);
|
||||
return Result.ok(entity.getId());
|
||||
}
|
||||
|
||||
@@ -24,6 +24,16 @@ public class SysUserFeedbackController extends BaseCurdController<SysUserFeedbac
|
||||
super(service);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户反馈关键字搜索字段。
|
||||
*
|
||||
* @return 反馈内容和联系方式属性
|
||||
*/
|
||||
@Override
|
||||
protected String[] getKeywordSearchProperties() {
|
||||
return new String[]{"feedbackContent", "contactInfo"};
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Result<?> onSaveOrUpdateBefore(SysUserFeedback entity, boolean isSave) {
|
||||
if (!isSave) {
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package tech.easyflow.admin.controller.system.vo;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import tech.easyflow.system.entity.SysAccount;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 管理端当前账号资料视图。
|
||||
*/
|
||||
public class SysAccountProfileVo extends SysAccount {
|
||||
|
||||
private String homePath;
|
||||
private List<String> roles = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* 创建空的当前账号资料视图。
|
||||
*/
|
||||
public SysAccountProfileVo() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据账号实体和角色信息创建资料视图。
|
||||
*
|
||||
* @param account 账号实体
|
||||
* @param roles 角色标识列表
|
||||
* @param homePath 默认首页
|
||||
* @return 当前账号资料视图
|
||||
*/
|
||||
public static SysAccountProfileVo from(SysAccount account, List<String> roles, String homePath) {
|
||||
SysAccountProfileVo profile = new SysAccountProfileVo();
|
||||
if (account != null) {
|
||||
BeanUtil.copyProperties(account, profile);
|
||||
}
|
||||
profile.setRoles(roles);
|
||||
profile.setHomePath(homePath);
|
||||
return profile;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取默认首页。
|
||||
*
|
||||
* @return 默认首页
|
||||
*/
|
||||
public String getHomePath() {
|
||||
return homePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置默认首页。
|
||||
*
|
||||
* @param homePath 默认首页
|
||||
*/
|
||||
public void setHomePath(String homePath) {
|
||||
this.homePath = homePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取角色标识列表。
|
||||
*
|
||||
* @return 角色标识列表
|
||||
*/
|
||||
public List<String> getRoles() {
|
||||
return roles;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置角色标识列表。
|
||||
*
|
||||
* @param roles 角色标识列表
|
||||
*/
|
||||
public void setRoles(List<String> roles) {
|
||||
this.roles = roles == null ? new ArrayList<>() : new ArrayList<>(roles);
|
||||
}
|
||||
}
|
||||
@@ -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,20 @@
|
||||
package tech.easyflow.admin.model;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
/**
|
||||
* 定时任务可运行的工作流安全选项。
|
||||
*
|
||||
* @param id 工作流 ID
|
||||
* @param title 工作流标题
|
||||
* @param description 工作流描述
|
||||
*/
|
||||
public record SysJobWorkflowOptionView(
|
||||
@JsonSerialize(using = ToStringSerializer.class) BigInteger id,
|
||||
String title,
|
||||
String description
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package tech.easyflow.admin.model;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 管理端系统表单所需的安全选项视图。
|
||||
*/
|
||||
public final class SystemFormOptionsView {
|
||||
|
||||
private SystemFormOptionsView() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 审批流程资源范围选项。
|
||||
*
|
||||
* @param categories 按资源类型分组的分类选项
|
||||
* @param departments 部门树
|
||||
*/
|
||||
public record ApprovalResourceScopeOptions(
|
||||
Map<String, List<CategoryOption>> categories,
|
||||
List<DepartmentOption> departments
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 角色表单选项。
|
||||
*
|
||||
* @param menus 菜单树
|
||||
* @param categories 按资源类型分组的非 Bot 分类选项
|
||||
*/
|
||||
public record RoleFormOptions(
|
||||
List<MenuOption> menus,
|
||||
Map<String, List<CategoryOption>> categories
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 账号表单选项。
|
||||
*
|
||||
* @param departments 部门树
|
||||
* @param roles 可用角色
|
||||
* @param positions 可用岗位
|
||||
*/
|
||||
public record AccountFormOptions(
|
||||
List<DepartmentOption> departments,
|
||||
List<RoleOption> roles,
|
||||
List<PositionOption> positions
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 分类安全选项。
|
||||
*
|
||||
* @param id 分类 ID
|
||||
* @param categoryName 分类名称
|
||||
*/
|
||||
public record CategoryOption(
|
||||
@JsonSerialize(using = ToStringSerializer.class) BigInteger id,
|
||||
String categoryName
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 部门树安全选项。
|
||||
*
|
||||
* @param id 部门 ID
|
||||
* @param parentId 上级部门 ID
|
||||
* @param deptName 部门名称
|
||||
* @param children 下级部门
|
||||
*/
|
||||
public record DepartmentOption(
|
||||
@JsonSerialize(using = ToStringSerializer.class) BigInteger id,
|
||||
@JsonSerialize(using = ToStringSerializer.class) BigInteger parentId,
|
||||
String deptName,
|
||||
List<DepartmentOption> children
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 菜单树安全选项。
|
||||
*
|
||||
* @param id 菜单 ID
|
||||
* @param parentId 上级菜单 ID
|
||||
* @param menuTitle 菜单标题
|
||||
* @param children 下级菜单
|
||||
*/
|
||||
public record MenuOption(
|
||||
@JsonSerialize(using = ToStringSerializer.class) BigInteger id,
|
||||
@JsonSerialize(using = ToStringSerializer.class) BigInteger parentId,
|
||||
String menuTitle,
|
||||
List<MenuOption> children
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 角色安全选项。
|
||||
*
|
||||
* @param id 角色 ID
|
||||
* @param roleName 角色名称
|
||||
*/
|
||||
public record RoleOption(
|
||||
@JsonSerialize(using = ToStringSerializer.class) BigInteger id,
|
||||
String roleName
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 岗位安全选项。
|
||||
*
|
||||
* @param id 岗位 ID
|
||||
* @param positionName 岗位名称
|
||||
*/
|
||||
public record PositionOption(
|
||||
@JsonSerialize(using = ToStringSerializer.class) BigInteger id,
|
||||
String positionName
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package tech.easyflow.admin.model.ai;
|
||||
|
||||
/**
|
||||
* 模型统一网关页面所需的安全配置。
|
||||
*
|
||||
* @param publishBaseUrl 模型发布基础地址
|
||||
*/
|
||||
public record ModelGatewayConfigView(String publishBaseUrl) {
|
||||
}
|
||||
@@ -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,180 @@
|
||||
package tech.easyflow.admin.model.ai;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 工作流设计器所需的安全选项视图。
|
||||
*
|
||||
* @param models 模型选项
|
||||
* @param knowledges 知识库选项
|
||||
* @param codeEngines 代码执行引擎选项
|
||||
*/
|
||||
public record WorkflowDesignerOptionsView(
|
||||
List<ModelOption> models,
|
||||
List<KnowledgeOption> knowledges,
|
||||
List<Map<String, Object>> codeEngines
|
||||
) {
|
||||
|
||||
/**
|
||||
* 模型安全选项。
|
||||
*
|
||||
* @param id 模型 ID
|
||||
* @param title 模型标题
|
||||
* @param description 模型描述
|
||||
* @param modelProvider 供应商安全摘要
|
||||
*/
|
||||
public record ModelOption(
|
||||
@JsonSerialize(using = ToStringSerializer.class) BigInteger id,
|
||||
String title,
|
||||
String description,
|
||||
ProviderOption modelProvider
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 模型供应商安全摘要。
|
||||
*
|
||||
* @param providerName 供应商名称
|
||||
* @param providerType 供应商类型
|
||||
* @param icon 供应商图标
|
||||
*/
|
||||
public record ProviderOption(String providerName, String providerType, String icon) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 知识库安全选项。
|
||||
*
|
||||
* @param id 知识库 ID
|
||||
* @param title 知识库标题
|
||||
* @param description 知识库描述
|
||||
* @param vectorEmbedModelId Embedding 模型 ID
|
||||
* @param dimensionOfVectorModel 向量维度
|
||||
* @param vectorStoreEnabled 是否可用于向量检索
|
||||
*/
|
||||
public record KnowledgeOption(
|
||||
@JsonSerialize(using = ToStringSerializer.class) BigInteger id,
|
||||
String title,
|
||||
String description,
|
||||
@JsonSerialize(using = ToStringSerializer.class) BigInteger vectorEmbedModelId,
|
||||
Integer dimensionOfVectorModel,
|
||||
Boolean vectorStoreEnabled
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件安全选项。
|
||||
*
|
||||
* @param id 插件 ID
|
||||
* @param name 插件名称
|
||||
* @param description 插件描述
|
||||
* @param icon 插件图标
|
||||
* @param tools 可用工具
|
||||
*/
|
||||
public record PluginOption(
|
||||
@JsonSerialize(using = ToStringSerializer.class) BigInteger id,
|
||||
String name,
|
||||
String description,
|
||||
String icon,
|
||||
List<PluginToolOption> tools
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件工具安全选项。
|
||||
*
|
||||
* @param id 工具 ID
|
||||
* @param name 工具名称
|
||||
* @param description 工具描述
|
||||
*/
|
||||
public record PluginToolOption(
|
||||
@JsonSerialize(using = ToStringSerializer.class) BigInteger id,
|
||||
String name,
|
||||
String description
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据源安全选项。
|
||||
*
|
||||
* @param id 数据源 ID
|
||||
* @param sourceName 数据源名称
|
||||
* @param sourceType 数据源类型
|
||||
*/
|
||||
public record DataSourceOption(
|
||||
@JsonSerialize(using = ToStringSerializer.class) BigInteger id,
|
||||
String sourceName,
|
||||
String sourceType
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据目录安全选项。
|
||||
*
|
||||
* @param id 目录 ID
|
||||
* @param sourceId 数据源 ID
|
||||
* @param catalogName 目录名称
|
||||
* @param catalogDesc 目录描述
|
||||
*/
|
||||
public record CatalogOption(
|
||||
@JsonSerialize(using = ToStringSerializer.class) BigInteger id,
|
||||
@JsonSerialize(using = ToStringSerializer.class) BigInteger sourceId,
|
||||
String catalogName,
|
||||
String catalogDesc
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 已接入数据集安全选项。
|
||||
*
|
||||
* @param id 数据集 ID
|
||||
* @param tenantId 租户 ID
|
||||
* @param sourceId 数据源 ID
|
||||
* @param catalogId 目录 ID
|
||||
* @param tableName 数据表名称
|
||||
* @param tableDesc 数据表描述
|
||||
*/
|
||||
public record DatasetOption(
|
||||
@JsonSerialize(using = ToStringSerializer.class) BigInteger id,
|
||||
@JsonSerialize(using = ToStringSerializer.class) BigInteger tenantId,
|
||||
@JsonSerialize(using = ToStringSerializer.class) BigInteger sourceId,
|
||||
@JsonSerialize(using = ToStringSerializer.class) BigInteger catalogId,
|
||||
String tableName,
|
||||
String tableDesc
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据集字段安全视图。
|
||||
*
|
||||
* @param fieldName 字段名称
|
||||
* @param fieldDesc 字段描述
|
||||
* @param jdbcType JDBC 类型
|
||||
* @param fieldType 业务字段类型
|
||||
*/
|
||||
public record DatasetFieldOption(
|
||||
String fieldName,
|
||||
String fieldDesc,
|
||||
String jdbcType,
|
||||
Integer fieldType
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据集结构安全视图。
|
||||
*
|
||||
* @param tableName 数据表名称
|
||||
* @param tableDesc 数据表描述
|
||||
* @param fields 字段列表
|
||||
*/
|
||||
public record DatasetSchemaOption(
|
||||
String tableName,
|
||||
String tableDesc,
|
||||
List<DatasetFieldOption> fields
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package tech.easyflow.admin.model.dashboard;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
/**
|
||||
* 工作台分布/排行项。
|
||||
*/
|
||||
@@ -15,12 +17,20 @@ public class DashboardDistributionItemVo {
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -61,14 +71,6 @@ public class DashboardDistributionItemVo {
|
||||
this.activeUserTotal = activeUserTotal;
|
||||
}
|
||||
|
||||
public Long getBotTotal() {
|
||||
return botTotal;
|
||||
}
|
||||
|
||||
public void setBotTotal(Long botTotal) {
|
||||
this.botTotal = botTotal;
|
||||
}
|
||||
|
||||
public Long getWorkflowTotal() {
|
||||
return workflowTotal;
|
||||
}
|
||||
@@ -84,4 +86,44 @@ public class DashboardDistributionItemVo {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ package tech.easyflow.admin.model.dashboard;
|
||||
public class DashboardOverviewQuery {
|
||||
|
||||
private String range;
|
||||
private String startDate;
|
||||
private String endDate;
|
||||
|
||||
public String getRange() {
|
||||
return range;
|
||||
@@ -14,4 +16,20 @@ public class DashboardOverviewQuery {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,12 @@ public class DashboardOverviewVo {
|
||||
|
||||
private DashboardSummaryVo summary;
|
||||
|
||||
private DashboardChatStatusVo chatStatus;
|
||||
|
||||
private List<DashboardTrendItemVo> trends;
|
||||
|
||||
private List<DashboardAssistantTrendSeriesVo> assistantTrends;
|
||||
|
||||
private List<DashboardDistributionItemVo> distribution;
|
||||
|
||||
private DashboardOverviewQuery query;
|
||||
@@ -34,6 +38,14 @@ public class DashboardOverviewVo {
|
||||
this.trends = trends;
|
||||
}
|
||||
|
||||
public DashboardChatStatusVo getChatStatus() {
|
||||
return chatStatus;
|
||||
}
|
||||
|
||||
public void setChatStatus(DashboardChatStatusVo chatStatus) {
|
||||
this.chatStatus = chatStatus;
|
||||
}
|
||||
|
||||
public List<DashboardDistributionItemVo> getDistribution() {
|
||||
return distribution;
|
||||
}
|
||||
@@ -42,6 +54,14 @@ public class DashboardOverviewVo {
|
||||
this.distribution = distribution;
|
||||
}
|
||||
|
||||
public List<DashboardAssistantTrendSeriesVo> getAssistantTrends() {
|
||||
return assistantTrends;
|
||||
}
|
||||
|
||||
public void setAssistantTrends(List<DashboardAssistantTrendSeriesVo> assistantTrends) {
|
||||
this.assistantTrends = assistantTrends;
|
||||
}
|
||||
|
||||
public DashboardOverviewQuery getQuery() {
|
||||
return query;
|
||||
}
|
||||
|
||||
@@ -9,12 +9,21 @@ public class DashboardSummaryVo {
|
||||
|
||||
private Long activeUserTotal;
|
||||
|
||||
private Long botTotal;
|
||||
/** 智能体总数。 */
|
||||
private Long agentTotal;
|
||||
|
||||
private Long workflowTotal;
|
||||
|
||||
private Long knowledgeBaseTotal;
|
||||
|
||||
private Long chatMessageTotal;
|
||||
|
||||
private Long chatSessionTotal;
|
||||
|
||||
private Long activeAssistantTotal;
|
||||
|
||||
private Long chatActiveUserTotal;
|
||||
|
||||
public Long getUserTotal() {
|
||||
return userTotal;
|
||||
}
|
||||
@@ -31,12 +40,22 @@ public class DashboardSummaryVo {
|
||||
this.activeUserTotal = activeUserTotal;
|
||||
}
|
||||
|
||||
public Long getBotTotal() {
|
||||
return botTotal;
|
||||
/**
|
||||
* 获取智能体总数。
|
||||
*
|
||||
* @return 智能体总数
|
||||
*/
|
||||
public Long getAgentTotal() {
|
||||
return agentTotal;
|
||||
}
|
||||
|
||||
public void setBotTotal(Long botTotal) {
|
||||
this.botTotal = botTotal;
|
||||
/**
|
||||
* 设置智能体总数。
|
||||
*
|
||||
* @param agentTotal 智能体总数
|
||||
*/
|
||||
public void setAgentTotal(Long agentTotal) {
|
||||
this.agentTotal = agentTotal;
|
||||
}
|
||||
|
||||
public Long getWorkflowTotal() {
|
||||
@@ -54,4 +73,36 @@ public class DashboardSummaryVo {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,10 @@ public class DashboardTrendItemVo {
|
||||
|
||||
private Long activeUserTotal;
|
||||
|
||||
private Long chatMessageTotal;
|
||||
|
||||
private Long chatSessionTotal;
|
||||
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
@@ -34,4 +38,20 @@ public class DashboardTrendItemVo {
|
||||
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;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user