mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-23 23:53:55 +08:00
Compare commits
382
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f7665d92d | ||
|
|
1b5c00231a | ||
|
|
95acba1126 | ||
|
|
1e6f0fd1be | ||
|
|
eb571fae57 | ||
|
|
2946be7284 | ||
|
|
425cb1d6bd | ||
|
|
cf9b0ec712 | ||
|
|
1b053dc9c8 | ||
|
|
d7ad656bd0 | ||
|
|
7dae67a57b | ||
|
|
7b0f59a747 | ||
|
|
2eaa5bbb11 | ||
|
|
c693ff1564 | ||
|
|
e9bf006d9d | ||
|
|
bef96f36e2 | ||
|
|
bebb7eac69 | ||
|
|
a787130b74 | ||
|
|
8b7f16b9c2 | ||
|
|
9ed66ec013 | ||
|
|
05c22cdf2a | ||
|
|
4bc0b67cd7 | ||
|
|
0a6f811448 | ||
|
|
5aeb55fffd | ||
|
|
c33ee9442e | ||
|
|
4670ad2925 | ||
|
|
b542388053 | ||
|
|
a71ce53a68 | ||
|
|
dd88107131 | ||
|
|
8b847a3826 | ||
|
|
036b89f24e | ||
|
|
354280d430 | ||
|
|
e2d16ec8d4 | ||
|
|
6a0192de27 | ||
|
|
ac6eeb5c1f | ||
|
|
23e213f6ea | ||
|
|
382c948482 | ||
|
|
a67d938523 | ||
|
|
26aca54f4b | ||
|
|
4717b8beac | ||
|
|
cd580b6f81 | ||
|
|
70740af7a6 | ||
|
|
7b30c25923 | ||
|
|
7db13b8be2 | ||
|
|
a0df41ac24 | ||
|
|
a432c295d3 | ||
|
|
ab8a35e233 | ||
|
|
160b67197f | ||
|
|
3510282cd5 | ||
|
|
bdf7c7facf | ||
|
|
151a0c1b99 | ||
|
|
52af722463 | ||
|
|
fa91c97685 | ||
|
|
a92d6ccdfb | ||
|
|
f5ce755fea | ||
|
|
606a42cfb2 | ||
|
|
f85e863144 | ||
|
|
550397de98 | ||
|
|
ff66b815a5 | ||
|
|
8c18e48b97 | ||
|
|
4acd02e2b0 | ||
|
|
ddfbacc717 | ||
|
|
2a4dbdb69b | ||
|
|
e9a2489e48 | ||
|
|
ce13a7e3e6 | ||
|
|
8a156e99f4 | ||
|
|
4313216816 | ||
|
|
8c0899415a | ||
|
|
3736a5845a | ||
|
|
dc7615c008 | ||
|
|
b03f9374ea | ||
|
|
cacfc9bfee | ||
|
|
ce3c8a2bd8 | ||
|
|
e1a7e50e1f | ||
|
|
90888de0ba | ||
|
|
ad4d32a05d | ||
|
|
08d6bb4056 | ||
|
|
1f0db7dc47 | ||
|
|
6f52d004e8 | ||
|
|
0681550bab | ||
|
|
97b9c95c2d | ||
|
|
6111c3b5a0 | ||
|
|
123c1e8880 | ||
|
|
6ce4503b22 | ||
|
|
b209de36f5 | ||
|
|
fc3e0869fd | ||
|
|
3436d50b43 | ||
|
|
7a58c04df3 | ||
|
|
150be65436 | ||
|
|
bacf4cd5e8 | ||
|
|
7049e8cce7 | ||
|
|
61f137ce4b | ||
|
|
35ea14b95b | ||
|
|
b7d07f92a2 | ||
|
|
d2cbec0981 | ||
|
|
6348f12d60 | ||
|
|
11caf6bc8a | ||
|
|
3cae9e3c32 | ||
|
|
0161e642d0 | ||
|
|
d41938fe07 | ||
|
|
e20c59e3df | ||
|
|
fbaf28de5c | ||
|
|
2daf492e00 | ||
|
|
ac9083cfc7 | ||
|
|
f36e605aed | ||
|
|
c1b836aaed | ||
|
|
07170f09eb | ||
|
|
8211de242f | ||
|
|
eea6807355 | ||
|
|
0ba7e81438 | ||
|
|
703c707b4c | ||
|
|
3ab14578c2 | ||
|
|
a15efa3090 | ||
|
|
8af8930997 | ||
|
|
ad01b05940 | ||
|
|
a0497fdc48 | ||
|
|
9634f84a98 | ||
|
|
27e5e1e9d7 | ||
|
|
8fd92729f2 | ||
|
|
736ffd80bf | ||
|
|
3783351f5d | ||
|
|
2cd9484c54 | ||
|
|
0f1e0d0d2e | ||
|
|
0e23672be7 | ||
|
|
a6904f6ebd | ||
|
|
784ba359f2 | ||
|
|
e08eb48d9a | ||
|
|
b9a1b154b7 | ||
|
|
64630e1b4e | ||
|
|
7dcc014ce4 | ||
|
|
389e8559aa | ||
|
|
c5f82369e5 | ||
|
|
52e63706a4 | ||
|
|
a42a8b2b4b | ||
|
|
11142104af | ||
|
|
a9f5f1ab81 | ||
|
|
a8f68c42fc | ||
|
|
1d3afb3c9a | ||
|
|
943622969a | ||
|
|
9d39168a35 | ||
|
|
61ab7f25d7 | ||
|
|
140fcd5887 | ||
|
|
3c2024d00d | ||
|
|
2aee1bf567 | ||
|
|
d89a26889c | ||
|
|
d611b2db73 | ||
|
|
b0e911b248 | ||
|
|
fb3b7830ee | ||
|
|
10eeffdf31 | ||
|
|
17847188dd | ||
|
|
7b2bfc087d | ||
|
|
24a8839753 | ||
|
|
64d25533d9 | ||
|
|
6f6c788cd5 | ||
|
|
8aa12d0961 | ||
|
|
d770632152 | ||
|
|
a30903b644 | ||
|
|
5469168d18 | ||
|
|
3c31231781 | ||
|
|
cc2e30205f | ||
|
|
f01de027cd | ||
|
|
fb195fc57b | ||
|
|
cc504fe542 | ||
|
|
fd93116d69 | ||
|
|
5622a13735 | ||
|
|
079c8630d9 | ||
|
|
11830b2f22 | ||
|
|
861b4dee4c | ||
|
|
603fa67824 | ||
|
|
4d8279d916 | ||
|
|
f91c4acb8f | ||
|
|
1342eccf86 | ||
|
|
87f9ab7bb4 | ||
|
|
94cc8564aa | ||
|
|
8fc2d05c99 | ||
|
|
1a6d9d7490 | ||
|
|
6c19f1bd26 | ||
|
|
6d89ae1c66 | ||
|
|
2a49f0804c | ||
|
|
d64bb36606 | ||
|
|
852e097e0d | ||
|
|
3aecb99f70 | ||
|
|
6eecf5d0cb | ||
|
|
a4d02ca2ab | ||
|
|
7ef5a67119 | ||
|
|
1c5dacd2d5 | ||
|
|
7732b7b1b9 | ||
|
|
6e7b938f51 | ||
|
|
52f5859b1f | ||
|
|
73d174bc4a | ||
|
|
9512fe642c | ||
|
|
0ef3a60a4c | ||
|
|
f40dc4a658 | ||
|
|
4747e54f6c | ||
|
|
9278fdb016 | ||
|
|
5b23804954 | ||
|
|
b2ed5eb2c0 | ||
|
|
07113dc4d2 | ||
|
|
1a5dd2d9d9 | ||
|
|
8a01432e9d | ||
|
|
7a39fd0152 | ||
|
|
0d2cb7b3c9 | ||
|
|
9123a174da | ||
|
|
2e165747d6 | ||
|
|
3bdf634e37 | ||
|
|
20ac6f3f96 | ||
|
|
818ac1eefb | ||
|
|
980930cd42 | ||
|
|
956ae37e56 | ||
|
|
15731892ad | ||
|
|
d6d50e0432 | ||
|
|
286079c93a | ||
|
|
860cbc36fd | ||
|
|
2ac7e5be9c | ||
|
|
ebfcfe148d | ||
|
|
435860d339 | ||
|
|
b7cb97a373 | ||
|
|
e7b1daf021 | ||
|
|
bbf2febd6d | ||
|
|
586520f321 | ||
|
|
22262be7cd | ||
|
|
9021eac54e | ||
|
|
b72967bcc3 | ||
|
|
469f836a82 | ||
|
|
1e8960f37c | ||
|
|
7aad8da6f6 | ||
|
|
dd24dc0abc | ||
|
|
4f6cdc745f | ||
|
|
bcf03a0378 | ||
|
|
4f615b3807 | ||
|
|
48087582c2 | ||
|
|
fbb0e02fdd | ||
|
|
d7ef85f6d2 | ||
|
|
2f2bd38f1e | ||
|
|
bc48a169f4 | ||
|
|
e777db6ef3 | ||
|
|
60498b24ae | ||
|
|
caae839b1d | ||
|
|
6a14d694b0 | ||
|
|
d86387971a | ||
|
|
ff1076deac | ||
|
|
5b986bf57f | ||
|
|
f1b2080d99 | ||
|
|
c2b95ca896 | ||
|
|
ed41de7b8e | ||
|
|
42a132f5cd | ||
|
|
7b2f6c2eee | ||
|
|
ee5a53d67d | ||
|
|
f691e9f0c9 | ||
|
|
7a32c208fb | ||
|
|
6984763ddc | ||
|
|
d6c3659d70 | ||
|
|
d51c2a241c | ||
|
|
a1d37b61e4 | ||
|
|
8b24151756 | ||
|
|
a9854756ec | ||
|
|
f95bed4189 | ||
|
|
f7bf29a5e4 | ||
|
|
5913dbbb7b | ||
|
|
f6bb55ae90 | ||
|
|
33e712b5fb | ||
|
|
1c26faddc4 | ||
|
|
ac887139ce | ||
|
|
bc2c68e89f | ||
|
|
cdc2363af1 | ||
|
|
ba07c623df | ||
|
|
344a73b87b | ||
|
|
9a81b6c0ee | ||
|
|
96f65e7c35 | ||
|
|
ff2cdeb483 | ||
|
|
fe8b82fcda | ||
|
|
f007e6614d | ||
|
|
0eafd37bf6 | ||
|
|
af3bc09403 | ||
|
|
90ebb8d499 | ||
|
|
f2c6cb8641 | ||
|
|
116a6973cf | ||
|
|
769407a5b0 | ||
|
|
a4583e273a | ||
|
|
645462abd0 | ||
|
|
b49c4520f9 | ||
|
|
acf45ae8ad | ||
|
|
0d1c18f5dd | ||
|
|
7b020ba55c | ||
|
|
cc081bc9eb | ||
|
|
187c2ee977 | ||
|
|
00ab407b04 | ||
|
|
bda7a90551 | ||
|
|
71b72f5b51 | ||
|
|
61fd8b21e0 | ||
|
|
9a4610ab04 | ||
|
|
fd8296262e | ||
|
|
63dfcf6659 | ||
|
|
8897b4f3c6 | ||
|
|
8d46585101 | ||
|
|
6967b2bce2 | ||
|
|
16a38bbfc4 | ||
|
|
861df73eab | ||
|
|
fa9809bee1 | ||
|
|
2a93745d01 | ||
|
|
85fab64588 | ||
|
|
b22f430e98 | ||
|
|
c3da3e937e | ||
|
|
4ea3bea85c | ||
|
|
520e24a54e | ||
|
|
e82cc53bfb | ||
|
|
3bddc1c722 | ||
|
|
f68fc5c90d | ||
|
|
ea8001a9c8 | ||
|
|
ca10d7290d | ||
|
|
7b3db72285 | ||
|
|
3639871dd2 | ||
|
|
5fb7fbe08f | ||
|
|
73f8a0977c | ||
|
|
87b99e0353 | ||
|
|
795dcec698 | ||
|
|
4f61d21d17 | ||
|
|
8a84428724 | ||
|
|
46f1dee20e | ||
|
|
75e24af858 | ||
|
|
fae6db2198 | ||
|
|
32aea32e2c | ||
|
|
63d9dae18b | ||
|
|
041c254ea4 | ||
|
|
378d68aba9 | ||
|
|
4076136fe4 | ||
|
|
1e6e250dac | ||
|
|
42b495d556 | ||
|
|
c88d709d56 | ||
|
|
fa523dc615 | ||
|
|
8718a07c7e | ||
|
|
39124d4930 | ||
|
|
8dbc0b9689 | ||
|
|
9fd3501605 | ||
|
|
4f72a516d3 | ||
|
|
e4cfabeb7e | ||
|
|
85a83727fd | ||
|
|
a31e16c1d9 | ||
|
|
3ab5dc49ac | ||
|
|
005dc55db3 | ||
|
|
ba1cf14ed4 | ||
|
|
6da32a60c0 | ||
|
|
ea9778f422 | ||
|
|
f0b176768d | ||
|
|
5aa671d6ff | ||
|
|
d02d094219 | ||
|
|
f5b4bae927 | ||
|
|
19fdf5d917 | ||
|
|
9682f615cf | ||
|
|
8162adeca1 | ||
|
|
ba2749430f | ||
|
|
e3088f4410 | ||
|
|
614b662e3d | ||
|
|
f5b9995f89 | ||
|
|
0ddd9ba8ff | ||
|
|
9c8d614d78 | ||
|
|
0075550229 | ||
|
|
4a547be882 | ||
|
|
22d0dcdf58 | ||
|
|
122d1def1d | ||
|
|
0f16a8d31f | ||
|
|
4b573fb4e2 | ||
|
|
6ae0af278b | ||
|
|
15650e1a6c | ||
|
|
9368885355 | ||
|
|
bfa638e17f | ||
|
|
3bdccfea62 | ||
|
|
5fa50339b4 | ||
|
|
f0c95bc987 | ||
|
|
8edc23b888 | ||
|
|
c4fc510a27 | ||
|
|
134000996b | ||
|
|
111909c26d | ||
|
|
f66966d72f | ||
|
|
06a2e6fbfa | ||
|
|
c8a28e98d6 | ||
|
|
564adbd851 | ||
|
|
ad93f43145 | ||
|
|
2bf1123647 | ||
|
|
a6ff6426e9 | ||
|
|
de7aedab96 | ||
|
|
dadf05ab4e |
@@ -3,6 +3,9 @@ __pycache__/
|
||||
.idea/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
.vscode
|
||||
.claude
|
||||
AGENTS.md
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
@@ -146,6 +149,7 @@ tmp
|
||||
.history
|
||||
.DS_Store
|
||||
main/xiaozhi-server/data
|
||||
main/xiaozhi-server/.claude
|
||||
|
||||
main/manager-web/node_modules
|
||||
.config.yaml
|
||||
|
||||
+2
-1
@@ -23,7 +23,8 @@ RUN apk update && \
|
||||
bash \
|
||||
fontconfig \
|
||||
ttf-dejavu \
|
||||
&& rm -rf /var/cache/apk/*
|
||||
&& rm -rf /var/cache/apk/* \
|
||||
&& mkdir -p /run/nginx /var/log/nginx /var/tmp/nginx /etc/nginx/conf.d
|
||||
|
||||
# 复制项目自带的中文字体
|
||||
COPY main/manager-web/public/generator/static/fonts/*.ttf /usr/share/fonts/
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
<a href="./README_en.md"><img alt="README in English" src="https://img.shields.io/badge/English-DFE0E5"></a>
|
||||
<a href="./README_vi.md"><img alt="Tiếng Việt" src="https://img.shields.io/badge/Tiếng Việt-DFE0E5"></a>
|
||||
<a href="./README_de.md"><img alt="Deutsch" src="https://img.shields.io/badge/Deutsch-DFE0E5"></a>
|
||||
<a href="./README_pt_BR.md"><img alt="Português (Brasil)" src="https://img.shields.io/badge/Português (Brasil)-DFE0E5"></a>
|
||||
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/releases">
|
||||
<img alt="GitHub Contributors" src="https://img.shields.io/github/v/release/xinnan-tech/xiaozhi-esp32-server?logo=docker" />
|
||||
</a>
|
||||
@@ -213,8 +214,8 @@ Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/
|
||||
|:---:|:---:|:---:|
|
||||
| ASR(语音识别) | FunASR(本地) | 👍XunfeiStreamASR(讯飞流式) |
|
||||
| LLM(大模型) | glm-4-flash(智谱) | 👍qwen-flash(阿里百炼) |
|
||||
| VLLM(视觉大模型) | glm-4v-flash(智谱) | 👍qwen2.5-vl-3b-instructh(阿里百炼) |
|
||||
| TTS(语音合成) | ✅LinkeraiTTS(灵犀流式) | 👍HuoshanDoubleStreamTTS(火山流式) |
|
||||
| VLLM(视觉大模型) | glm-4v-flash(智谱) | 👍qwen3.5-flash(阿里百炼) |
|
||||
| TTS(语音合成) | EdgeTTS(微软) | 👍HuoshanDoubleStreamTTS(火山流式) |
|
||||
| Intent(意图识别) | function_call(函数调用) | function_call(函数调用) |
|
||||
| Memory(记忆功能) | mem_local_short(本地短期记忆) | mem_local_short(本地短期记忆) |
|
||||
|
||||
@@ -242,7 +243,7 @@ Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/
|
||||
| 智能对话 | 支持多种LLM(大语言模型),实现智能对话 |
|
||||
| 视觉感知 | 支持多种VLLM(视觉大模型),实现多模态交互 |
|
||||
| 意图识别 | 支持外挂的大模型意图识别、大模型自主函数调用,提供插件化意图处理机制 |
|
||||
| 记忆系统 | 支持本地短期记忆、mem0ai接口记忆,具备记忆总结功能 |
|
||||
| 记忆系统 | 支持本地短期记忆、mem0ai接口记忆、PowerMem智能记忆,具备记忆总结功能 |
|
||||
| 知识库 | 支持RAGFlow知识库,让大模型判断需要调度知识库后再回答 |
|
||||
| 工具调用 | 支持客户端IOT协议、客户MCP协议、服务端MCP协议、MCP接入点协议、自定义工具函数 |
|
||||
| 指令下发 | 依托MQTT协议,支持从智控台将MCP指令下发到ESP32设备 |
|
||||
@@ -330,6 +331,7 @@ Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/
|
||||
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|
||||
|:------:|:---------------:|:----:|:---------:|:--:|
|
||||
| Memory | mem0ai | 接口调用 | 1000次/月额度 | |
|
||||
| Memory | [powermem](./docs/powermem-integration.md) | 本地总结 | 取决于LLM和DB | OceanBase开源,支持智能检索 |
|
||||
| Memory | mem_local_short | 本地总结 | 免费 | |
|
||||
| Memory | nomem | 无记忆模式 | 免费 | |
|
||||
|
||||
|
||||
+6
-4
@@ -21,6 +21,7 @@ Unterstützt MQTT+UDP-Protokoll, Websocket-Protokoll, MCP-Endpunkte und Stimmabd
|
||||
<a href="./README_en.md"><img alt="README in English" src="https://img.shields.io/badge/English-DFE0E5"></a>
|
||||
<a href="./README_vi.md"><img alt="Tiếng Việt" src="https://img.shields.io/badge/Tiếng Việt-DFE0E5"></a>
|
||||
<a href="./README_de.md"><img alt="Deutsch" src="https://img.shields.io/badge/Deutsch-DBEDFA"></a>
|
||||
<a href="./README_pt_BR.md"><img alt="Português (Brasil)" src="https://img.shields.io/badge/Português (Brasil)-DFE0E5"></a>
|
||||
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/releases">
|
||||
<img alt="GitHub Contributors" src="https://img.shields.io/github/v/release/xinnan-tech/xiaozhi-esp32-server?logo=docker" />
|
||||
</a>
|
||||
@@ -211,8 +212,8 @@ Websocket-Schnittstellenadresse: wss://2662r3426b.vicp.fun/xiaozhi/v1/
|
||||
|:---:|:---:|:---:|
|
||||
| ASR (Spracherkennung) | FunASR (Lokal) | 👍XunfeiStreamASR (Xunfei-Streaming) |
|
||||
| LLM (Großes Modell) | glm-4-flash (Zhipu) | 👍qwen-flash (Alibaba Bailian) |
|
||||
| VLLM (Vision Large Model) | glm-4v-flash (Zhipu) | 👍qwen2.5-vl-3b-instructh (Alibaba Bailian) |
|
||||
| TTS (Sprachsynthese) | ✅LinkeraiTTS (Lingxi-Streaming) | 👍HuoshanDoubleStreamTTS (Volcano-Streaming) |
|
||||
| VLLM (Vision Large Model) | glm-4v-flash (Zhipu) | 👍qwen3.5-flash (Alibaba Bailian) |
|
||||
| TTS (Sprachsynthese) | EdgeTTS (Microsoft) | 👍HuoshanDoubleStreamTTS (Volcano-Streaming) |
|
||||
| Intent (Absichtserkennung) | function_call (Funktionsaufruf) | function_call (Funktionsaufruf) |
|
||||
| Memory (Gedächtnisfunktion) | mem_local_short (Lokales Kurzzeitgedächtnis) | mem_local_short (Lokales Kurzzeitgedächtnis) |
|
||||
|
||||
@@ -240,7 +241,7 @@ Dieses Projekt bietet die folgenden Testwerkzeuge, um Ihnen bei der Überprüfun
|
||||
| Intelligenter Dialog | Unterstützt mehrere LLM (große Sprachmodelle), implementiert intelligenten Dialog |
|
||||
| Visuelle Wahrnehmung | Unterstützt mehrere VLLM (Vision Large Models), implementiert multimodale Interaktion |
|
||||
| Absichtserkennung | Unterstützt LLM-Absichtserkennung, Function Call-Funktionsaufruf, bietet plugin-basierten Absichtsverarbeitungsmechanismus |
|
||||
| Gedächtnissystem | Unterstützt lokales Kurzzeitgedächtnis, mem0ai-Schnittstellengedächtnis, mit Gedächtniszusammenfassungsfunktion |
|
||||
| Gedächtnissystem | Unterstützt lokales Kurzzeitgedächtnis, mem0ai-Schnittstellengedächtnis, PowerMem intelligentes Gedächtnis, mit Gedächtniszusammenfassungsfunktion |
|
||||
| Wissensdatenbank | Unterstützt RAGFlow-Wissensdatenbank, ermöglicht großem Modell die Bewertung, ob Wissensdatenbank benötigt wird, bevor geantwortet wird |
|
||||
| Werkzeugaufruf | Unterstützt Client-IOT-Protokoll, Client-MCP-Protokoll, Server-MCP-Protokoll, MCP-Endpunktprotokoll, benutzerdefinierte Werkzeugfunktionen |
|
||||
| Befehlsübermittlung | Basierend auf MQTT-Protokoll, unterstützt die Übermittlung von MCP-Befehlen von der intelligenten Steuerkonsole an ESP32-Geräte |
|
||||
@@ -258,7 +259,7 @@ Wenn Sie ein Softwareentwickler sind, finden Sie hier einen [Offenen Brief an En
|
||||
---
|
||||
|
||||
## Produktökosystem 👬
|
||||
Xiaozhi ist ein Ökosystem. Wenn Sie dieses Produkt verwenden, können Sie sich auch andere [hervorragende Projekte](https://github.com/78/xiaozhi-esp32?tab=readme-ov-file#related-open-source-projects) in diesem Ökosystem ansehen
|
||||
Xiaozhi ist ein Ökosystem. Wenn Sie dieses Produkt verwenden, können Sie sich auch andere [hervorragende Projekte](https://github.com/78/xiaozhi-esp32/blob/main/README_zh.md#%E7%9B%B8%E5%85%B3%E5%BC%80%E6%BA%90%E9%A1%B9%E7%9B%AE) in diesem Ökosystem ansehen
|
||||
|
||||
---
|
||||
|
||||
@@ -328,6 +329,7 @@ Tatsächlich kann jedes VLLM, das OpenAI-Schnittstellenaufrufe unterstützt, int
|
||||
| Typ | Plattformname | Verwendungsmethode | Preismodell | Hinweise |
|
||||
|:------:|:---------------:|:----:|:---------:|:--:|
|
||||
| Memory | mem0ai | Schnittstellenaufrufe | 1000 Mal/Monat Kontingent | |
|
||||
| Memory | [powermem](./docs/powermem-integration.md) | Lokale Zusammenfassung | Abhängig von LLM und DB | OceanBase Open Source, unterstützt intelligente Abfrage |
|
||||
| Memory | mem_local_short | Lokale Zusammenfassung | Kostenlos | |
|
||||
| Memory | nomem | Kein Gedächtnismodus | Kostenlos | |
|
||||
|
||||
|
||||
+19
-17
@@ -21,6 +21,7 @@ Support for MQTT+UDP protocol, Websocket protocol, MCP access point, voiceprint
|
||||
<a href="./README_en.md"><img alt="README in English" src="https://img.shields.io/badge/English-DBEDFA"></a>
|
||||
<a href="./README_vi.md"><img alt="Tiếng Việt" src="https://img.shields.io/badge/Tiếng Việt-DFE0E5"></a>
|
||||
<a href="./README_de.md"><img alt="Deutsch" src="https://img.shields.io/badge/Deutsch-DFE0E5"></a>
|
||||
<a href="./README_pt_BR.md"><img alt="Português (Brasil)" src="https://img.shields.io/badge/Português (Brasil)-DFE0E5"></a>
|
||||
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/releases">
|
||||
<img alt="GitHub Contributors" src="https://img.shields.io/github/v/release/xinnan-tech/xiaozhi-esp32-server?logo=docker" />
|
||||
</a>
|
||||
@@ -182,8 +183,9 @@ This project provides two deployment methods. Please choose based on your specif
|
||||
| Deployment Method | Features | Applicable Scenarios | Deployment Docs | Configuration Requirements | Video Tutorials |
|
||||
|---------|------|---------|---------|---------|---------|
|
||||
| **Simplified Installation** | Intelligent dialogue, single agent management | Low-configuration environments, data stored in config files, no database required | [①Docker Version](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E5%8F%AA%E8%BF%90%E8%A1%8Cserver) / [②Source Code Deployment](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E5%8F%AA%E8%BF%90%E8%A1%8Cserver)| 2 cores 4GB if using `FunASR`, 2 cores 2GB if all APIs | - |
|
||||
| **Full Module Installation** | Intelligent dialogue, multi-user management, multi-agent management, intelligent console interface operation | Complete functionality experience, data stored in database |[①Docker Version](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [②Source Code Deployment](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [③Source Code Deployment Auto-Update Tutorial](./docs/dev-ops-integration.md) | 4 cores 8GB if using `FunASR`, 2 cores 4GB if all APIs| [Local Source Code Startup Video Tutorial](https://www.bilibili.com/video/BV1wBJhz4Ewe) |
|
||||
| **Full Module Installation** | Intelligent dialogue, multi-user management, multi-agent management, intelligent console interface operation | Complete functionality experience, data stored in database |[①Docker Version](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [②Source Code Deployment](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [③Source Code Deployment Auto-Update Tutorial](./docs/dev-ops-integration.md) | 4 cores 8GB if using `FunASR`, 2 cores 4GB if all APIs| [Local Source Code Startup Video Tutorial](https://www.bilibili.com/video/BV1wBJhz4Ewe) |
|
||||
|
||||
For frequently asked questions and related tutorials, please refer to [this link](./docs/FAQ.md)
|
||||
|
||||
> 💡 Note: Below is a test platform deployed with the latest code. You can burn and test if needed. Concurrent users: 6, data will be cleared daily.
|
||||
|
||||
@@ -210,19 +212,20 @@ Websocket Interface Address: wss://2662r3426b.vicp.fun/xiaozhi/v1/
|
||||
|:---:|:---:|:---:|
|
||||
| ASR(Speech Recognition) | FunASR(Local) | 👍XunfeiStreamASR(Xunfei Streaming) |
|
||||
| LLM(Large Model) | glm-4-flash(Zhipu) | 👍qwen-flash(Alibaba Bailian) |
|
||||
| VLLM(Vision Large Model) | glm-4v-flash(Zhipu) | 👍qwen2.5-vl-3b-instructh(Alibaba Bailian) |
|
||||
| TTS(Speech Synthesis) | ✅LinkeraiTTS(Lingxi streaming) | 👍HuoshanDoubleStreamTTS(Volcano Streaming) |
|
||||
| VLLM(Vision Large Model) | glm-4v-flash(Zhipu) | 👍qwen3.5-flash(Alibaba Bailian) |
|
||||
| TTS(Speech Synthesis) | EdgeTTS(Microsoft) | 👍HuoshanDoubleStreamTTS(Volcano Streaming) |
|
||||
| Intent(Intent Recognition) | function_call(Function calling) | function_call(Function calling) |
|
||||
| Memory(Memory function) | mem_local_short(Local short-term memory) | mem_local_short(Local short-term memory) |
|
||||
|
||||
If you are concerned about the latency of each component, please refer to the [Xiaozhi Component Performance Test Report](https://github.com/xinnan-tech/xiaozhi-performance-research), and test in your own environment following the test methods in the report.
|
||||
|
||||
#### 🔧 Testing Tools
|
||||
This project provides the following testing tools to help you verify the system and choose suitable models:
|
||||
|
||||
| Tool Name | Location | Usage Method | Function Description |
|
||||
|:---:|:---|:---:|:---:|
|
||||
| Audio Interaction Test Tool | main》xiaozhi-server》test》test_page.html | Open directly with Google Chrome | Tests audio playback and reception functions, verifies if Python-side audio processing is normal |
|
||||
| Model Response Test Tool 1 | main》xiaozhi-server》performance_tester.py | Execute `python performance_tester.py` | Tests response speed of three core modules: ASR(speech recognition), LLM(large model), TTS(speech synthesis) |
|
||||
| Model Response Test Tool 2 | main》xiaozhi-server》performance_tester_vllm.py | Execute `python performance_tester_vllm.py` | Tests VLLM(vision model) response speed |
|
||||
| Model Response Test Tool | main》xiaozhi-server》performance_tester.py | Execute `python performance_tester.py` | Tests response speed of three core modules: ASR(speech recognition), LLM(large model), VLLM(vision model), TTS(speech synthesis) |
|
||||
|
||||
> 💡 Note: When testing model speed, only models with configured keys will be tested.
|
||||
|
||||
@@ -238,10 +241,10 @@ This project provides the following testing tools to help you verify the system
|
||||
| Intelligent Dialogue | Supports multiple LLM(large language models), implements intelligent dialogue |
|
||||
| Visual Perception | Supports multiple VLLM(vision large models), implements multimodal interaction |
|
||||
| Intent Recognition | Supports LLM intent recognition, Function Call function calling, provides plugin-based intent processing mechanism |
|
||||
| Memory System | Supports local short-term memory, mem0ai interface memory, with memory summarization functionality |
|
||||
| Memory System | Supports local short-term memory, mem0ai interface memory, PowerMem intelligent memory, with memory summarization functionality |
|
||||
| Knowledge Base | Supports RAGFlow knowledge base, enabling LLM to judge whether to schedule the knowledge base after receiving the user's question, and then answer the question |
|
||||
| Command Delivery | Supports MCP command delivery to ESP32 devices via MQTT protocol from Smart Console |
|
||||
| Tool Calling | Supports client IOT protocol, client MCP protocol, server MCP protocol, MCP endpoint protocol, custom tool functions |
|
||||
| Command Delivery | Supports MCP command delivery to ESP32 devices via MQTT protocol from Smart Console |
|
||||
| Management Backend | Provides Web management interface, supports user management, system configuration and device management; Supports Simplified Chinese, Traditional Chinese and English display |
|
||||
| Testing Tools | Provides performance testing tools, vision model testing tools, and audio interaction testing tools |
|
||||
| Deployment Support | Supports Docker deployment and local deployment, provides complete configuration file management |
|
||||
@@ -249,20 +252,14 @@ This project provides the following testing tools to help you verify the system
|
||||
|
||||
### Under Development 🚧
|
||||
|
||||
To learn about specific development plan progress, [click here](https://github.com/users/xinnan-tech/projects/3)
|
||||
To learn about specific development plan progress, [click here](https://github.com/users/xinnan-tech/projects/3). For frequently asked questions and related tutorials, please refer to [this link](./docs/FAQ.md)
|
||||
|
||||
If you are a software developer, here is an [Open Letter to Developers](docs/contributor_open_letter.md). Welcome to join!
|
||||
|
||||
---
|
||||
|
||||
## Product Ecosystem 👬
|
||||
Xiaozhi is an ecosystem. When using this product, you can also check out other [excellent projects](https://github.com/78/xiaozhi-esp32?tab=readme-ov-file#related-open-source-projects) in this ecosystem
|
||||
|
||||
| Project Name | Project Address | Project Description |
|
||||
|:---------------------|:--------|:--------|
|
||||
| Xiaozhi Android Client | [xiaozhi-android-client](https://github.com/TOM88812/xiaozhi-android-client) | An Android and iOS voice dialogue application based on xiaozhi-server, supporting real-time voice interaction and text dialogue.<br/>Currently a Flutter version, connecting iOS and Android platforms. |
|
||||
| Xiaozhi Desktop Client | [py-xiaozhi](https://github.com/Huang-junsen/py-xiaozhi) | This project provides a Python-based AI client for beginners, allowing users to experience Xiaozhi AI functionality through code even without physical hardware conditions. |
|
||||
| Xiaozhi Java Server | [xiaozhi-esp32-server-java](https://github.com/joey-zhou/xiaozhi-esp32-server-java) | Xiaozhi open-source backend service Java version is a Java-based open-source project.<br/>It includes frontend and backend services, aiming to provide users with a complete backend service solution. |
|
||||
Xiaozhi is an ecosystem. When using this product, you can also check out other [excellent projects](https://github.com/78/xiaozhi-esp32/blob/main/README_zh.md#%E7%9B%B8%E5%85%B3%E5%BC%80%E6%BA%90%E9%A1%B9%E7%9B%AE) in this ecosystem
|
||||
|
||||
---
|
||||
|
||||
@@ -276,8 +273,10 @@ Xiaozhi is an ecosystem. When using this product, you can also check out other [
|
||||
| Dify interface calls | Dify | - |
|
||||
| FastGPT interface calls | FastGPT | - |
|
||||
| Coze interface calls | Coze | - |
|
||||
| Xinference interface calls | Xinference | - |
|
||||
| HomeAssistant interface calls | HomeAssistant | - |
|
||||
|
||||
In fact, any LLM that supports OpenAI interface calls can be integrated and used, including Xinference and HomeAssistant interfaces.
|
||||
In fact, any LLM that supports OpenAI interface calls can be integrated and used.
|
||||
|
||||
---
|
||||
|
||||
@@ -296,7 +295,7 @@ In fact, any VLLM that supports OpenAI interface calls can be integrated and use
|
||||
| Usage Method | Supported Platforms | Free Platforms |
|
||||
|:---:|:---:|:---:|
|
||||
| Interface calls | EdgeTTS, iFLYTEK, Volcano Engine, Tencent Cloud, Alibaba Cloud and Bailian, CosyVoiceSiliconflow, TTS302AI, CozeCnTTS, GizwitsTTS, ACGNTTS, OpenAITTS, Lingxi Streaming TTS, MinimaxTTS | Lingxi Streaming TTS, EdgeTTS, CosyVoiceSiliconflow(partial) |
|
||||
| Local services | FishSpeech, GPT_SOVITS_V2, GPT_SOVITS_V3, MinimaxTTS | FishSpeech, GPT_SOVITS_V2, GPT_SOVITS_V3, MinimaxTTS |
|
||||
| Local services | FishSpeech, GPT_SOVITS_V2, GPT_SOVITS_V3, Index-TTS, PaddleSpeech | Index-TTS, PaddleSpeech, FishSpeech, GPT_SOVITS_V2, GPT_SOVITS_V3 |
|
||||
|
||||
---
|
||||
|
||||
@@ -330,7 +329,9 @@ In fact, any VLLM that supports OpenAI interface calls can be integrated and use
|
||||
| Type | Platform Name | Usage Method | Pricing Model | Notes |
|
||||
|:------:|:---------------:|:----:|:---------:|:--:|
|
||||
| Memory | mem0ai | Interface calls | 1000 times/month quota | |
|
||||
| Memory | [powermem](./docs/powermem-integration.md) | Local summarization | Depends on LLM and DB | OceanBase open source, supports intelligent retrieval |
|
||||
| Memory | mem_local_short | Local summarization | Free | |
|
||||
| Memory | nomem | No memory mode | Free | |
|
||||
|
||||
---
|
||||
|
||||
@@ -340,6 +341,7 @@ In fact, any VLLM that supports OpenAI interface calls can be integrated and use
|
||||
|:------:|:-------------:|:----:|:-------:|:---------------------:|
|
||||
| Intent | intent_llm | Interface calls | Based on LLM pricing | Recognizes intent through large models, strong generalization |
|
||||
| Intent | function_call | Interface calls | Based on LLM pricing | Completes intent through large model function calling, fast speed, good effect |
|
||||
| Intent | nointent | No intent mode | Free | Does not perform intent recognition, directly returns dialogue result |
|
||||
|
||||
---
|
||||
|
||||
|
||||
+376
@@ -0,0 +1,376 @@
|
||||
[](https://github.com/xinnan-tech/xiaozhi-esp32-server)
|
||||
|
||||
<h1 align="center">Serviço Backend Xiaozhi xiaozhi-esp32-server</h1>
|
||||
|
||||
<p align="center">
|
||||
Este projeto é baseado na teoria e tecnologia de inteligência simbiótica humano-máquina para desenvolver sistemas inteligentes de hardware e software para terminais<br/>fornecendo serviços de backend para o projeto de hardware inteligente de código aberto
|
||||
<a href="https://github.com/78/xiaozhi-esp32">xiaozhi-esp32</a><br/>
|
||||
Implementado usando Python, Java e Vue de acordo com o <a href="https://ccnphfhqs21z.feishu.cn/wiki/M0XiwldO9iJwHikpXD5cEx71nKh">Protocolo de Comunicação Xiaozhi</a><br/>
|
||||
Suporte ao protocolo MQTT+UDP, protocolo WebSocket, ponto de acesso MCP, reconhecimento de impressão vocal e base de conhecimento
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="./docs/FAQ.md">Perguntas Frequentes</a>
|
||||
· <a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/issues">Reportar Problemas</a>
|
||||
· <a href="./README.md#%E9%83%A8%E7%BD%B2%E6%96%87%E6%A1%A3">Documentação de Implantação</a>
|
||||
· <a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/releases">Notas de Lançamento</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="./README.md"><img alt="简体中文版自述文件" src="https://img.shields.io/badge/简体中文-DFE0E5"></a>
|
||||
<a href="./README_en.md"><img alt="README in English" src="https://img.shields.io/badge/English-DFE0E5"></a>
|
||||
<a href="./README_vi.md"><img alt="Tiếng Việt" src="https://img.shields.io/badge/Tiếng Việt-DFE0E5"></a>
|
||||
<a href="./README_de.md"><img alt="Deutsch" src="https://img.shields.io/badge/Deutsch-DFE0E5"></a>
|
||||
<a href="./README_pt_BR.md"><img alt="Português (Brasil)" src="https://img.shields.io/badge/Português (Brasil)-DBEDFA"></a>
|
||||
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/releases">
|
||||
<img alt="GitHub Contributors" src="https://img.shields.io/github/v/release/xinnan-tech/xiaozhi-esp32-server?logo=docker" />
|
||||
</a>
|
||||
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/blob/main/LICENSE">
|
||||
<img alt="GitHub pull requests" src="https://img.shields.io/badge/license-MIT-white?labelColor=black" />
|
||||
</a>
|
||||
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server">
|
||||
<img alt="stars" src="https://img.shields.io/github/stars/xinnan-tech/xiaozhi-esp32-server?color=ffcb47&labelColor=black" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
Liderado pela Equipe do Professor Siyuan Liu (Universidade de Tecnologia do Sul da China)
|
||||
</br>
|
||||
刘思源教授团队主导研发(华南理工大学)
|
||||
</br>
|
||||
<img src="./docs/images/hnlg.jpg" alt="Universidade de Tecnologia do Sul da China (华南理工大学)" width="50%">
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## Público-Alvo 👥
|
||||
|
||||
Este projeto requer dispositivos de hardware ESP32 para funcionar. Se você adquiriu hardware relacionado ao ESP32, conectou-se com sucesso ao serviço backend implantado pelo Brother Xia e deseja construir seu próprio serviço backend `xiaozhi-esp32` de forma independente, então este projeto é perfeito para você.
|
||||
|
||||
Quer ver os efeitos de uso? Clique nos vídeos abaixo 🎥
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
|
||||
<picture>
|
||||
<img alt="Experiência de velocidade de resposta" src="docs/images/demo9.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
|
||||
<picture>
|
||||
<img alt="Segredo da otimização de velocidade" src="docs/images/demo6.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
|
||||
<picture>
|
||||
<img alt="Cenário médico complexo" src="docs/images/demo1.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
|
||||
<picture>
|
||||
<img alt="Envio de comandos MQTT" src="docs/images/demo4.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
|
||||
<picture>
|
||||
<img alt="Reconhecimento de impressão vocal" src="docs/images/demo14.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
|
||||
<picture>
|
||||
<img alt="Controle de interruptores de eletrodomésticos" src="docs/images/demo5.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
|
||||
<picture>
|
||||
<img alt="Ponto de acesso MCP" src="docs/images/demo13.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
|
||||
<picture>
|
||||
<img alt="Tarefas com múltiplos comandos" src="docs/images/demo11.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
|
||||
<picture>
|
||||
<img alt="Reproduzir música" src="docs/images/demo7.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1Z8XuYZEAS" target="_blank">
|
||||
<picture>
|
||||
<img alt="Plugin de clima" src="docs/images/demo8.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV12J7WzBEaH" target="_blank">
|
||||
<picture>
|
||||
<img alt="Interrupção em tempo real" src="docs/images/demo10.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1Co76z7EvK" target="_blank">
|
||||
<picture>
|
||||
<img alt="Fotografar e identificar objetos" src="docs/images/demo12.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
|
||||
<picture>
|
||||
<img alt="Timbre de voz personalizado" src="docs/images/demo2.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
|
||||
<picture>
|
||||
<img alt="Comunicação em cantonês" src="docs/images/demo3.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV17LXWYvENb" target="_blank">
|
||||
<picture>
|
||||
<img alt="Transmissão de notícias" src="docs/images/demo0.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
---
|
||||
|
||||
## Avisos ⚠️
|
||||
|
||||
1. Este projeto é um software de código aberto. Este software não possui parceria comercial com nenhum provedor de serviços de API de terceiros (incluindo, mas não se limitando a reconhecimento de fala, modelos de linguagem, síntese de voz e outras plataformas) com os quais se conecta, e não fornece nenhuma forma de garantia quanto à qualidade de serviço ou segurança financeira desses provedores. Recomenda-se que os usuários priorizem provedores de serviço com licenças comerciais relevantes e leiam cuidadosamente seus termos de serviço e políticas de privacidade. Este software não armazena nenhuma chave de conta, não participa de fluxos de fundos e não assume o risco de perda de fundos recarregados.
|
||||
|
||||
2. A funcionalidade deste projeto não está completa e não passou por avaliação de segurança de rede. Por favor, não o utilize em ambientes de produção. Se você implantar este projeto para fins de aprendizado em um ambiente de rede pública, certifique-se de que as medidas de proteção necessárias estejam em vigor.
|
||||
|
||||
---
|
||||
|
||||
## Documentação de Implantação
|
||||
|
||||

|
||||
|
||||
Este projeto oferece dois métodos de implantação. Por favor, escolha de acordo com suas necessidades específicas:
|
||||
|
||||
#### 🚀 Seleção do Método de Implantação
|
||||
| Método de Implantação | Funcionalidades | Cenários Aplicáveis | Documentação de Implantação | Requisitos de Configuração | Tutoriais em Vídeo |
|
||||
|---------|------|---------|---------|---------|---------|
|
||||
| **Instalação Simplificada** | Diálogo inteligente, gerenciamento de agente único | Ambientes de baixa configuração, dados armazenados em arquivos de configuração, sem necessidade de banco de dados | [①Versão Docker](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E5%8F%AA%E8%BF%90%E8%A1%8Cserver) / [②Implantação via Código-Fonte](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E5%8F%AA%E8%BF%90%E8%A1%8Cserver)| 2 núcleos 4GB se usar `FunASR`, 2 núcleos 2GB se todas APIs | - |
|
||||
| **Instalação de Módulo Completo** | Diálogo inteligente, gerenciamento multiusuário, gerenciamento de múltiplos agentes, operação de interface do console inteligente | Experiência com funcionalidade completa, dados armazenados em banco de dados |[①Versão Docker](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [②Implantação via Código-Fonte](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [③Tutorial de Atualização Automática via Código-Fonte](./docs/dev-ops-integration.md) | 4 núcleos 8GB se usar `FunASR`, 2 núcleos 4GB se todas APIs| [Tutorial em Vídeo de Inicialização via Código-Fonte Local](https://www.bilibili.com/video/BV1wBJhz4Ewe) |
|
||||
|
||||
Perguntas frequentes e tutoriais relacionados podem ser consultados [neste link](./docs/FAQ.md)
|
||||
|
||||
> 💡 Nota: Abaixo está uma plataforma de teste implantada com o código mais recente. Você pode gravar e testar se necessário. Usuários simultâneos: 6, os dados serão limpos diariamente.
|
||||
|
||||
```
|
||||
Endereço do Console de Controle Inteligente: https://2662r3426b.vicp.fun
|
||||
Endereço do Console de Controle Inteligente (H5): https://2662r3426b.vicp.fun/h5/index.html
|
||||
|
||||
Ferramenta de Teste de Serviço: https://2662r3426b.vicp.fun/test/
|
||||
Endereço da Interface OTA: https://2662r3426b.vicp.fun/xiaozhi/ota/
|
||||
Endereço da Interface WebSocket: wss://2662r3426b.vicp.fun/xiaozhi/v1/
|
||||
```
|
||||
|
||||
#### 🚩 Descrição e Recomendações de Configuração
|
||||
> [!Note]
|
||||
> Este projeto oferece dois esquemas de configuração:
|
||||
>
|
||||
> 1. `Configurações Gratuitas Nível Básico`: Adequado para uso pessoal e doméstico, todos os componentes utilizam soluções gratuitas, sem necessidade de pagamento adicional.
|
||||
>
|
||||
> 2. `Configuração de Streaming`: Adequado para demonstrações, treinamentos, cenários com mais de 2 usuários simultâneos, etc. Utiliza tecnologia de processamento em streaming para velocidade de resposta mais rápida e melhor experiência.
|
||||
>
|
||||
> A partir da versão `0.5.2`, o projeto suporta configuração de streaming. Em comparação com versões anteriores, a velocidade de resposta é melhorada em aproximadamente `2,5 segundos`, melhorando significativamente a experiência do usuário.
|
||||
|
||||
| Nome do Módulo | Configurações Gratuitas Nível Básico | Configuração de Streaming |
|
||||
|:---:|:---:|:---:|
|
||||
| ASR(Reconhecimento de Fala) | FunASR(Local) | 👍XunfeiStreamASR(Xunfei Streaming) |
|
||||
| LLM(Modelo de Linguagem) | glm-4-flash(Zhipu) | 👍qwen-flash(Alibaba Bailian) |
|
||||
| VLLM(Modelo de Visão) | glm-4v-flash(Zhipu) | 👍qwen3.5-flash(Alibaba Bailian) |
|
||||
| TTS(Síntese de Voz) | EdgeTTS(Microsoft) | 👍HuoshanDoubleStreamTTS(Volcano Streaming) |
|
||||
| Intent(Reconhecimento de Intenção) | function_call(Chamada de função) | function_call(Chamada de função) |
|
||||
| Memory(Função de Memória) | mem_local_short(Memória local de curto prazo) | mem_local_short(Memória local de curto prazo) |
|
||||
|
||||
Se você está preocupado com o tempo de resposta de cada componente, consulte o [Relatório de Teste de Desempenho dos Componentes Xiaozhi](https://github.com/xinnan-tech/xiaozhi-performance-research), e teste em seu próprio ambiente seguindo os métodos de teste do relatório.
|
||||
|
||||
#### 🔧 Ferramentas de Teste
|
||||
Este projeto fornece as seguintes ferramentas de teste para ajudá-lo a verificar o sistema e escolher modelos adequados:
|
||||
|
||||
| Nome da Ferramenta | Localização | Método de Uso | Descrição da Função |
|
||||
|:---:|:---|:---:|:---:|
|
||||
| Ferramenta de Teste de Interação por Áudio | main》xiaozhi-server》test》test_page.html | Abrir diretamente com Google Chrome | Testa as funções de reprodução e recepção de áudio, verifica se o processamento de áudio no lado Python está normal |
|
||||
| Ferramenta de Teste de Resposta de Modelo | main》xiaozhi-server》performance_tester.py | Execute `python performance_tester.py` | Testa a velocidade de resposta dos três módulos principais: ASR(reconhecimento de fala), LLM(modelo de linguagem), VLLM(modelo de visão), TTS(síntese de voz) |
|
||||
|
||||
> 💡 Nota: Ao testar a velocidade dos modelos, apenas os modelos com chaves configuradas serão testados.
|
||||
|
||||
---
|
||||
## Lista de Funcionalidades ✨
|
||||
### Implementado ✅
|
||||

|
||||
| Módulo de Funcionalidade | Descrição |
|
||||
|:---:|:---|
|
||||
| Arquitetura Principal | Baseado em [gateway MQTT+UDP](https://github.com/xinnan-tech/xiaozhi-esp32-server/blob/main/docs/mqtt-gateway-integration.md), servidores WebSocket e HTTP, fornece sistema completo de gerenciamento de console e autenticação |
|
||||
| Interação por Voz | Suporta ASR em streaming (reconhecimento de fala), TTS em streaming (síntese de voz), VAD (detecção de atividade vocal), suporta reconhecimento multilíngue e processamento de voz |
|
||||
| Reconhecimento de Impressão Vocal | Suporta registro, gerenciamento e reconhecimento de impressão vocal de múltiplos usuários, processa em paralelo com o ASR, reconhecimento de identidade do falante em tempo real e repassa ao LLM para respostas personalizadas |
|
||||
| Diálogo Inteligente | Suporta múltiplos LLM (modelos de linguagem de grande porte), implementa diálogo inteligente |
|
||||
| Percepção Visual | Suporta múltiplos VLLM (modelos de visão de grande porte), implementa interação multimodal |
|
||||
| Reconhecimento de Intenção | Suporta reconhecimento de intenção por LLM, Function Call (chamada de função), fornece mecanismo de processamento de intenção baseado em plugins |
|
||||
| Sistema de Memória | Suporta memória local de curto prazo, memória via interface mem0ai, memória inteligente PowerMem, com funcionalidade de resumo de memória |
|
||||
| Base de Conhecimento | Suporta base de conhecimento RAGFlow, permitindo que o LLM julgue se deve acionar a base de conhecimento após receber a pergunta do usuário, e então responda à pergunta |
|
||||
| Chamada de Ferramentas | Suporta protocolo IOT do cliente, protocolo MCP do cliente, protocolo MCP do servidor, protocolo de endpoint MCP, funções de ferramentas personalizadas |
|
||||
| Envio de Comandos | Suporta envio de comandos MCP para dispositivos ESP32 via protocolo MQTT a partir do Console Inteligente |
|
||||
| Backend de Gerenciamento | Fornece interface de gerenciamento Web, suporta gerenciamento de usuários, configuração do sistema e gerenciamento de dispositivos; Suporta exibição em Chinês Simplificado, Chinês Tradicional e Inglês |
|
||||
| Ferramentas de Teste | Fornece ferramentas de teste de desempenho, ferramentas de teste de modelo de visão e ferramentas de teste de interação por áudio |
|
||||
| Suporte à Implantação | Suporta implantação via Docker e implantação local, fornece gerenciamento completo de arquivos de configuração |
|
||||
| Sistema de Plugins | Suporta extensões de plugins funcionais, desenvolvimento de plugins personalizados e carregamento dinâmico de plugins |
|
||||
|
||||
### Em Desenvolvimento 🚧
|
||||
|
||||
Para conhecer o progresso específico do plano de desenvolvimento, [clique aqui](https://github.com/users/xinnan-tech/projects/3). Perguntas frequentes e tutoriais relacionados podem ser consultados [neste link](./docs/FAQ.md)
|
||||
|
||||
Se você é um desenvolvedor de software, aqui está uma [Carta Aberta aos Desenvolvedores](docs/contributor_open_letter.md). Seja bem-vindo a participar!
|
||||
|
||||
---
|
||||
|
||||
## Ecossistema do Produto 👬
|
||||
Xiaozhi é um ecossistema. Ao utilizar este produto, você também pode conferir outros [projetos excelentes](https://github.com/78/xiaozhi-esp32/blob/main/README_zh.md#%E7%9B%B8%E5%85%B3%E5%BC%80%E6%BA%90%E9%A1%B9%E7%9B%AE) neste ecossistema
|
||||
|
||||
---
|
||||
|
||||
## Lista de Plataformas/Componentes Suportados 📋
|
||||
### LLM Modelos de Linguagem
|
||||
|
||||
| Método de Uso | Plataformas Suportadas | Plataformas Gratuitas |
|
||||
|:---:|:---:|:---:|
|
||||
| Chamadas via interface OpenAI | Alibaba Bailian, Volcano Engine, DeepSeek, Zhipu, Gemini, iFLYTEK | Zhipu, Gemini |
|
||||
| Chamadas via interface Ollama | Ollama | - |
|
||||
| Chamadas via interface Dify | Dify | - |
|
||||
| Chamadas via interface FastGPT | FastGPT | - |
|
||||
| Chamadas via interface Coze | Coze | - |
|
||||
| Chamadas via interface Xinference | Xinference | - |
|
||||
| Chamadas via interface HomeAssistant | HomeAssistant | - |
|
||||
|
||||
Na verdade, qualquer LLM que suporte chamadas via interface openai pode ser integrado e utilizado.
|
||||
|
||||
---
|
||||
|
||||
### VLLM Modelos de Visão
|
||||
|
||||
| Método de Uso | Plataformas Suportadas | Plataformas Gratuitas |
|
||||
|:---:|:---:|:---:|
|
||||
| Chamadas via interface OpenAI | Alibaba Bailian, Zhipu ChatGLMVLLM | Zhipu ChatGLMVLLM |
|
||||
|
||||
Na verdade, qualquer VLLM que suporte chamadas via interface OpenAI pode ser integrado e utilizado.
|
||||
|
||||
---
|
||||
|
||||
### TTS Síntese de Voz
|
||||
|
||||
| Método de Uso | Plataformas Suportadas | Plataformas Gratuitas |
|
||||
|:---:|:---:|:---:|
|
||||
| Chamadas via interface | EdgeTTS, iFLYTEK, Volcano Engine, Tencent Cloud, Alibaba Cloud e Bailian, CosyVoiceSiliconflow, TTS302AI, CozeCnTTS, GizwitsTTS, ACGNTTS, OpenAITTS, Lingxi Streaming TTS, MinimaxTTS | Lingxi Streaming TTS, EdgeTTS, CosyVoiceSiliconflow(parcial) |
|
||||
| Serviços locais | FishSpeech, GPT_SOVITS_V2, GPT_SOVITS_V3, Index-TTS, PaddleSpeech | Index-TTS, PaddleSpeech, FishSpeech, GPT_SOVITS_V2, GPT_SOVITS_V3 |
|
||||
|
||||
---
|
||||
|
||||
### VAD Detecção de Atividade Vocal
|
||||
|
||||
| Tipo | Nome da Plataforma | Método de Uso | Modelo de Preço | Observações |
|
||||
|:---:|:---------:|:----:|:----:|:--:|
|
||||
| VAD | SileroVAD | Uso local | Gratuito | |
|
||||
|
||||
---
|
||||
|
||||
### ASR Reconhecimento de Fala
|
||||
|
||||
| Método de Uso | Plataformas Suportadas | Plataformas Gratuitas |
|
||||
|:---:|:---:|:---:|
|
||||
| Uso local | FunASR, SherpaASR | FunASR, SherpaASR |
|
||||
| Chamadas via interface | FunASRServer, Volcano Engine, iFLYTEK, Tencent Cloud, Alibaba Cloud, Baidu Cloud, OpenAI ASR | FunASRServer |
|
||||
|
||||
---
|
||||
|
||||
### Reconhecimento de Impressão Vocal
|
||||
|
||||
| Método de Uso | Plataformas Suportadas | Plataformas Gratuitas |
|
||||
|:---:|:---:|:---:|
|
||||
| Uso local | 3D-Speaker | 3D-Speaker |
|
||||
|
||||
---
|
||||
|
||||
### Armazenamento de Memória
|
||||
|
||||
| Tipo | Nome da Plataforma | Método de Uso | Modelo de Preço | Observações |
|
||||
|:------:|:---------------:|:----:|:---------:|:--:|
|
||||
| Memória | mem0ai | Chamadas via interface | Cota de 1000 vezes/mês | |
|
||||
| Memória | [powermem](./docs/powermem-integration.md) | Resumo local | Depende do LLM e BD | OceanBase de código aberto, suporta busca inteligente |
|
||||
| Memória | mem_local_short | Resumo local | Gratuito | |
|
||||
| Memória | nomem | Modo sem memória | Gratuito | |
|
||||
|
||||
---
|
||||
|
||||
### Reconhecimento de Intenção
|
||||
|
||||
| Tipo | Nome da Plataforma | Método de Uso | Modelo de Preço | Observações |
|
||||
|:------:|:-------------:|:----:|:-------:|:---------------------:|
|
||||
| Intenção | intent_llm | Chamadas via interface | Baseado no preço do LLM | Reconhece intenção através de modelos de linguagem, forte generalização |
|
||||
| Intenção | function_call | Chamadas via interface | Baseado no preço do LLM | Completa a intenção através de chamada de função do modelo de linguagem, velocidade rápida, bom resultado |
|
||||
| Intenção | nointent | Modo sem intenção | Gratuito | Não realiza reconhecimento de intenção, retorna diretamente o resultado do diálogo |
|
||||
|
||||
---
|
||||
|
||||
### RAG Geração Aumentada por Recuperação
|
||||
|
||||
| Tipo | Nome da Plataforma | Método de Uso | Modelo de Preço | Observações |
|
||||
|:------:|:-------------:|:----:|:-------:|:---------------------:|
|
||||
| RAG | ragflow | Chamadas via interface | Cobrado com base nos tokens consumidos para fatiamento e segmentação de palavras | Utiliza o recurso de geração aumentada por recuperação do RagFlow para fornecer respostas de diálogo mais precisas |
|
||||
|
||||
---
|
||||
|
||||
## Agradecimentos 🙏
|
||||
|
||||
| Logo | Projeto/Empresa | Descrição |
|
||||
|:---:|:---:|:---|
|
||||
| <img src="./docs/images/logo_bailing.png" width="160"> | [Robô de Diálogo por Voz Bailing](https://github.com/wwbin2017/bailing) | Este projeto foi inspirado pelo [Robô de Diálogo por Voz Bailing](https://github.com/wwbin2017/bailing) e implementado com base nele |
|
||||
| <img src="./docs/images/logo_tenclass.png" width="160"> | [Tenclass](https://www.tenclass.com/) | Agradecimentos à [Tenclass](https://www.tenclass.com/) por formular protocolos de comunicação padrão, soluções de compatibilidade multidispositivo e demonstrações práticas de cenários de alta concorrência para o ecossistema Xiaozhi; fornecendo suporte completo de documentação técnica para este projeto |
|
||||
| <img src="./docs/images/logo_xuanfeng.png" width="160"> | [Xuanfeng Technology (玄凤科技)](https://github.com/Eric0308) | Agradecimentos à [Xuanfeng Technology](https://github.com/Eric0308) por contribuir com o framework de chamada de função, protocolo de comunicação MCP e implementação do mecanismo de chamada baseado em plugins. Através de um sistema padronizado de agendamento de instruções e capacidades de expansão dinâmica, melhora significativamente a eficiência de interação e extensibilidade funcional dos dispositivos de frontend (IoT) |
|
||||
| <img src="./docs/images/logo_junsen.png" width="160"> | [huangjunsen](https://github.com/huangjunsen0406) | Agradecimentos a [huangjunsen](https://github.com/huangjunsen0406) por contribuir com o módulo `Console de Controle Inteligente Mobile`, que permite controle eficiente e interação em tempo real em dispositivos móveis, melhorando significativamente a conveniência operacional e a eficiência de gerenciamento do sistema em cenários móveis. |
|
||||
| <img src="./docs/images/logo_huiyuan.png" width="160"> | [Huiyuan Design (汇远设计)](http://ui.kwd988.net/) | Agradecimentos à [Huiyuan Design](http://ui.kwd988.net/) por fornecer soluções visuais profissionais para este projeto, utilizando sua experiência prática de design atendendo mais de mil empresas para potencializar a experiência do usuário deste produto |
|
||||
| <img src="./docs/images/logo_qinren.png" width="160"> | [Xi'an Qinren Information Technology (西安勤人信息科技)](https://www.029app.com/) | Agradecimentos à [Xi'an Qinren Information Technology](https://www.029app.com/) por aprofundar o sistema visual deste projeto, garantindo consistência e extensibilidade do estilo de design geral em aplicações de múltiplos cenários |
|
||||
| <img src="./docs/images/logo_contributors.png" width="160"> | [Contribuidores de Código](https://github.com/xinnan-tech/xiaozhi-esp32-server/graphs/contributors) | Agradecimentos a [todos os contribuidores de código](https://github.com/xinnan-tech/xiaozhi-esp32-server/graphs/contributors), seus esforços tornaram o projeto mais robusto e poderoso. |
|
||||
|
||||
|
||||
<a href="https://star-history.com/#xinnan-tech/xiaozhi-esp32-server&Date">
|
||||
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=xinnan-tech/xiaozhi-esp32-server&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=xinnan-tech/xiaozhi-esp32-server&type=Date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=xinnan-tech/xiaozhi-esp32-server&type=Date" />
|
||||
</picture>
|
||||
</a>
|
||||
+6
-4
@@ -21,6 +21,7 @@ Hỗ trợ giao thức MQTT+UDP, giao thức Websocket, điểm truy cập MCP,
|
||||
<a href="./README_en.md"><img alt="README in English" src="https://img.shields.io/badge/English-DFE0E5"></a>
|
||||
<a href="./README_vi.md"><img alt="Tiếng Việt" src="https://img.shields.io/badge/Tiếng Việt-DBEDFA"></a>
|
||||
<a href="./README_de.md"><img alt="Deutsch" src="https://img.shields.io/badge/Deutsch-DFE0E5"></a>
|
||||
<a href="./README_pt_BR.md"><img alt="Português (Brasil)" src="https://img.shields.io/badge/Português (Brasil)-DFE0E5"></a>
|
||||
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/releases">
|
||||
<img alt="GitHub Contributors" src="https://img.shields.io/github/v/release/xinnan-tech/xiaozhi-esp32-server?logo=docker" />
|
||||
</a>
|
||||
@@ -212,8 +213,8 @@ Công cụ kiểm tra dịch vụ: https://2662r3426b.vicp.fun/test/
|
||||
|:---:|:---:|:---:|
|
||||
| ASR(Nhận dạng giọng nói) | FunASR(Local) | 👍XunfeiStreamASR(Xunfei Streaming) |
|
||||
| LLM(Mô hình lớn) | glm-4-flash(Zhipu) | 👍qwen-flash(Alibaba Bailian) |
|
||||
| VLLM(Mô hình lớn thị giác) | glm-4v-flash(Zhipu) | 👍qwen2.5-vl-3b-instructh(Alibaba Bailian) |
|
||||
| TTS(Tổng hợp giọng nói) | ✅LinkeraiTTS(Lingxi streaming) | 👍HuoshanDoubleStreamTTS(Volcano Streaming) |
|
||||
| VLLM(Mô hình lớn thị giác) | glm-4v-flash(Zhipu) | 👍qwen3.5-flash(Alibaba Bailian) |
|
||||
| TTS(Tổng hợp giọng nói) | EdgeTTS(Microsoft) | 👍HuoshanDoubleStreamTTS(Volcano Streaming) |
|
||||
| Intent(Nhận dạng ý định) | function_call(Gọi hàm) | function_call(Gọi hàm) |
|
||||
| Memory(Chức năng bộ nhớ) | mem_local_short(Bộ nhớ ngắn hạn cục bộ) | mem_local_short(Bộ nhớ ngắn hạn cục bộ) |
|
||||
|
||||
@@ -241,7 +242,7 @@ Dự án này cung cấp các công cụ kiểm tra sau để giúp bạn xác m
|
||||
| Đối thoại thông minh | Hỗ trợ nhiều LLM(Mô hình ngôn ngữ lớn), thực hiện đối thoại thông minh |
|
||||
| Cảm nhận thị giác | Hỗ trợ nhiều VLLM(Mô hình lớn thị giác), thực hiện tương tác đa phương thức |
|
||||
| Nhận dạng ý định | Hỗ trợ nhận dạng ý định mô hình lớn gắn ngoài, gọi hàm tự chủ mô hình lớn, cung cấp cơ chế xử lý ý định dạng plugin |
|
||||
| Hệ thống bộ nhớ | Hỗ trợ bộ nhớ ngắn hạn cục bộ, bộ nhớ giao diện mem0ai, có chức năng tóm tắt bộ nhớ |
|
||||
| Hệ thống bộ nhớ | Hỗ trợ bộ nhớ ngắn hạn cục bộ, bộ nhớ giao diện mem0ai, bộ nhớ thông minh PowerMem, có chức năng tóm tắt bộ nhớ |
|
||||
| Kho tri thức | Hỗ trợ kho tri thức RAGFlow, cho phép mô hình lớn đánh giá cần gọi kho tri thức trước khi trả lời |
|
||||
| Gọi công cụ | Hỗ trợ giao thức IOT phía client, giao thức MCP phía client, giao thức MCP phía server, giao thức điểm truy cập MCP, hàm công cụ tùy chỉnh |
|
||||
| Gửi lệnh | Dựa vào giao thức MQTT, hỗ trợ gửi lệnh MCP từ bảng điều khiển thông minh xuống thiết bị ESP32 |
|
||||
@@ -259,7 +260,7 @@ Nếu bạn là một nhà phát triển phần mềm, đây có một [Lá thư
|
||||
---
|
||||
|
||||
## Hệ sinh thái sản phẩm 👬
|
||||
Xiaozhi là một hệ sinh thái, khi bạn sử dụng sản phẩm này, bạn cũng có thể xem các [dự án xuất sắc](https://github.com/78/xiaozhi-esp32?tab=readme-ov-file#related-open-source-projects) khác trong hệ sinh thái này
|
||||
Xiaozhi là một hệ sinh thái, khi bạn sử dụng sản phẩm này, bạn cũng có thể xem các [dự án xuất sắc](https://github.com/78/xiaozhi-esp32/blob/main/README_zh.md#%E7%9B%B8%E5%85%B3%E5%BC%80%E6%BA%90%E9%A1%B9%E7%9B%AE) khác trong hệ sinh thái này
|
||||
|
||||
---
|
||||
|
||||
@@ -329,6 +330,7 @@ Trên thực tế, bất kỳ VLLM nào hỗ trợ gọi giao diện openai đ
|
||||
| Loại | Tên nền tảng | Phương pháp sử dụng | Mô hình thu phí | Ghi chú |
|
||||
|:------:|:---------------:|:----:|:---------:|:--:|
|
||||
| Memory | mem0ai | Gọi giao diện | Hạn mức 1000 lần/tháng | |
|
||||
| Memory | [powermem](./docs/powermem-integration.md) | Tóm tắt cục bộ | Phụ thuộc vào LLM và DB | OceanBase mã nguồn mở, hỗ trợ tìm kiếm thông minh |
|
||||
| Memory | mem_local_short | Tóm tắt cục bộ | Miễn phí | |
|
||||
| Memory | nomem | Chế độ không có bộ nhớ | Miễn phí | |
|
||||
|
||||
|
||||
@@ -418,7 +418,7 @@ manager-api:
|
||||
secret: 12345678-xxxx-xxxx-xxxx-123456789000
|
||||
```
|
||||
|
||||
## 5.运行项目
|
||||
## 9.运行项目
|
||||
|
||||
```
|
||||
# 确保在xiaozhi-server目录下执行
|
||||
|
||||
+4
-2
@@ -40,8 +40,8 @@ conda install conda-forge::ffmpeg
|
||||
|:---:|:---:|:---:|
|
||||
| ASR(语音识别) | FunASR(本地) | 👍XunfeiStreamASR(讯飞流式) |
|
||||
| LLM(大模型) | glm-4-flash(智谱) | 👍qwen-flash(阿里百炼) |
|
||||
| VLLM(视觉大模型) | glm-4v-flash(智谱) | 👍qwen2.5-vl-3b-instructh(阿里百炼) |
|
||||
| TTS(语音合成) | ✅LinkeraiTTS(灵犀流式) | 👍HuoshanDoubleStreamTTS(火山流式) |
|
||||
| VLLM(视觉大模型) | glm-4v-flash(智谱) | 👍qwen3.5-flash(阿里百炼) |
|
||||
| TTS(语音合成) | EdgeTTS(微软) | 👍HuoshanDoubleStreamTTS(火山流式) |
|
||||
| Intent(意图识别) | function_call(函数调用) | function_call(函数调用) |
|
||||
| Memory(记忆功能) | mem_local_short(本地短期记忆) | mem_local_short(本地短期记忆) |
|
||||
|
||||
@@ -82,6 +82,8 @@ VAD:
|
||||
8、[新闻插件源配置指南](./newsnow_plugin_config.md)<br/>
|
||||
9、[知识库ragflow集成指南](./ragflow-integration.md)<br/>
|
||||
10、[如何部署上下文源](./context-provider-integration.md)<br/>
|
||||
11、[如何集成PowerMem智能记忆](./powermem-integration.md)<br/>
|
||||
12、[如何配置天气插件查询天气](./weather-integration.md)<br/>
|
||||
|
||||
### 11、语音克隆、本地语音部署相关教程
|
||||
1、[如何在智控台克隆音色](./huoshan-streamTTS-voice-cloning.md)<br/>
|
||||
|
||||
@@ -127,7 +127,7 @@ pm2 restart xz-mqtt
|
||||
```
|
||||
192.168.0.7:8884
|
||||
```
|
||||
4. 在智控台顶部,点击`参数管理`,搜索`server.mqtt_manager_api`,点击编辑,填入你在`.env`文件中设置的`PUBLIC_IP`+`:`+`UDP_PORT`。类似这样
|
||||
4. 在智控台顶部,点击`参数管理`,搜索`server.mqtt_manager_api`,点击编辑,填入你在`.env`文件中设置的`PUBLIC_IP`+`:`+`API_PORT`。类似这样
|
||||
```
|
||||
192.168.0.7:8007
|
||||
```
|
||||
@@ -154,7 +154,7 @@ curl 'http://localhost:8002/xiaozhi/ota/' \
|
||||
pm2 logs xz-mqtt
|
||||
```
|
||||
|
||||
## 第三部分:全模块运行实现小智硬件MQTT+UDP连接
|
||||
## 第三部分:单模块运行xiaozhi-server实现小智硬件MQTT+UDP连接
|
||||
|
||||
打开你的`data/.config.yaml`文件,在`server`下找到`mqtt_gateway`填入你在`.env`文件中设置的`PUBLIC_IP`+`:`+`MQTT_PORT`。类似这样
|
||||
```
|
||||
@@ -184,4 +184,4 @@ curl 'http://localhost:8002/xiaozhi/ota/' \
|
||||
唤醒后留意mqtt-gateway的日志,确认是否有连接成功的日志。
|
||||
```
|
||||
pm2 logs xz-mqtt
|
||||
```
|
||||
```
|
||||
|
||||
@@ -0,0 +1,345 @@
|
||||
# PowerMem 记忆组件集成指南
|
||||
|
||||
## 简介
|
||||
|
||||
[PowerMem](https://www.powermem.ai/) 是由 OceanBase 开源的 Agent 记忆组件,通过本地 LLM 进行记忆总结和智能检索,为 AI 代理提供高效的记忆管理功能。
|
||||
|
||||
费用说明:PowerMem 本身开源免费,实际费用取决于您选择的 LLM 和数据库:
|
||||
- 使用 SQLite + 免费 LLM(如智谱 glm-4-flash)= **完全免费**
|
||||
- 使用云端 LLM 或云端数据库 = 按对应服务收费
|
||||
|
||||
> 💡 **最佳性能提示**:PowerMem 配合 OceanBase 使用可实现最大性能释放,SQLite 仅建议在资源不足的情况下使用。
|
||||
|
||||
- **GitHub**: https://github.com/oceanbase/powermem
|
||||
- **官网**: https://www.powermem.ai/
|
||||
- **使用示例**: https://github.com/oceanbase/powermem/tree/main/examples
|
||||
|
||||
## 功能特性
|
||||
|
||||
- **本地总结**:通过 LLM 在本地进行记忆总结和提取
|
||||
- **用户画像**:通过 `UserMemory` 自动提取用户信息(姓名、职业、兴趣等),持续更新用户画像
|
||||
- **智能遗忘**:基于艾宾浩斯遗忘曲线,自动"遗忘"过时噪声信息
|
||||
- **多种存储后端**:支持 OceanBase(推荐,最佳性能)、SeekDB(推荐,AI应用存储一体)、PostgreSQL、SQLite(轻量备选)
|
||||
- **多种 LLM 支持**:通义千问、智谱(glm-4-flash 免费)、OpenAI 等
|
||||
- **智能检索**:基于向量搜索的语义检索能力
|
||||
- **私有部署**:完全支持本地私有化部署
|
||||
- **异步操作**:高效的异步记忆管理
|
||||
|
||||
## 安装
|
||||
|
||||
PowerMem 已添加到项目依赖中,如果需要手动安装:
|
||||
|
||||
```bash
|
||||
pip install powermem
|
||||
```
|
||||
|
||||
## 配置说明
|
||||
|
||||
### 基础配置
|
||||
|
||||
在 `config.yaml` 中配置 PowerMem:
|
||||
|
||||
```yaml
|
||||
selected_module:
|
||||
Memory: powermem
|
||||
|
||||
Memory:
|
||||
powermem:
|
||||
type: powermem
|
||||
# 是否启用用户画像功能
|
||||
# 用户画像支持: oceanbase、seekdb、sqlite (powermem 0.3.0+)
|
||||
enable_user_profile: true
|
||||
|
||||
# ========== LLM 配置 ==========
|
||||
llm:
|
||||
provider: openai # 可选: qwen, openai, zhipu 等
|
||||
config:
|
||||
api_key: 你的LLM API密钥
|
||||
model: qwen-plus
|
||||
# openai_base_url: https://api.openai.com/v1 # 可选,自定义服务地址
|
||||
|
||||
# ========== Embedding 配置 ==========
|
||||
embedder:
|
||||
provider: openai # 可选: qwen, openai 等
|
||||
config:
|
||||
api_key: 你的嵌入模型API密钥
|
||||
model: text-embedding-v4
|
||||
openai_base_url: https://dashscope.aliyuncs.com/compatible-mode/v1
|
||||
# embedding_dims: 1024 # 向量维度,非1536时需配置
|
||||
|
||||
# ========== Database 配置 ==========
|
||||
vector_store:
|
||||
provider: sqlite # 可选: oceanbase(推荐), seekdb(推荐), postgres, sqlite(轻量)
|
||||
config: {} # SQLite 无需额外配置
|
||||
```
|
||||
|
||||
### 配置参数详解
|
||||
|
||||
#### LLM 配置
|
||||
|
||||
| 参数 | 说明 | 可选值 |
|
||||
|------|------|--------|
|
||||
| `llm.provider` | LLM 提供商 | `qwen`, `openai`, `zhipu` 等 |
|
||||
| `llm.config.api_key` | API 密钥 | - |
|
||||
| `llm.config.model` | 模型名称 | 根据提供商选择 |
|
||||
| `llm.config.openai_base_url` | 自定义服务地址(可选) | - |
|
||||
|
||||
#### Embedding 配置
|
||||
|
||||
| 参数 | 说明 | 可选值 |
|
||||
|------|------|--------|
|
||||
| `embedder.provider` | 嵌入模型提供商 | `qwen`, `openai` 等 |
|
||||
| `embedder.config.api_key` | API 密钥 | - |
|
||||
| `embedder.config.model` | 模型名称 | 根据提供商选择 |
|
||||
| `embedder.config.openai_base_url` | 自定义服务地址(可选) | - |
|
||||
|
||||
#### Database 配置
|
||||
|
||||
| 参数 | 说明 | 可选值 |
|
||||
|------|------|--------|
|
||||
| `vector_store.provider` | 存储后端类型 | `oceanbase`(推荐), `seekdb`(推荐), `postgres`, `sqlite`(轻量) |
|
||||
| `vector_store.config` | 数据库连接配置 | 根据 provider 设置 |
|
||||
|
||||
### 记忆模式说明
|
||||
|
||||
PowerMem 支持两种记忆模式:
|
||||
|
||||
| 模式 | 配置 | 功能 | 存储要求 |
|
||||
|------|------|------|----------|
|
||||
| **普通记忆** | `enable_user_profile: false` | 对话记忆存储与检索 | 支持所有数据库 |
|
||||
| **用户画像** | `enable_user_profile: true` | 记忆 + 自动提取用户画像 | oceanbase、seekdb、sqlite |
|
||||
|
||||
> 📌 **版本说明**:PowerMem 0.3.0+ 版本,用户画像功能支持 OceanBase、SeekDB、SQLite 三种存储后端。
|
||||
|
||||
### 使用通义千问(推荐)
|
||||
|
||||
1. 访问 [阿里云百炼平台](https://bailian.console.aliyun.com/) 注册账号
|
||||
2. 在 [API Key 管理](https://bailian.console.aliyun.com/?apiKey=1#/api-key) 页面获取 API 密钥
|
||||
3. 配置如下:
|
||||
|
||||
```yaml
|
||||
Memory:
|
||||
powermem:
|
||||
type: powermem
|
||||
enable_user_profile: true
|
||||
llm:
|
||||
provider: qwen
|
||||
config:
|
||||
api_key: sk-xxxxxxxxxxxxxxxx
|
||||
model: qwen-plus
|
||||
embedder:
|
||||
provider: openai
|
||||
config:
|
||||
api_key: sk-xxxxxxxxxxxxxxxx
|
||||
model: text-embedding-v4
|
||||
openai_base_url: https://dashscope.aliyuncs.com/compatible-mode/v1
|
||||
vector_store:
|
||||
provider: sqlite
|
||||
config: {}
|
||||
```
|
||||
|
||||
### 使用智谱免费 LLM(完全免费方案)
|
||||
|
||||
智谱提供免费的 glm-4-flash 模型,配合 SQLite 可实现完全免费使用:
|
||||
|
||||
1. 访问 [智谱AI开放平台](https://bigmodel.cn/) 注册账号
|
||||
2. 在 [API Keys](https://bigmodel.cn/usercenter/proj-mgmt/apikeys) 页面获取 API 密钥
|
||||
3. 配置如下:
|
||||
|
||||
```yaml
|
||||
Memory:
|
||||
powermem:
|
||||
type: powermem
|
||||
enable_user_profile: true
|
||||
llm:
|
||||
provider: openai # 使用 openai 兼容模式
|
||||
config:
|
||||
api_key: xxxxxxxxxxxxxxxx.xxxxxxxxxxxxxxxx
|
||||
model: glm-4-flash
|
||||
openai_base_url: https://open.bigmodel.cn/api/paas/v4/
|
||||
embedder:
|
||||
provider: openai
|
||||
config:
|
||||
api_key: xxxxxxxxxxxxxxxx.xxxxxxxxxxxxxxxx
|
||||
model: embedding-3
|
||||
openai_base_url: https://open.bigmodel.cn/api/paas/v4/
|
||||
vector_store:
|
||||
provider: sqlite
|
||||
config: {}
|
||||
```
|
||||
|
||||
### 使用 OpenAI
|
||||
|
||||
```yaml
|
||||
Memory:
|
||||
powermem:
|
||||
type: powermem
|
||||
enable_user_profile: true
|
||||
llm:
|
||||
provider: openai
|
||||
config:
|
||||
api_key: sk-xxxxxxxxxxxxxxxx
|
||||
model: gpt-4o-mini
|
||||
openai_base_url: https://api.openai.com/v1
|
||||
embedder:
|
||||
provider: openai
|
||||
config:
|
||||
api_key: sk-xxxxxxxxxxxxxxxx
|
||||
model: text-embedding-3-small
|
||||
openai_base_url: https://api.openai.com/v1
|
||||
vector_store:
|
||||
provider: sqlite
|
||||
config: {}
|
||||
```
|
||||
|
||||
### 使用 OceanBase(最佳性能方案)
|
||||
|
||||
OceanBase 是 PowerMem 的最佳搭档,可实现最大性能释放:
|
||||
|
||||
1. 部署 OceanBase 数据库(支持开源本地部署或使用云服务)
|
||||
- 开源部署:https://github.com/oceanbase/oceanbase
|
||||
- 云服务:https://www.oceanbase.com/
|
||||
2. 配置如下:
|
||||
|
||||
```yaml
|
||||
Memory:
|
||||
powermem:
|
||||
type: powermem
|
||||
enable_user_profile: true
|
||||
llm:
|
||||
provider: qwen
|
||||
config:
|
||||
api_key: sk-xxxxxxxxxxxxxxxx
|
||||
model: qwen-plus
|
||||
embedder:
|
||||
provider: openai
|
||||
config:
|
||||
api_key: sk-xxxxxxxxxxxxxxxx
|
||||
model: text-embedding-v4
|
||||
openai_base_url: https://dashscope.aliyuncs.com/compatible-mode/v1
|
||||
vector_store:
|
||||
provider: oceanbase
|
||||
config:
|
||||
host: 127.0.0.1
|
||||
port: 2881
|
||||
user: root@test
|
||||
password: your_password
|
||||
db_name: powermem
|
||||
collection_name: memories # 默认值
|
||||
embedding_model_dims: 1536 # 嵌入向量维度,必需参数
|
||||
```
|
||||
|
||||
## 设备记忆隔离
|
||||
|
||||
PowerMem 会自动使用设备 ID(`device_id`)作为 `user_id` 进行记忆隔离。这意味着:
|
||||
|
||||
- 每个设备拥有独立的记忆空间
|
||||
- 不同设备之间的记忆完全隔离
|
||||
- 同一设备的多次对话可以共享记忆上下文
|
||||
|
||||
## 用户画像(UserMemory)
|
||||
|
||||
PowerMem 提供 `UserMemory` 类,可自动从对话中提取用户画像信息。
|
||||
|
||||
> 📌 **版本说明**:PowerMem 0.3.0+ 版本,用户画像功能支持 OceanBase、SeekDB、SQLite 三种存储后端。
|
||||
|
||||
### 启用用户画像
|
||||
|
||||
在配置中设置 `enable_user_profile: true` 即可启用:
|
||||
|
||||
```yaml
|
||||
Memory:
|
||||
powermem:
|
||||
type: powermem
|
||||
enable_user_profile: true # 启用用户画像
|
||||
llm:
|
||||
provider: qwen
|
||||
config:
|
||||
api_key: sk-xxxxxxxxxxxxxxxx
|
||||
model: qwen-plus
|
||||
embedder:
|
||||
provider: openai
|
||||
config:
|
||||
api_key: sk-xxxxxxxxxxxxxxxx
|
||||
model: text-embedding-v4
|
||||
openai_base_url: https://dashscope.aliyuncs.com/compatible-mode/v1
|
||||
vector_store:
|
||||
provider: sqlite # 用户画像支持: oceanbase、seekdb、sqlite
|
||||
config: {}
|
||||
```
|
||||
|
||||
### 用户画像能力
|
||||
|
||||
| 能力 | 说明 |
|
||||
|------|------|
|
||||
| **信息提取** | 自动从对话中提取姓名、年龄、职业、兴趣等 |
|
||||
| **持续更新** | 随着对话进行,不断完善用户画像 |
|
||||
| **画像检索** | 将用户画像与记忆搜索结合,提升检索相关性 |
|
||||
| **智能遗忘** | 基于艾宾浩斯遗忘曲线,淡化过时信息 |
|
||||
|
||||
### 工作原理
|
||||
|
||||
启用用户画像后,小智在查询记忆时会自动返回:
|
||||
1. **用户画像**:用户的基本信息、兴趣爱好等
|
||||
2. **相关记忆**:与当前对话相关的历史记忆
|
||||
|
||||
> ✅ **版本说明**:PowerMem 0.3.0+ 版本,用户画像功能支持 OceanBase、SeekDB、SQLite 三种存储后端。
|
||||
|
||||
## 与其他记忆组件的对比
|
||||
|
||||
| 特性 | PowerMem | mem0ai | mem_local_short |
|
||||
|------|----------|--------|-----------------|
|
||||
| 工作方式 | 本地总结 | 云端接口 | 本地总结 |
|
||||
| 存储位置 | 本地/云端DB | 云端 | 本地YAML |
|
||||
| 费用 | 取决于LLM和DB | 1000次/月免费 | 完全免费 |
|
||||
| 智能检索 | ✅ 向量搜索 | ✅ 向量搜索 | ❌ 全量返回 |
|
||||
| 用户画像 | ✅ UserMemory | ❌ | ❌ |
|
||||
| 智能遗忘 | ✅ 遗忘曲线 | ❌ | ❌ |
|
||||
| 私有部署 | ✅ 支持 | ❌ 仅云端 | ✅ 支持 |
|
||||
| 数据库支持 | OceanBase(推荐)/SeekDB/PostgreSQL/SQLite | - | YAML 文件 |
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 1. API 密钥错误
|
||||
|
||||
如果出现 `API key is required` 错误,请检查:
|
||||
- `llm_api_key` 和 `embedding_api_key` 是否正确填写
|
||||
- API 密钥是否有效
|
||||
|
||||
### 2. 模型不存在
|
||||
|
||||
如果出现模型不存在的错误,请确认:
|
||||
- `llm_model` 和 `embedding_model` 名称是否正确
|
||||
- 对应的模型服务是否已开通
|
||||
|
||||
### 3. 连接超时
|
||||
|
||||
如果出现连接超时,可以尝试:
|
||||
- 检查网络连接
|
||||
- 如果使用代理,配置 `llm_base_url` 和 `embedding_base_url`
|
||||
|
||||
## 测试验证
|
||||
|
||||
可以在虚拟环境中测试 PowerMem 是否正常工作:
|
||||
|
||||
```bash
|
||||
# 激活虚拟环境
|
||||
source .venv/bin/activate
|
||||
|
||||
# 测试 PowerMem 导入
|
||||
python -c "from powermem import AsyncMemory; print('PowerMem 导入成功')"
|
||||
|
||||
# 测试 UserMemory 导入(用户画像功能)
|
||||
python -c "from powermem import UserMemory; print('UserMemory 导入成功')"
|
||||
```
|
||||
|
||||
## 更多资源
|
||||
|
||||
- [PowerMem 官方文档](https://www.powermem.ai/)
|
||||
- [PowerMem GitHub 仓库](https://github.com/oceanbase/powermem)
|
||||
- [PowerMem 使用示例](https://github.com/oceanbase/powermem/tree/main/examples)
|
||||
- [OceanBase 官网](https://www.oceanbase.com/)
|
||||
- [OceanBase GitHub](https://github.com/oceanbase/oceanbase)
|
||||
- [SeekDB GitHub](https://github.com/oceanbase/seekdb)(AI原生搜索数据库)
|
||||
- [阿里云百炼平台](https://bailian.console.aliyun.com/)
|
||||
|
||||
@@ -2,7 +2,9 @@ package xiaozhi.common.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.client.JdkClientHttpRequestFactory;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import java.time.Duration;
|
||||
|
||||
/**
|
||||
* RestTemplate配置
|
||||
@@ -12,6 +14,8 @@ public class RestTemplateConfig {
|
||||
|
||||
@Bean
|
||||
public RestTemplate restTemplate() {
|
||||
return new RestTemplate();
|
||||
JdkClientHttpRequestFactory factory = new JdkClientHttpRequestFactory();
|
||||
factory.setReadTimeout(Duration.ofSeconds(30));
|
||||
return new RestTemplate(factory);
|
||||
}
|
||||
}
|
||||
@@ -79,6 +79,30 @@ public class SwaggerConfig {
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public GroupedOpenApi knowledgeApi() {
|
||||
return GroupedOpenApi.builder()
|
||||
.group("knowledge")
|
||||
.pathsToMatch("/datasets/**")
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public GroupedOpenApi botApi() {
|
||||
return GroupedOpenApi.builder()
|
||||
.group("bot")
|
||||
.pathsToMatch("/api/v1/**")
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public GroupedOpenApi correctWordApi() {
|
||||
return GroupedOpenApi.builder()
|
||||
.group("correct-word")
|
||||
.pathsToMatch("/correct-word/**")
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public OpenAPI customOpenAPI() {
|
||||
return new OpenAPI().info(new Info()
|
||||
|
||||
@@ -151,6 +151,21 @@ public interface Constant {
|
||||
*/
|
||||
String MEMORY_NO_MEM = "Memory_nomem";
|
||||
|
||||
/**
|
||||
* 仅上报聊天记录(不总结记忆)
|
||||
*/
|
||||
String MEMORY_MEM_REPORT_ONLY = "Memory_mem_report_only";
|
||||
|
||||
/**
|
||||
* Mem0AI记忆
|
||||
*/
|
||||
String MEMORY_MEM0AI = "Memory_mem0ai";
|
||||
|
||||
/**
|
||||
* PowerMem记忆
|
||||
*/
|
||||
String MEMORY_POWERMEM = "Memory_powermem";
|
||||
|
||||
/**
|
||||
* 火山引擎双声道语音克隆
|
||||
*/
|
||||
@@ -304,7 +319,7 @@ public interface Constant {
|
||||
/**
|
||||
* 版本号
|
||||
*/
|
||||
public static final String VERSION = "0.9.1";
|
||||
public static final String VERSION = "0.9.3";
|
||||
|
||||
/**
|
||||
* 无效固件URL
|
||||
|
||||
@@ -245,4 +245,19 @@ public interface ErrorCode {
|
||||
int DEVICE_NOT_EXIST = 10194; // 设备不存在
|
||||
int OTA_UPLOAD_COUNT_EXCEED = 10195; // OTA上传次数超过限制
|
||||
|
||||
// 智能体标签相关错误码
|
||||
int AGENT_TAG_NAME_DUPLICATE = 10196; // 标签名称已存在
|
||||
int AGENT_TAG_NAME_EMPTY = 10197; // 标签名称不能为空
|
||||
int AGENT_TAG_NOT_EXIST = 10198; // 标签不存在
|
||||
|
||||
int RAG_DOCUMENT_PARSING_DELETE_ERROR = 10199; // 文档解析中,禁止删除
|
||||
|
||||
// 智能体MCP相关错误码
|
||||
int MCP_ACCESS_POINT_ADDRESS_NO_PERMISSION = 10200; // 没有权限查看该智能体的MCP接入点地址
|
||||
int MCP_ACCESS_POINT_ADDRESS_NOT_CONFIGURED = 10201; // 请联系管理员进入参数管理配置mcp接入点地址
|
||||
int MCP_ACCESS_POINT_TOOLS_LIST_NO_PERMISSION = 10202; // 没有权限查看该智能体的MCP工具列表
|
||||
|
||||
// 替换词相关错误码
|
||||
int CORRECT_WORD_FILE_NAME_EXISTS = 10203; // 文件名已存在
|
||||
int FILE_SIZE_OVER_LIMIT = 10204; // 文件大小超过限制
|
||||
}
|
||||
|
||||
@@ -13,23 +13,25 @@ public class RenException extends RuntimeException {
|
||||
private String msg;
|
||||
|
||||
public RenException(int code) {
|
||||
super(MessageUtils.getMessage(code));
|
||||
this.code = code;
|
||||
this.msg = MessageUtils.getMessage(code);
|
||||
}
|
||||
|
||||
public RenException(int code, String... params) {
|
||||
super(MessageUtils.getMessage(code, params));
|
||||
this.code = code;
|
||||
this.msg = MessageUtils.getMessage(code, params);
|
||||
}
|
||||
|
||||
public RenException(int code, Throwable e) {
|
||||
super(e);
|
||||
super(MessageUtils.getMessage(code), e);
|
||||
this.code = code;
|
||||
this.msg = MessageUtils.getMessage(code);
|
||||
}
|
||||
|
||||
public RenException(int code, Throwable e, String... params) {
|
||||
super(e);
|
||||
super(MessageUtils.getMessage(code, params), e);
|
||||
this.code = code;
|
||||
this.msg = MessageUtils.getMessage(code, params);
|
||||
}
|
||||
|
||||
@@ -187,4 +187,5 @@ public class RedisKeys {
|
||||
public static String getOtaUploadCountKey(Long username) {
|
||||
return "ota:upload:count:" + username;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -12,28 +12,29 @@ import xiaozhi.modules.sys.service.SysParamsService;
|
||||
* 封装了重复的SM2解密、验证码提取和验证逻辑
|
||||
*/
|
||||
public class Sm2DecryptUtil {
|
||||
|
||||
|
||||
/**
|
||||
* 验证码长度
|
||||
*/
|
||||
private static final int CAPTCHA_LENGTH = 5;
|
||||
|
||||
|
||||
/**
|
||||
* 解密SM2加密内容,提取验证码并验证
|
||||
*
|
||||
* @param encryptedPassword SM2加密的密码字符串
|
||||
* @param captchaId 验证码ID
|
||||
* @param captchaService 验证码服务
|
||||
* @param sysParamsService 系统参数服务
|
||||
* @param captchaId 验证码ID
|
||||
* @param captchaService 验证码服务
|
||||
* @param sysParamsService 系统参数服务
|
||||
* @return 解密后的实际密码
|
||||
*/
|
||||
public static String decryptAndValidateCaptcha(String encryptedPassword, String captchaId,
|
||||
CaptchaService captchaService, SysParamsService sysParamsService) {
|
||||
public static String decryptAndValidateCaptcha(String encryptedPassword, String captchaId,
|
||||
CaptchaService captchaService, SysParamsService sysParamsService) {
|
||||
// 获取SM2私钥
|
||||
String privateKeyStr = sysParamsService.getValue(Constant.SM2_PRIVATE_KEY, true);
|
||||
if (StringUtils.isBlank(privateKeyStr)) {
|
||||
throw new RenException(ErrorCode.SM2_KEY_NOT_CONFIGURED);
|
||||
}
|
||||
|
||||
|
||||
// 使用SM2私钥解密密码
|
||||
String decryptedContent;
|
||||
try {
|
||||
@@ -41,19 +42,20 @@ public class Sm2DecryptUtil {
|
||||
} catch (Exception e) {
|
||||
throw new RenException(ErrorCode.SM2_DECRYPT_ERROR);
|
||||
}
|
||||
|
||||
|
||||
// 分离验证码和密码:前5位是验证码,后面是密码
|
||||
if (decryptedContent.length() > CAPTCHA_LENGTH) {
|
||||
String embeddedCaptcha = decryptedContent.substring(0, CAPTCHA_LENGTH);
|
||||
String actualPassword = decryptedContent.substring(CAPTCHA_LENGTH);
|
||||
|
||||
// 验证嵌入的验证码是否正确
|
||||
|
||||
boolean embeddedCaptchaValid = captchaService.validate(captchaId, embeddedCaptcha, true);
|
||||
if (!embeddedCaptchaValid) {
|
||||
throw new RenException(ErrorCode.SMS_CAPTCHA_ERROR);
|
||||
}
|
||||
|
||||
|
||||
return actualPassword;
|
||||
} else if (decryptedContent.length() > 0) {
|
||||
throw new RenException(ErrorCode.SMS_CAPTCHA_ERROR);
|
||||
} else {
|
||||
throw new RenException(ErrorCode.SM2_DECRYPT_ERROR);
|
||||
}
|
||||
|
||||
+60
-1
@@ -42,6 +42,9 @@ import xiaozhi.modules.agent.dto.AgentMemoryDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentUpdateDTO;
|
||||
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||
import xiaozhi.modules.agent.entity.AgentTemplateEntity;
|
||||
import xiaozhi.modules.agent.dto.AgentTagDTO;
|
||||
import xiaozhi.modules.agent.entity.AgentTagEntity;
|
||||
import xiaozhi.modules.agent.service.AgentTagService;
|
||||
import xiaozhi.modules.agent.service.AgentChatAudioService;
|
||||
import xiaozhi.modules.agent.service.AgentChatHistoryService;
|
||||
import xiaozhi.modules.agent.service.AgentChatSummaryService;
|
||||
@@ -49,6 +52,7 @@ import xiaozhi.modules.agent.service.AgentContextProviderService;
|
||||
import xiaozhi.modules.agent.service.AgentPluginMappingService;
|
||||
import xiaozhi.modules.agent.service.AgentService;
|
||||
import xiaozhi.modules.agent.service.AgentTemplateService;
|
||||
import xiaozhi.modules.correctword.service.CorrectWordFileService;
|
||||
import xiaozhi.modules.agent.vo.AgentChatHistoryUserVO;
|
||||
import xiaozhi.modules.agent.vo.AgentInfoVO;
|
||||
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||
@@ -69,6 +73,8 @@ public class AgentController {
|
||||
private final AgentContextProviderService agentContextProviderService;
|
||||
private final AgentChatSummaryService agentChatSummaryService;
|
||||
private final RedisUtils redisUtils;
|
||||
private final AgentTagService agentTagService;
|
||||
private final CorrectWordFileService correctWordFileService;
|
||||
|
||||
@GetMapping("/list")
|
||||
@Operation(summary = "获取用户智能体列表")
|
||||
@@ -77,7 +83,7 @@ public class AgentController {
|
||||
@RequestParam(value = "keyword", required = false) String keyword,
|
||||
@RequestParam(value = "searchType", defaultValue = "name") String searchType) {
|
||||
UserDetail user = SecurityUser.getUser();
|
||||
|
||||
|
||||
// 直接调用整合后的getUserAgents方法,无需再区分搜索和普通查询
|
||||
List<AgentDTO> agents = agentService.getUserAgents(user.getId(), keyword, searchType);
|
||||
return new Result<List<AgentDTO>>().ok(agents);
|
||||
@@ -146,6 +152,13 @@ public class AgentController {
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/chat-title/{sessionId}/generate")
|
||||
@Operation(summary = "根据会话ID生成聊天标题")
|
||||
public Result<Void> generateAndSaveChatTitle(@PathVariable String sessionId) {
|
||||
agentChatSummaryService.generateAndSaveChatTitle(sessionId);
|
||||
return new Result<Void>().ok(null);
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
@Operation(summary = "更新智能体")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
@@ -166,6 +179,8 @@ public class AgentController {
|
||||
agentPluginMappingService.deleteByAgentId(id);
|
||||
// 删除关联的上下文源配置
|
||||
agentContextProviderService.deleteByAgentId(id);
|
||||
// 删除关联的替换词文件关联记录
|
||||
correctWordFileService.deleteMappingsByAgentId(id);
|
||||
// 再删除智能体
|
||||
agentService.deleteById(id);
|
||||
return new Result<>();
|
||||
@@ -275,6 +290,50 @@ public class AgentController {
|
||||
.body(audioData);
|
||||
}
|
||||
|
||||
@PostMapping("/tag")
|
||||
@Operation(summary = "创建标签")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<AgentTagEntity> createTag(@RequestBody Map<String, String> params) {
|
||||
String tagName = params.get("tagName");
|
||||
if (StringUtils.isBlank(tagName)) {
|
||||
return new Result<AgentTagEntity>().error("标签名称不能为空");
|
||||
}
|
||||
AgentTagEntity tag = agentTagService.saveTag(tagName);
|
||||
return new Result<AgentTagEntity>().ok(tag);
|
||||
}
|
||||
|
||||
@GetMapping("/tag/list")
|
||||
@Operation(summary = "获取所有标签列表")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<List<AgentTagDTO>> getAllTags() {
|
||||
List<AgentTagDTO> tags = agentTagService.getAllTags();
|
||||
return new Result<List<AgentTagDTO>>().ok(tags);
|
||||
}
|
||||
|
||||
@DeleteMapping("/tag/{id}")
|
||||
@Operation(summary = "删除标签")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<Void> deleteTag(@PathVariable String id) {
|
||||
agentTagService.deleteTag(id);
|
||||
return new Result<Void>().ok(null);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/tags")
|
||||
@Operation(summary = "获取智能体的标签")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<List<AgentTagDTO>> getAgentTags(@PathVariable String id) {
|
||||
List<AgentTagDTO> tags = agentTagService.getTagsByAgentId(id);
|
||||
return new Result<List<AgentTagDTO>>().ok(tags);
|
||||
}
|
||||
|
||||
@PutMapping("/{id}/tags")
|
||||
@Operation(summary = "保存智能体的标签")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<Void> saveAgentTags(@PathVariable String id, @RequestBody Map<String, Object> params) {
|
||||
List<String> tagIds = (List<String>) params.get("tagIds");
|
||||
List<String> tagNames = (List<String>) params.get("tagNames");
|
||||
agentTagService.saveAgentTags(id, tagIds, tagNames);
|
||||
return new Result<Void>().ok(null);
|
||||
}
|
||||
|
||||
}
|
||||
+4
-3
@@ -11,6 +11,7 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import xiaozhi.common.exception.ErrorCode;
|
||||
import xiaozhi.common.user.UserDetail;
|
||||
import xiaozhi.common.utils.Result;
|
||||
import xiaozhi.modules.agent.service.AgentMcpAccessPointService;
|
||||
@@ -40,11 +41,11 @@ public class AgentMcpAccessPointController {
|
||||
|
||||
// 检查权限
|
||||
if (!agentService.checkAgentPermission(agentId, user.getId())) {
|
||||
return new Result<String>().error("没有权限查看该智能体的MCP接入点地址");
|
||||
return new Result<String>().error(ErrorCode.MCP_ACCESS_POINT_ADDRESS_NO_PERMISSION);
|
||||
}
|
||||
String agentMcpAccessAddress = agentMcpAccessPointService.getAgentMcpAccessAddress(agentId);
|
||||
if (agentMcpAccessAddress == null) {
|
||||
return new Result<String>().ok("请联系管理员进入参数管理配置mcp接入点地址");
|
||||
return new Result<String>().error(ErrorCode.MCP_ACCESS_POINT_ADDRESS_NOT_CONFIGURED);
|
||||
}
|
||||
return new Result<String>().ok(agentMcpAccessAddress);
|
||||
}
|
||||
@@ -58,7 +59,7 @@ public class AgentMcpAccessPointController {
|
||||
|
||||
// 检查权限
|
||||
if (!agentService.checkAgentPermission(agentId, user.getId())) {
|
||||
return new Result<List<String>>().error("没有权限查看该智能体的MCP工具列表");
|
||||
return new Result<List<String>>().error(ErrorCode.MCP_ACCESS_POINT_TOOLS_LIST_NO_PERMISSION);
|
||||
}
|
||||
List<String> agentMcpToolsList = agentMcpAccessPointService.getAgentMcpToolsList(agentId);
|
||||
return new Result<List<String>>().ok(agentMcpToolsList);
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package xiaozhi.modules.agent.dao;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
|
||||
import xiaozhi.modules.agent.entity.AgentChatTitleEntity;
|
||||
|
||||
@Mapper
|
||||
public interface AgentChatTitleDao extends BaseMapper<AgentChatTitleEntity> {
|
||||
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package xiaozhi.modules.agent.dao;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import xiaozhi.common.dao.BaseDao;
|
||||
import xiaozhi.modules.agent.entity.AgentCorrectWordMappingEntity;
|
||||
|
||||
@Mapper
|
||||
public interface AgentCorrectWordMappingDao extends BaseDao<AgentCorrectWordMappingEntity> {
|
||||
|
||||
int deleteByAgentId(@Param("agentId") String agentId);
|
||||
|
||||
int deleteByFileId(@Param("fileId") String fileId);
|
||||
|
||||
int batchInsertMapping(@Param("list") List<AgentCorrectWordMappingEntity> mappings);
|
||||
|
||||
List<AgentCorrectWordMappingEntity> selectByAgentId(@Param("agentId") String agentId);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package xiaozhi.modules.agent.dao;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import xiaozhi.common.dao.BaseDao;
|
||||
import xiaozhi.modules.agent.entity.AgentTagEntity;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface AgentTagDao extends BaseDao<AgentTagEntity> {
|
||||
|
||||
List<AgentTagEntity> selectByAgentId(@Param("agentId") String agentId);
|
||||
|
||||
List<AgentTagEntity> selectByAgentIds(@Param("agentIds") List<String> agentIds);
|
||||
|
||||
List<AgentTagEntity> selectAll();
|
||||
|
||||
List<String> selectAgentIdsByTagName(@Param("tagName") String tagName);
|
||||
|
||||
List<AgentTagEntity> selectByTagNames(@Param("tagNames") List<String> tagNames);
|
||||
|
||||
int batchInsert(@Param("list") List<AgentTagEntity> tagList);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package xiaozhi.modules.agent.dao;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import xiaozhi.common.dao.BaseDao;
|
||||
import xiaozhi.modules.agent.entity.AgentTagRelationEntity;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface AgentTagRelationDao extends BaseDao<AgentTagRelationEntity> {
|
||||
|
||||
int deleteByAgentId(@Param("agentId") String agentId);
|
||||
|
||||
int insertRelation(AgentTagRelationEntity relation);
|
||||
|
||||
int batchInsertRelation(@Param("list") List<AgentTagRelationEntity> relations);
|
||||
}
|
||||
@@ -23,4 +23,9 @@ public class AgentChatSessionDTO {
|
||||
* 聊天条数
|
||||
*/
|
||||
private Integer chatCount;
|
||||
|
||||
/**
|
||||
* 会话标题
|
||||
*/
|
||||
private String title;
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import java.util.List;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import xiaozhi.modules.agent.dto.AgentTagDTO;
|
||||
|
||||
/**
|
||||
* 智能体数据传输对象
|
||||
@@ -46,4 +47,7 @@ public class AgentDTO {
|
||||
|
||||
@Schema(description = "设备数量", example = "10")
|
||||
private Integer deviceCount;
|
||||
|
||||
@Schema(description = "标签列表")
|
||||
private List<AgentTagDTO> tags;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package xiaozhi.modules.agent.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Schema(description = "智能体标签DTO")
|
||||
public class AgentTagDTO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "标签ID")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "标签名称")
|
||||
private String tagName;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package xiaozhi.modules.agent.dto;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
@@ -32,6 +33,9 @@ public class AgentUpdateDTO implements Serializable {
|
||||
@Schema(description = "大语言模型标识", example = "llm_model_02", nullable = true)
|
||||
private String llmModelId;
|
||||
|
||||
@Schema(description = "小模型标识", example = "slm_model_02", nullable = true)
|
||||
private String slmModelId;
|
||||
|
||||
@Schema(description = "VLLM模型标识", example = "vllm_model_02", required = false)
|
||||
private String vllmModelId;
|
||||
|
||||
@@ -41,6 +45,18 @@ public class AgentUpdateDTO implements Serializable {
|
||||
@Schema(description = "音色标识", example = "voice_02", nullable = true)
|
||||
private String ttsVoiceId;
|
||||
|
||||
@Schema(description = "音色语言", example = "普通话", nullable = true)
|
||||
private String ttsLanguage;
|
||||
|
||||
@Schema(description = "TTS音量", example = "50", nullable = true)
|
||||
private Integer ttsVolume;
|
||||
|
||||
@Schema(description = "TTS语速", example = "50", nullable = true)
|
||||
private Integer ttsRate;
|
||||
|
||||
@Schema(description = "TTS音调", example = "50", nullable = true)
|
||||
private Integer ttsPitch;
|
||||
|
||||
@Schema(description = "记忆模型标识", example = "mem_model_02", nullable = true)
|
||||
private String memModelId;
|
||||
|
||||
@@ -72,6 +88,9 @@ public class AgentUpdateDTO implements Serializable {
|
||||
@Schema(description = "上下文源配置", nullable = true)
|
||||
private List<ContextProviderDTO> contextProviders;
|
||||
|
||||
@Schema(description = "替换词文件ID列表", nullable = true)
|
||||
private List<String> correctWordFileIds;
|
||||
|
||||
@Data
|
||||
@Schema(description = "插件函数信息")
|
||||
public static class FunctionInfo implements Serializable {
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package xiaozhi.modules.agent.entity;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@TableName(value = "ai_agent_chat_title")
|
||||
public class AgentChatTitleEntity {
|
||||
|
||||
@TableId(type = IdType.ASSIGN_UUID)
|
||||
private String id;
|
||||
|
||||
@TableField(value = "session_id")
|
||||
private String sessionId;
|
||||
|
||||
@TableField(value = "title")
|
||||
private String title;
|
||||
|
||||
@TableField(value = "created_at")
|
||||
private Date createdAt;
|
||||
|
||||
@TableField(value = "updated_at")
|
||||
private Date updatedAt;
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package xiaozhi.modules.agent.entity;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@TableName("ai_agent_correct_word_mapping")
|
||||
@Schema(description = "智能体替换词文件关联")
|
||||
public class AgentCorrectWordMappingEntity {
|
||||
|
||||
@TableId(type = IdType.ASSIGN_UUID)
|
||||
@Schema(description = "主键")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "智能体ID")
|
||||
private String agentId;
|
||||
|
||||
@Schema(description = "替换词文件ID")
|
||||
private String fileId;
|
||||
|
||||
@Schema(description = "创建者")
|
||||
private Long creator;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
private Date createdAt;
|
||||
|
||||
@Schema(description = "更新者")
|
||||
private Long updater;
|
||||
|
||||
@Schema(description = "更新时间")
|
||||
private Date updatedAt;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package xiaozhi.modules.agent.entity;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
@@ -36,6 +37,9 @@ public class AgentEntity {
|
||||
@Schema(description = "大语言模型标识")
|
||||
private String llmModelId;
|
||||
|
||||
@Schema(description = "小模型标识")
|
||||
private String slmModelId;
|
||||
|
||||
@Schema(description = "VLLM模型标识")
|
||||
private String vllmModelId;
|
||||
|
||||
@@ -45,6 +49,18 @@ public class AgentEntity {
|
||||
@Schema(description = "音色标识")
|
||||
private String ttsVoiceId;
|
||||
|
||||
@Schema(description = "音色语言")
|
||||
private String ttsLanguage;
|
||||
|
||||
@Schema(description = "TTS音量")
|
||||
private Integer ttsVolume;
|
||||
|
||||
@Schema(description = "TTS语速")
|
||||
private Integer ttsRate;
|
||||
|
||||
@Schema(description = "TTS音调")
|
||||
private Integer ttsPitch;
|
||||
|
||||
@Schema(description = "记忆模型标识")
|
||||
private String memModelId;
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package xiaozhi.modules.agent.entity;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@TableName("ai_agent_tag")
|
||||
@Schema(description = "智能体标签")
|
||||
public class AgentTagEntity {
|
||||
|
||||
@TableId(type = IdType.ASSIGN_UUID)
|
||||
@Schema(description = "主键")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "标签名称")
|
||||
private String tagName;
|
||||
|
||||
@Schema(description = "排序")
|
||||
private Integer sort;
|
||||
|
||||
@Schema(description = "创建者")
|
||||
private Long creator;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
private Date createdAt;
|
||||
|
||||
@Schema(description = "更新者")
|
||||
private Long updater;
|
||||
|
||||
@Schema(description = "更新时间")
|
||||
private Date updatedAt;
|
||||
|
||||
@Schema(description = "删除标记")
|
||||
private Integer deleted;
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package xiaozhi.modules.agent.entity;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@TableName("ai_agent_tag_relation")
|
||||
@Schema(description = "智能体标签关联")
|
||||
public class AgentTagRelationEntity {
|
||||
|
||||
@TableId(type = IdType.ASSIGN_UUID)
|
||||
@Schema(description = "主键")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "智能体ID")
|
||||
private String agentId;
|
||||
|
||||
@Schema(description = "标签ID")
|
||||
private String tagId;
|
||||
|
||||
@Schema(description = "排序")
|
||||
private Integer sort;
|
||||
|
||||
@Schema(description = "创建者")
|
||||
private Long creator;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
private Date createdAt;
|
||||
|
||||
@Schema(description = "更新者")
|
||||
private Long updater;
|
||||
|
||||
@Schema(description = "更新时间")
|
||||
private Date updatedAt;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package xiaozhi.modules.agent.entity;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
@@ -64,6 +65,26 @@ public class AgentTemplateEntity implements Serializable {
|
||||
*/
|
||||
private String ttsVoiceId;
|
||||
|
||||
/**
|
||||
* 音色语言
|
||||
*/
|
||||
private String ttsLanguage;
|
||||
|
||||
/**
|
||||
* TTS音量
|
||||
*/
|
||||
private Integer ttsVolume;
|
||||
|
||||
/**
|
||||
* TTS语速
|
||||
*/
|
||||
private Integer ttsRate;
|
||||
|
||||
/**
|
||||
* TTS音调
|
||||
*/
|
||||
private Integer ttsPitch;
|
||||
|
||||
/**
|
||||
* 记忆模型标识
|
||||
*/
|
||||
|
||||
+8
@@ -12,4 +12,12 @@ public interface AgentChatSummaryService {
|
||||
* @return 保存结果
|
||||
*/
|
||||
boolean generateAndSaveChatSummary(String sessionId);
|
||||
|
||||
/**
|
||||
* 根据会话ID生成聊天标题并保存
|
||||
*
|
||||
* @param sessionId 会话ID
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean generateAndSaveChatTitle(String sessionId);
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package xiaozhi.modules.agent.service;
|
||||
|
||||
import xiaozhi.modules.agent.entity.AgentChatTitleEntity;
|
||||
|
||||
public interface AgentChatTitleService {
|
||||
|
||||
void saveOrUpdateTitle(String sessionId, String title);
|
||||
|
||||
String getTitleBySessionId(String sessionId);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package xiaozhi.modules.agent.service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import xiaozhi.modules.agent.dto.AgentTagDTO;
|
||||
import xiaozhi.modules.agent.entity.AgentTagEntity;
|
||||
|
||||
public interface AgentTagService {
|
||||
|
||||
AgentTagEntity saveTag(String tagName);
|
||||
|
||||
void deleteTag(String tagId);
|
||||
|
||||
List<AgentTagDTO> getTagsByAgentId(String agentId);
|
||||
|
||||
void saveAgentTags(String agentId, List<String> tagIds, List<String> tagNames);
|
||||
|
||||
void deleteAgentTags(String agentId);
|
||||
|
||||
List<AgentTagDTO> getTagsByAgentIds(List<String> agentIds);
|
||||
|
||||
List<AgentTagDTO> getAllTags();
|
||||
|
||||
List<String> getAgentIdsByTagName(String tagName);
|
||||
}
|
||||
+1
-1
@@ -52,7 +52,7 @@ public class AgentChatHistoryBizServiceImpl implements AgentChatHistoryBizServic
|
||||
public Boolean report(AgentChatHistoryReportDTO report) {
|
||||
String macAddress = report.getMacAddress();
|
||||
Byte chatType = report.getChatType();
|
||||
Long reportTimeMillis = null != report.getReportTime() ? report.getReportTime() * 1000
|
||||
Long reportTimeMillis = null != report.getReportTime() ? report.getReportTime()
|
||||
: System.currentTimeMillis();
|
||||
log.info("小智设备聊天上报请求: macAddress={}, type={} reportTime={}", macAddress, chatType, reportTimeMillis);
|
||||
|
||||
|
||||
+7
-1
@@ -6,6 +6,7 @@ import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import cn.hutool.core.collection.ListUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@@ -26,6 +27,7 @@ import xiaozhi.modules.agent.dto.AgentChatHistoryDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentChatSessionDTO;
|
||||
import xiaozhi.modules.agent.entity.AgentChatHistoryEntity;
|
||||
import xiaozhi.modules.agent.service.AgentChatHistoryService;
|
||||
import xiaozhi.modules.agent.service.AgentChatTitleService;
|
||||
import xiaozhi.modules.agent.vo.AgentChatHistoryUserVO;
|
||||
|
||||
/**
|
||||
@@ -36,9 +38,12 @@ import xiaozhi.modules.agent.vo.AgentChatHistoryUserVO;
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AgentChatHistoryServiceImpl extends ServiceImpl<AiAgentChatHistoryDao, AgentChatHistoryEntity>
|
||||
implements AgentChatHistoryService {
|
||||
|
||||
private final AgentChatTitleService agentChatTitleService;
|
||||
|
||||
@Override
|
||||
public PageData<AgentChatSessionDTO> getSessionListByAgentId(Map<String, Object> params) {
|
||||
String agentId = (String) params.get("agentId");
|
||||
@@ -61,6 +66,7 @@ public class AgentChatHistoryServiceImpl extends ServiceImpl<AiAgentChatHistoryD
|
||||
dto.setSessionId((String) map.get("session_id"));
|
||||
dto.setCreatedAt((LocalDateTime) map.get("created_at"));
|
||||
dto.setChatCount(((Number) map.get("chat_count")).intValue());
|
||||
dto.setTitle(agentChatTitleService.getTitleBySessionId(dto.getSessionId()));
|
||||
return dto;
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
@@ -91,7 +97,7 @@ public class AgentChatHistoryServiceImpl extends ServiceImpl<AiAgentChatHistoryD
|
||||
if (ToolUtil.isNotEmpty(audioIds)) {
|
||||
// 每批删除1000条
|
||||
List<List<String>> batch = ListUtil.split(audioIds, 1000);
|
||||
batch.forEach(dataList->{
|
||||
batch.forEach(dataList -> {
|
||||
baseMapper.deleteAudioByIds(dataList);
|
||||
});
|
||||
}
|
||||
|
||||
+121
-23
@@ -14,6 +14,7 @@ import org.springframework.stereotype.Service;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.modules.agent.dto.AgentChatHistoryDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentChatSummaryDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentMemoryDTO;
|
||||
@@ -21,6 +22,7 @@ import xiaozhi.modules.agent.dto.AgentUpdateDTO;
|
||||
import xiaozhi.modules.agent.entity.AgentChatHistoryEntity;
|
||||
import xiaozhi.modules.agent.service.AgentChatHistoryService;
|
||||
import xiaozhi.modules.agent.service.AgentChatSummaryService;
|
||||
import xiaozhi.modules.agent.service.AgentChatTitleService;
|
||||
import xiaozhi.modules.agent.service.AgentService;
|
||||
import xiaozhi.modules.agent.vo.AgentInfoVO;
|
||||
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||
@@ -41,6 +43,7 @@ public class AgentChatSummaryServiceImpl implements AgentChatSummaryService {
|
||||
|
||||
private final AgentChatHistoryService agentChatHistoryService;
|
||||
private final AgentService agentService;
|
||||
private final AgentChatTitleService agentChatTitleService;
|
||||
private final DeviceService deviceService;
|
||||
private final LLMService llmService;
|
||||
private final ModelConfigService modelConfigService;
|
||||
@@ -90,33 +93,40 @@ public class AgentChatSummaryServiceImpl implements AgentChatSummaryService {
|
||||
@Override
|
||||
public boolean generateAndSaveChatSummary(String sessionId) {
|
||||
try {
|
||||
// 1. 生成总结
|
||||
AgentChatSummaryDTO summaryDTO = generateChatSummary(sessionId);
|
||||
if (!summaryDTO.isSuccess()) {
|
||||
log.info("生成总结失败: {}", summaryDTO.getErrorMessage());
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. 获取设备信息(通过会话关联的设备)
|
||||
DeviceEntity device = getDeviceBySessionId(sessionId);
|
||||
if (device == null) {
|
||||
log.info("未找到与会话 {} 关联的设备", sessionId);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 3. 更新智能体记忆
|
||||
AgentMemoryDTO memoryDTO = new AgentMemoryDTO();
|
||||
memoryDTO.setSummaryMemory(summaryDTO.getSummary());
|
||||
String agentId = device.getAgentId();
|
||||
String memModelId = agentService.getAgentById(agentId).getMemModelId();
|
||||
|
||||
// 调用现有接口更新记忆
|
||||
agentService.updateAgentById(device.getAgentId(),
|
||||
new AgentUpdateDTO() {
|
||||
if (memModelId == null || memModelId.equals(Constant.MEMORY_MEM_REPORT_ONLY)) {
|
||||
log.info("会话 {} 使用仅上报聊天记录模式,跳过记忆总结", sessionId);
|
||||
return true;
|
||||
}
|
||||
|
||||
boolean shouldSummarizeMemory = !memModelId.equals(Constant.MEMORY_NO_MEM)
|
||||
&& !memModelId.equals(Constant.MEMORY_MEM0AI)
|
||||
&& !memModelId.equals(Constant.MEMORY_POWERMEM);
|
||||
|
||||
if (shouldSummarizeMemory) {
|
||||
AgentChatSummaryDTO summaryDTO = generateChatSummary(sessionId);
|
||||
if (summaryDTO.isSuccess()) {
|
||||
agentService.updateAgentById(agentId, new AgentUpdateDTO() {
|
||||
{
|
||||
setSummaryMemory(summaryDTO.getSummary());
|
||||
}
|
||||
});
|
||||
log.info("成功保存会话 {} 的聊天记录总结到智能体 {}", sessionId, agentId);
|
||||
} else {
|
||||
log.info("生成总结失败: {}", summaryDTO.getErrorMessage());
|
||||
}
|
||||
} else {
|
||||
log.info("会话 {} 使用 {} 模式,跳过记忆总结", sessionId, memModelId);
|
||||
}
|
||||
|
||||
log.info("成功保存会话 {} 的聊天记录总结到智能体 {}", sessionId, device.getAgentId());
|
||||
return true;
|
||||
|
||||
} catch (Exception e) {
|
||||
@@ -125,6 +135,98 @@ public class AgentChatSummaryServiceImpl implements AgentChatSummaryService {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean generateAndSaveChatTitle(String sessionId) {
|
||||
try {
|
||||
// 自动获取agentId
|
||||
String agentId = findAgentIdBySessionId(sessionId);
|
||||
if (StringUtils.isBlank(agentId)) {
|
||||
log.warn("会话 {} 无法获取智能体信息,跳过标题生成", sessionId);
|
||||
return false;
|
||||
}
|
||||
|
||||
List<AgentChatHistoryDTO> chatHistory = getChatHistoryBySessionId(sessionId);
|
||||
if (chatHistory == null || chatHistory.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
List<String> meaningfulMessages = extractMeaningfulMessages(chatHistory);
|
||||
if (meaningfulMessages.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
StringBuilder conversation = new StringBuilder();
|
||||
for (int i = 0; i < meaningfulMessages.size(); i++) {
|
||||
conversation.append("消息").append(i + 1).append(": ").append(meaningfulMessages.get(i)).append("\n");
|
||||
}
|
||||
|
||||
String slmModelId = getSlmModelId(agentId);
|
||||
String title = llmService.generateTitle(conversation.toString(), slmModelId);
|
||||
|
||||
if (StringUtils.isNotBlank(title)) {
|
||||
agentChatTitleService.saveOrUpdateTitle(sessionId, title);
|
||||
log.info("成功保存会话 {} 的标题: {}", sessionId, title);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (Exception e) {
|
||||
log.error("生成会话 {} 的标题时发生错误: {}", sessionId, e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private String getSlmModelId(String agentId) {
|
||||
try {
|
||||
if (StringUtils.isBlank(agentId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
AgentInfoVO agentInfo = agentService.getAgentById(agentId);
|
||||
if (agentInfo == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String slmModelId = agentInfo.getSlmModelId();
|
||||
if (StringUtils.isNotBlank(slmModelId)) {
|
||||
log.info("会话 {} 使用SLM模型: {}", agentId, slmModelId);
|
||||
return slmModelId;
|
||||
}
|
||||
|
||||
ModelConfigEntity defaultLlmConfig = getDefaultLLMConfig();
|
||||
if (defaultLlmConfig != null) {
|
||||
log.info("会话 {} 使用默认LLM模型: {}", agentId, defaultLlmConfig.getId());
|
||||
return defaultLlmConfig.getId();
|
||||
}
|
||||
|
||||
String llmModelId = agentInfo.getLlmModelId();
|
||||
log.info("会话 {} 使用LLM模型(最终回退): {}", agentId, llmModelId);
|
||||
return llmModelId;
|
||||
} catch (Exception e) {
|
||||
log.error("获取智能体slm模型ID失败,agentId: {}, 错误: {}", agentId, e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private ModelConfigEntity getDefaultLLMConfig() {
|
||||
try {
|
||||
List<ModelConfigEntity> llmConfigs = modelConfigService.getEnabledModelsByType("LLM");
|
||||
if (llmConfigs == null || llmConfigs.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (ModelConfigEntity config : llmConfigs) {
|
||||
if (config.getIsDefault() != null && config.getIsDefault() == 1) {
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
return llmConfigs.get(0);
|
||||
} catch (Exception e) {
|
||||
log.error("获取默认LLM配置失败: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据会话ID获取聊天记录
|
||||
*/
|
||||
@@ -305,15 +407,13 @@ public class AgentChatSummaryServiceImpl implements AgentChatSummaryService {
|
||||
*/
|
||||
private String callJavaLLMForSummaryWithHistory(String conversation, String historyMemory, String agentId) {
|
||||
try {
|
||||
// 获取智能体配置,从中提取记忆总结的模型ID
|
||||
String modelId = getMemorySummaryModelId(agentId);
|
||||
String modelId = getSlmModelId(agentId);
|
||||
|
||||
if (StringUtils.isBlank(modelId)) {
|
||||
log.info("未找到记忆总结的LLM模型配置,使用默认LLM服务");
|
||||
log.info("未找到SLM模型,使用默认LLM服务");
|
||||
return llmService.generateSummaryWithHistory(conversation, historyMemory, null, null);
|
||||
}
|
||||
|
||||
// 使用指定的模型ID调用LLM服务(支持历史记忆合并)
|
||||
String summary = llmService.generateSummaryWithHistory(conversation, historyMemory, null, modelId);
|
||||
|
||||
if (StringUtils.isNotBlank(summary) && !summary.equals("服务暂不可用") && !summary.equals("总结生成失败")) {
|
||||
@@ -333,15 +433,13 @@ public class AgentChatSummaryServiceImpl implements AgentChatSummaryService {
|
||||
*/
|
||||
private String callJavaLLMForSummary(String conversation, String agentId) {
|
||||
try {
|
||||
// 获取智能体配置,从中提取记忆总结的模型ID
|
||||
String modelId = getMemorySummaryModelId(agentId);
|
||||
String modelId = getSlmModelId(agentId);
|
||||
|
||||
if (StringUtils.isBlank(modelId)) {
|
||||
log.info("未找到记忆总结的LLM模型配置,使用默认LLM服务");
|
||||
log.info("未找到SLM模型,使用默认LLM服务");
|
||||
return llmService.generateSummary(conversation);
|
||||
}
|
||||
|
||||
// 使用指定的模型ID调用LLM服务
|
||||
String summary = llmService.generateSummaryWithModel(conversation, modelId);
|
||||
|
||||
if (StringUtils.isNotBlank(summary) && !summary.equals("服务暂不可用") && !summary.equals("总结生成失败")) {
|
||||
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
package xiaozhi.modules.agent.service.impl;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import xiaozhi.modules.agent.dao.AgentChatTitleDao;
|
||||
import xiaozhi.modules.agent.entity.AgentChatTitleEntity;
|
||||
import xiaozhi.modules.agent.service.AgentChatTitleService;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AgentChatTitleServiceImpl implements AgentChatTitleService {
|
||||
|
||||
private final AgentChatTitleDao agentChatTitleDao;
|
||||
|
||||
@Override
|
||||
public void saveOrUpdateTitle(String sessionId, String title) {
|
||||
if (StringUtils.isBlank(sessionId) || StringUtils.isBlank(title)) {
|
||||
return;
|
||||
}
|
||||
|
||||
QueryWrapper<AgentChatTitleEntity> wrapper = new QueryWrapper<>();
|
||||
wrapper.eq("session_id", sessionId);
|
||||
|
||||
AgentChatTitleEntity existing = agentChatTitleDao.selectOne(wrapper);
|
||||
|
||||
if (existing != null) {
|
||||
existing.setTitle(title);
|
||||
existing.setUpdatedAt(new Date());
|
||||
agentChatTitleDao.updateById(existing);
|
||||
} else {
|
||||
AgentChatTitleEntity newEntity = AgentChatTitleEntity.builder()
|
||||
.id(java.util.UUID.randomUUID().toString().replace("-", ""))
|
||||
.sessionId(sessionId)
|
||||
.title(title)
|
||||
.createdAt(new Date())
|
||||
.updatedAt(new Date())
|
||||
.build();
|
||||
agentChatTitleDao.insert(newEntity);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTitleBySessionId(String sessionId) {
|
||||
if (StringUtils.isBlank(sessionId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
QueryWrapper<AgentChatTitleEntity> wrapper = new QueryWrapper<>();
|
||||
wrapper.eq("session_id", sessionId);
|
||||
|
||||
AgentChatTitleEntity entity = agentChatTitleDao.selectOne(wrapper);
|
||||
return entity != null ? entity.getTitle() : null;
|
||||
}
|
||||
}
|
||||
+92
-20
@@ -31,19 +31,24 @@ import xiaozhi.common.utils.ConvertUtils;
|
||||
import xiaozhi.common.utils.JsonUtils;
|
||||
import xiaozhi.common.utils.ToolUtil;
|
||||
import xiaozhi.modules.agent.dao.AgentDao;
|
||||
import xiaozhi.modules.agent.dao.AgentTagDao;
|
||||
import xiaozhi.modules.agent.dto.AgentCreateDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentTagDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentUpdateDTO;
|
||||
import xiaozhi.modules.agent.entity.AgentContextProviderEntity;
|
||||
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||
import xiaozhi.modules.agent.entity.AgentPluginMapping;
|
||||
import xiaozhi.modules.agent.entity.AgentTagEntity;
|
||||
import xiaozhi.modules.agent.entity.AgentTemplateEntity;
|
||||
import xiaozhi.modules.agent.service.AgentChatHistoryService;
|
||||
import xiaozhi.modules.agent.service.AgentContextProviderService;
|
||||
import xiaozhi.modules.agent.service.AgentPluginMappingService;
|
||||
import xiaozhi.modules.agent.service.AgentService;
|
||||
import xiaozhi.modules.agent.service.AgentTagService;
|
||||
import xiaozhi.modules.agent.service.AgentTemplateService;
|
||||
import xiaozhi.modules.agent.vo.AgentInfoVO;
|
||||
import xiaozhi.modules.correctword.service.CorrectWordFileService;
|
||||
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||
import xiaozhi.modules.device.service.DeviceService;
|
||||
import xiaozhi.modules.model.dto.ModelProviderDTO;
|
||||
@@ -59,6 +64,7 @@ import xiaozhi.modules.timbre.service.TimbreService;
|
||||
@AllArgsConstructor
|
||||
public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> implements AgentService {
|
||||
private final AgentDao agentDao;
|
||||
private final AgentTagDao agentTagDao;
|
||||
private final TimbreService timbreModelService;
|
||||
private final ModelConfigService modelConfigService;
|
||||
private final RedisUtils redisUtils;
|
||||
@@ -68,6 +74,8 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
||||
private final AgentTemplateService agentTemplateService;
|
||||
private final ModelProviderService modelProviderService;
|
||||
private final AgentContextProviderService agentContextProviderService;
|
||||
private final AgentTagService agentTagService;
|
||||
private final CorrectWordFileService correctWordFileService;
|
||||
|
||||
@Override
|
||||
public PageData<AgentEntity> adminAgentList(Map<String, Object> params) {
|
||||
@@ -98,6 +106,10 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
||||
agent.setContextProviders(contextProviderEntity.getContextProviders());
|
||||
}
|
||||
|
||||
// 查询替换词文件ID列表
|
||||
List<String> correctWordFileIds = correctWordFileService.getAgentCorrectWordFileIds(id);
|
||||
agent.setCorrectWordFileIds(correctWordFileIds);
|
||||
|
||||
// 无需额外查询插件列表,已通过SQL查询出来
|
||||
return agent;
|
||||
}
|
||||
@@ -134,32 +146,32 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
||||
QueryWrapper<AgentEntity> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.eq("user_id", userId).orderByDesc("created_at");
|
||||
|
||||
// 如果有搜索关键词,根据搜索类型添加相应的查询条件
|
||||
if (StringUtils.isNotBlank(keyword)) {
|
||||
if ("mac".equals(searchType)) {
|
||||
// 按MAC地址搜索:先搜索设备,再获取对应的智能体
|
||||
queryWrapper.and(w -> {
|
||||
// 按名称搜索
|
||||
w.like("agent_name", keyword);
|
||||
|
||||
// 按MAC地址搜索:先查设备,再获取对应的智能体ID
|
||||
List<DeviceEntity> devices = Optional
|
||||
.ofNullable(deviceService.searchDevicesByMacAddress(keyword, userId)).orElseGet(ArrayList::new);
|
||||
// 获取设备对应的智能体ID列表
|
||||
.ofNullable(deviceService.searchDevicesByMacAddress(keyword, userId))
|
||||
.orElseGet(ArrayList::new);
|
||||
List<String> agentIds = devices.stream()
|
||||
.map(DeviceEntity::getAgentId)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
if (ToolUtil.isNotEmpty(agentIds)) {
|
||||
queryWrapper.in("id", agentIds);
|
||||
} else {
|
||||
return new ArrayList<>();
|
||||
w.or().in("id", agentIds);
|
||||
}
|
||||
} else {
|
||||
// 按名称搜索
|
||||
queryWrapper.like("agent_name", keyword);
|
||||
}
|
||||
|
||||
// 按标签名搜索
|
||||
List<String> tagAgentIds = agentTagService.getAgentIdsByTagName(keyword);
|
||||
if (ToolUtil.isNotEmpty(tagAgentIds)) {
|
||||
w.or().in("id", tagAgentIds);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 执行查询
|
||||
List<AgentEntity> agentEntities = baseDao.selectList(queryWrapper);
|
||||
|
||||
// 转换为DTO并设置所有必要字段
|
||||
return agentEntities.stream().map(this::buildAgentDTO).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@@ -193,6 +205,19 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
||||
// 获取设备数量
|
||||
dto.setDeviceCount(getDeviceCountByAgentId(agent.getId()));
|
||||
|
||||
// 获取标签列表
|
||||
List<AgentTagEntity> tags = agentTagDao.selectByAgentId(agent.getId());
|
||||
if (ToolUtil.isNotEmpty(tags)) {
|
||||
dto.setTags(tags.stream().map(this::convertTagToDTO).collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
private AgentTagDTO convertTagToDTO(AgentTagEntity entity) {
|
||||
AgentTagDTO dto = new AgentTagDTO();
|
||||
dto.setId(entity.getId());
|
||||
dto.setTagName(entity.getTagName());
|
||||
return dto;
|
||||
}
|
||||
|
||||
@@ -273,6 +298,9 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
||||
if (dto.getLlmModelId() != null) {
|
||||
existingEntity.setLlmModelId(dto.getLlmModelId());
|
||||
}
|
||||
if (dto.getSlmModelId() != null) {
|
||||
existingEntity.setSlmModelId(dto.getSlmModelId());
|
||||
}
|
||||
if (dto.getVllmModelId() != null) {
|
||||
existingEntity.setVllmModelId(dto.getVllmModelId());
|
||||
}
|
||||
@@ -282,6 +310,18 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
||||
if (dto.getTtsVoiceId() != null) {
|
||||
existingEntity.setTtsVoiceId(dto.getTtsVoiceId());
|
||||
}
|
||||
if (dto.getTtsLanguage() != null) {
|
||||
existingEntity.setTtsLanguage(dto.getTtsLanguage());
|
||||
}
|
||||
if (dto.getTtsVolume() != null) {
|
||||
existingEntity.setTtsVolume(dto.getTtsVolume());
|
||||
}
|
||||
if (dto.getTtsRate() != null) {
|
||||
existingEntity.setTtsRate(dto.getTtsRate());
|
||||
}
|
||||
if (dto.getTtsPitch() != null) {
|
||||
existingEntity.setTtsPitch(dto.getTtsPitch());
|
||||
}
|
||||
if (dto.getMemModelId() != null) {
|
||||
existingEntity.setMemModelId(dto.getMemModelId());
|
||||
}
|
||||
@@ -367,13 +407,14 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
||||
existingEntity.setUpdatedAt(new Date());
|
||||
|
||||
// 更新记忆策略
|
||||
if (existingEntity.getMemModelId() == null || existingEntity.getMemModelId().equals(Constant.MEMORY_NO_MEM)) {
|
||||
// 删除所有记录
|
||||
// 删除所有记录
|
||||
if (existingEntity.getMemModelId() != null && existingEntity.getMemModelId().equals(Constant.MEMORY_NO_MEM)) {
|
||||
agentChatHistoryService.deleteByAgentId(existingEntity.getId(), true, true);
|
||||
existingEntity.setSummaryMemory("");
|
||||
} else if (existingEntity.getChatHistoryConf() != null && existingEntity.getChatHistoryConf() == 1) {
|
||||
// 删除音频数据
|
||||
agentChatHistoryService.deleteByAgentId(existingEntity.getId(), true, false);
|
||||
// 删除记忆
|
||||
} else if (existingEntity.getMemModelId() != null
|
||||
&& existingEntity.getMemModelId().equals(Constant.MEMORY_MEM_REPORT_ONLY)) {
|
||||
existingEntity.setSummaryMemory("");
|
||||
}
|
||||
|
||||
// 更新上下文源配置
|
||||
@@ -384,6 +425,11 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
||||
agentContextProviderService.saveOrUpdateByAgentId(contextEntity);
|
||||
}
|
||||
|
||||
// 更新替换词文件关联
|
||||
if (dto.getCorrectWordFileIds() != null) {
|
||||
correctWordFileService.saveAgentCorrectWords(agentId, dto.getCorrectWordFileIds());
|
||||
}
|
||||
|
||||
boolean b = validateLLMIntentParams(dto.getLlmModelId(), dto.getIntentModelId());
|
||||
if (!b) {
|
||||
throw new RenException(ErrorCode.LLM_INTENT_PARAMS_MISMATCH);
|
||||
@@ -466,6 +512,13 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
||||
entity.setLanguage(template.getLanguage());
|
||||
}
|
||||
|
||||
if (entity.getSlmModelId() == null) {
|
||||
String defaultSlmModelId = getDefaultLLMModelId();
|
||||
if (defaultSlmModelId != null) {
|
||||
entity.setSlmModelId(defaultSlmModelId);
|
||||
}
|
||||
}
|
||||
|
||||
// 设置用户ID和创建者信息
|
||||
UserDetail user = SecurityUser.getUser();
|
||||
entity.setUserId(user.getId());
|
||||
@@ -504,4 +557,23 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
||||
return entity.getId();
|
||||
}
|
||||
|
||||
private String getDefaultLLMModelId() {
|
||||
try {
|
||||
List<ModelConfigEntity> llmConfigs = modelConfigService.getEnabledModelsByType("LLM");
|
||||
if (llmConfigs == null || llmConfigs.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (ModelConfigEntity config : llmConfigs) {
|
||||
if (config.getIsDefault() != null && config.getIsDefault() == 1) {
|
||||
return config.getId();
|
||||
}
|
||||
}
|
||||
|
||||
return llmConfigs.get(0).getId();
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+198
@@ -0,0 +1,198 @@
|
||||
package xiaozhi.modules.agent.service.impl;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import xiaozhi.common.exception.ErrorCode;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||
import xiaozhi.modules.agent.dao.AgentTagDao;
|
||||
import xiaozhi.modules.agent.dao.AgentTagRelationDao;
|
||||
import xiaozhi.modules.agent.dto.AgentTagDTO;
|
||||
import xiaozhi.modules.agent.entity.AgentTagEntity;
|
||||
import xiaozhi.modules.agent.entity.AgentTagRelationEntity;
|
||||
import xiaozhi.modules.agent.service.AgentTagService;
|
||||
|
||||
@Service
|
||||
@AllArgsConstructor
|
||||
public class AgentTagServiceImpl extends BaseServiceImpl<AgentTagDao, AgentTagEntity> implements AgentTagService {
|
||||
|
||||
private final AgentTagRelationDao agentTagRelationDao;
|
||||
|
||||
@Override
|
||||
public AgentTagEntity saveTag(String tagName) {
|
||||
if (tagName == null || tagName.trim().isEmpty()) {
|
||||
throw new RenException(ErrorCode.AGENT_TAG_NAME_EMPTY);
|
||||
}
|
||||
|
||||
QueryWrapper<AgentTagEntity> wrapper = new QueryWrapper<>();
|
||||
wrapper.eq("tag_name", tagName);
|
||||
wrapper.eq("deleted", 0);
|
||||
AgentTagEntity existTag = baseDao.selectOne(wrapper);
|
||||
if (existTag != null) {
|
||||
return existTag;
|
||||
}
|
||||
|
||||
AgentTagEntity tag = new AgentTagEntity();
|
||||
tag.setId(UUID.randomUUID().toString().replace("-", ""));
|
||||
tag.setTagName(tagName);
|
||||
tag.setSort(0);
|
||||
tag.setCreatedAt(new Date());
|
||||
tag.setUpdatedAt(new Date());
|
||||
tag.setDeleted(0);
|
||||
baseDao.insert(tag);
|
||||
return tag;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteTag(String tagId) {
|
||||
AgentTagEntity tag = baseDao.selectById(tagId);
|
||||
if (tag != null) {
|
||||
tag.setDeleted(1);
|
||||
tag.setUpdatedAt(new Date());
|
||||
baseDao.updateById(tag);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AgentTagDTO> getTagsByAgentId(String agentId) {
|
||||
List<AgentTagEntity> tags = baseDao.selectByAgentId(agentId);
|
||||
return tags.stream().map(this::convertToDTO).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void saveAgentTags(String agentId, List<String> tagIds, List<String> tagNames) {
|
||||
agentTagRelationDao.deleteByAgentId(agentId);
|
||||
|
||||
List<AgentTagEntity> currentTags = baseDao.selectByAgentId(agentId);
|
||||
List<String> currentTagNames = currentTags.stream()
|
||||
.map(AgentTagEntity::getTagName)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
List<String> allTagIds = new ArrayList<>();
|
||||
List<String> newTagNames = new ArrayList<>();
|
||||
|
||||
if (tagNames != null && !tagNames.isEmpty()) {
|
||||
Set<String> addedTagNames = new HashSet<>();
|
||||
for (String tagName : tagNames) {
|
||||
if (tagName == null || tagName.trim().isEmpty()) {
|
||||
throw new RenException(ErrorCode.AGENT_TAG_NAME_EMPTY);
|
||||
}
|
||||
|
||||
if (currentTagNames.contains(tagName) || addedTagNames.contains(tagName)) {
|
||||
throw new RenException(ErrorCode.AGENT_TAG_NAME_DUPLICATE);
|
||||
}
|
||||
addedTagNames.add(tagName);
|
||||
newTagNames.add(tagName);
|
||||
}
|
||||
}
|
||||
|
||||
List<AgentTagEntity> existTags = new ArrayList<>();
|
||||
if (!newTagNames.isEmpty()) {
|
||||
existTags = baseDao.selectByTagNames(newTagNames);
|
||||
}
|
||||
|
||||
Map<String, AgentTagEntity> existTagMap = existTags.stream()
|
||||
.collect(Collectors.toMap(AgentTagEntity::getTagName, t -> t, (a, b) -> a));
|
||||
|
||||
List<AgentTagEntity> tagsToInsert = new ArrayList<>();
|
||||
for (String tagName : newTagNames) {
|
||||
AgentTagEntity existTag = existTagMap.get(tagName);
|
||||
if (existTag != null) {
|
||||
allTagIds.add(existTag.getId());
|
||||
} else {
|
||||
AgentTagEntity tag = new AgentTagEntity();
|
||||
tag.setId(UUID.randomUUID().toString().replace("-", ""));
|
||||
tag.setTagName(tagName);
|
||||
tag.setSort(0);
|
||||
tag.setDeleted(0);
|
||||
tag.setCreatedAt(new Date());
|
||||
tag.setUpdatedAt(new Date());
|
||||
tagsToInsert.add(tag);
|
||||
}
|
||||
}
|
||||
|
||||
if (!tagsToInsert.isEmpty()) {
|
||||
baseDao.batchInsert(tagsToInsert);
|
||||
for (AgentTagEntity tag : tagsToInsert) {
|
||||
allTagIds.add(tag.getId());
|
||||
}
|
||||
}
|
||||
|
||||
if (tagIds != null && !tagIds.isEmpty()) {
|
||||
List<AgentTagEntity> tagIdEntities = baseDao.selectBatchIds(tagIds);
|
||||
for (AgentTagEntity tag : tagIdEntities) {
|
||||
if (tag != null && (currentTagNames.contains(tag.getTagName()) ||
|
||||
newTagNames.contains(tag.getTagName()))) {
|
||||
throw new RenException(ErrorCode.AGENT_TAG_NAME_DUPLICATE);
|
||||
}
|
||||
}
|
||||
allTagIds.addAll(tagIds);
|
||||
}
|
||||
|
||||
if (allTagIds.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<AgentTagRelationEntity> relations = new ArrayList<>();
|
||||
Date now = new Date();
|
||||
int sort = 0;
|
||||
for (String tagId : allTagIds) {
|
||||
AgentTagRelationEntity relation = new AgentTagRelationEntity();
|
||||
relation.setId(UUID.randomUUID().toString().replace("-", ""));
|
||||
relation.setAgentId(agentId);
|
||||
relation.setTagId(tagId);
|
||||
relation.setSort(sort++);
|
||||
relation.setCreatedAt(now);
|
||||
relation.setUpdatedAt(now);
|
||||
relations.add(relation);
|
||||
}
|
||||
agentTagRelationDao.batchInsertRelation(relations);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void deleteAgentTags(String agentId) {
|
||||
agentTagRelationDao.deleteByAgentId(agentId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AgentTagDTO> getTagsByAgentIds(List<String> agentIds) {
|
||||
if (agentIds == null || agentIds.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
List<AgentTagEntity> tags = baseDao.selectByAgentIds(agentIds);
|
||||
return tags.stream().map(this::convertToDTO).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AgentTagDTO> getAllTags() {
|
||||
List<AgentTagEntity> tags = baseDao.selectAll();
|
||||
return tags.stream().map(this::convertToDTO).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getAgentIdsByTagName(String tagName) {
|
||||
return baseDao.selectAgentIdsByTagName(tagName);
|
||||
}
|
||||
|
||||
private AgentTagDTO convertToDTO(AgentTagEntity entity) {
|
||||
AgentTagDTO dto = new AgentTagDTO();
|
||||
dto.setId(entity.getId());
|
||||
dto.setTagName(entity.getTagName());
|
||||
return dto;
|
||||
}
|
||||
}
|
||||
@@ -25,4 +25,7 @@ public class AgentInfoVO extends AgentEntity
|
||||
|
||||
@Schema(description = "上下文源配置")
|
||||
private List<ContextProviderDTO> contextProviders;
|
||||
|
||||
@Schema(description = "替换词文件ID列表")
|
||||
private List<String> correctWordFileIds;
|
||||
}
|
||||
|
||||
+11
@@ -1,5 +1,7 @@
|
||||
package xiaozhi.modules.config.controller;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
@@ -12,6 +14,7 @@ import lombok.AllArgsConstructor;
|
||||
import xiaozhi.common.utils.Result;
|
||||
import xiaozhi.common.validator.ValidatorUtils;
|
||||
import xiaozhi.modules.config.dto.AgentModelsDTO;
|
||||
import xiaozhi.modules.config.dto.CorrectWordsDTO;
|
||||
import xiaozhi.modules.config.service.ConfigService;
|
||||
|
||||
/**
|
||||
@@ -41,4 +44,12 @@ public class ConfigController {
|
||||
Object models = configService.getAgentModels(dto.getMacAddress(), dto.getSelectedModule());
|
||||
return new Result<Object>().ok(models);
|
||||
}
|
||||
|
||||
@PostMapping("correct-words")
|
||||
@Operation(summary = "获取智能体替换词")
|
||||
public Result<Object> getCorrectWords(@Valid @RequestBody CorrectWordsDTO dto) {
|
||||
ValidatorUtils.validateEntity(dto);
|
||||
List<String> list = configService.getCorrectWords(dto.getMacAddress());
|
||||
return new Result<Object>().ok(list);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package xiaozhi.modules.config.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Schema(description = "获取智能体替换词DTO")
|
||||
public class CorrectWordsDTO {
|
||||
|
||||
@NotBlank(message = "设备MAC地址不能为空")
|
||||
@Schema(description = "设备MAC地址")
|
||||
private String macAddress;
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
package xiaozhi.modules.config.service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public interface ConfigService {
|
||||
/**
|
||||
* 获取服务器配置
|
||||
*
|
||||
*
|
||||
* @param isCache 是否缓存
|
||||
* @return 配置信息
|
||||
*/
|
||||
@@ -13,10 +14,18 @@ public interface ConfigService {
|
||||
|
||||
/**
|
||||
* 获取智能体模型配置
|
||||
*
|
||||
*
|
||||
* @param macAddress MAC地址
|
||||
* @param selectedModule 客户端已实例化的模型
|
||||
* @return 模型配置信息
|
||||
*/
|
||||
Map<String, Object> getAgentModels(String macAddress, Map<String, String> selectedModule);
|
||||
|
||||
/**
|
||||
* 获取智能体替换词
|
||||
*
|
||||
* @param macAddress 设备MAC地址
|
||||
* @return 替换词列表,格式如 ["模板1|模板01", "模板2|模板02"]
|
||||
*/
|
||||
List<String> getCorrectWords(String macAddress);
|
||||
}
|
||||
+66
-7
@@ -1,10 +1,12 @@
|
||||
package xiaozhi.modules.config.service.impl;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -30,7 +32,9 @@ import xiaozhi.modules.agent.service.AgentMcpAccessPointService;
|
||||
import xiaozhi.modules.agent.service.AgentPluginMappingService;
|
||||
import xiaozhi.modules.agent.service.AgentService;
|
||||
import xiaozhi.modules.agent.service.AgentTemplateService;
|
||||
import xiaozhi.modules.correctword.service.CorrectWordFileService;
|
||||
import xiaozhi.modules.agent.vo.AgentVoicePrintVO;
|
||||
import xiaozhi.modules.correctword.vo.CorrectWordSimpleVO;
|
||||
import xiaozhi.modules.config.service.ConfigService;
|
||||
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||
import xiaozhi.modules.device.service.DeviceService;
|
||||
@@ -58,6 +62,7 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
private final AgentContextProviderService agentContextProviderService;
|
||||
private final VoiceCloneService cloneVoiceService;
|
||||
private final AgentVoicePrintDao agentVoicePrintDao;
|
||||
private final CorrectWordFileService correctWordFileService;
|
||||
|
||||
@Override
|
||||
public Object getConfig(Boolean isCache) {
|
||||
@@ -87,6 +92,10 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
agent.getVadModelId(),
|
||||
agent.getAsrModelId(),
|
||||
null,
|
||||
@@ -95,6 +104,7 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
result,
|
||||
isCache);
|
||||
|
||||
@@ -109,7 +119,7 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
// 检查是否为管理控制台请求
|
||||
String redisKey = RedisKeys.getTmpRegisterMacKey(macAddress);
|
||||
Object isAdminRequest = redisUtils.get(redisKey);
|
||||
|
||||
|
||||
if (isAdminRequest != null && "true".equals(isAdminRequest)) {
|
||||
// 管理控制台请求,返回getConfig的结果
|
||||
redisUtils.delete(redisKey); // 使用后清理
|
||||
@@ -135,15 +145,24 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
String voice = null;
|
||||
String referenceAudio = null;
|
||||
String referenceText = null;
|
||||
String language = null;
|
||||
TimbreDetailsVO timbre = timbreService.get(agent.getTtsVoiceId());
|
||||
if (timbre != null) {
|
||||
voice = timbre.getTtsVoice();
|
||||
referenceAudio = timbre.getReferenceAudio();
|
||||
referenceText = timbre.getReferenceText();
|
||||
// 优先使用用户选择的语言,如果没有则使用音色支持的第一个语言
|
||||
if (StringUtils.isNotBlank(agent.getTtsLanguage())) {
|
||||
language = agent.getTtsLanguage();
|
||||
} else if (StringUtils.isNotBlank(timbre.getLanguages())) {
|
||||
language = timbre.getLanguages().split("、")[0].trim();
|
||||
}
|
||||
} else {
|
||||
VoiceCloneEntity voice_print = cloneVoiceService.selectById(agent.getTtsVoiceId());
|
||||
if (voice_print != null) {
|
||||
voice = voice_print.getVoiceId();
|
||||
// 优先使用用户选择的语言,如果没有则使用默认值
|
||||
language = StringUtils.isNotBlank(agent.getTtsLanguage()) ? agent.getTtsLanguage() : "普通话";
|
||||
}
|
||||
}
|
||||
// 构建返回数据
|
||||
@@ -190,10 +209,11 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
mcpEndpoint = mcpEndpoint.replace("/mcp/", "/call/");
|
||||
result.put("mcp_endpoint", mcpEndpoint);
|
||||
}
|
||||
|
||||
|
||||
// 获取上下文源配置
|
||||
AgentContextProviderEntity contextProviderEntity = agentContextProviderService.getByAgentId(agent.getId());
|
||||
if (contextProviderEntity != null && contextProviderEntity.getContextProviders() != null && !contextProviderEntity.getContextProviders().isEmpty()) {
|
||||
if (contextProviderEntity != null && contextProviderEntity.getContextProviders() != null
|
||||
&& !contextProviderEntity.getContextProviders().isEmpty()) {
|
||||
result.put("context_providers", contextProviderEntity.getContextProviders());
|
||||
}
|
||||
|
||||
@@ -208,10 +228,15 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
voice,
|
||||
referenceAudio,
|
||||
referenceText,
|
||||
language,
|
||||
agent.getTtsVolume(),
|
||||
agent.getTtsRate(),
|
||||
agent.getTtsPitch(),
|
||||
agent.getVadModelId(),
|
||||
agent.getAsrModelId(),
|
||||
agent.getLlmModelId(),
|
||||
agent.getVllmModelId(),
|
||||
agent.getSlmModelId(),
|
||||
agent.getTtsModelId(),
|
||||
agent.getMemModelId(),
|
||||
agent.getIntentModelId(),
|
||||
@@ -222,6 +247,18 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getCorrectWords(String macAddress) {
|
||||
DeviceEntity device = deviceService.getDeviceByMacAddress(macAddress);
|
||||
if (device == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<CorrectWordSimpleVO> items = correctWordFileService.getAllItemsByAgentId(device.getAgentId());
|
||||
return items.stream()
|
||||
.map(item -> item.getSourceWord() + "|" + item.getTargetWord())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建配置信息
|
||||
*
|
||||
@@ -385,10 +422,15 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
String voice,
|
||||
String referenceAudio,
|
||||
String referenceText,
|
||||
String language,
|
||||
Integer ttsVolume,
|
||||
Integer ttsRate,
|
||||
Integer ttsPitch,
|
||||
String vadModelId,
|
||||
String asrModelId,
|
||||
String llmModelId,
|
||||
String vllmModelId,
|
||||
String slmModelId,
|
||||
String ttsModelId,
|
||||
String memModelId,
|
||||
String intentModelId,
|
||||
@@ -397,9 +439,9 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
boolean isCache) {
|
||||
Map<String, String> selectedModule = new HashMap<>();
|
||||
|
||||
String[] modelTypes = { "VAD", "ASR", "TTS", "Memory", "Intent", "LLM", "VLLM", "RAG" };
|
||||
String[] modelTypes = { "VAD", "ASR", "TTS", "Memory", "Intent", "LLM", "VLLM", "SLM", "RAG" };
|
||||
String[] modelIds = { vadModelId, asrModelId, ttsModelId, memModelId, intentModelId, llmModelId, vllmModelId,
|
||||
ragModelId };
|
||||
slmModelId, ragModelId };
|
||||
String intentLLMModelId = null;
|
||||
String memLocalShortLLMModelId = null;
|
||||
|
||||
@@ -423,11 +465,19 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
((Map<String, Object>) model.getConfigJson()).put("ref_audio", referenceAudio);
|
||||
if (referenceText != null)
|
||||
((Map<String, Object>) model.getConfigJson()).put("ref_text", referenceText);
|
||||
if (language != null)
|
||||
((Map<String, Object>) model.getConfigJson()).put("language", language);
|
||||
if (ttsVolume != null)
|
||||
((Map<String, Object>) model.getConfigJson()).put("ttsVolume", ttsVolume);
|
||||
if (ttsRate != null)
|
||||
((Map<String, Object>) model.getConfigJson()).put("ttsRate", ttsRate);
|
||||
if (ttsPitch != null)
|
||||
((Map<String, Object>) model.getConfigJson()).put("ttsPitch", ttsPitch);
|
||||
|
||||
// 火山引擎声音克隆需要替换resource_id
|
||||
Map<String, Object> map = (Map<String, Object>) model.getConfigJson();
|
||||
if (Constant.VOICE_CLONE_HUOSHAN_DOUBLE_STREAM.equals(map.get("type"))) {
|
||||
// 如果voice是”S_“开头的,使用seed-icl-1.0
|
||||
// 如果voice是”S_”开头的,使用seed-icl-1.0
|
||||
if (voice != null && voice.startsWith("S_")) {
|
||||
map.put("resource_id", "seed-icl-1.0");
|
||||
}
|
||||
@@ -445,7 +495,7 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
if (map.get("functions") != null) {
|
||||
String functionStr = (String) map.get("functions");
|
||||
if (StringUtils.isNotBlank(functionStr)) {
|
||||
String[] functions = functionStr.split("\\;");
|
||||
String[] functions = functionStr.split(";");
|
||||
map.put("functions", functions);
|
||||
}
|
||||
}
|
||||
@@ -478,6 +528,15 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
typeConfig.put(memLocalShortLLM.getId(), memLocalShortLLM.getConfigJson());
|
||||
}
|
||||
}
|
||||
// LLM也返回所选的SLM,如果同名id则不重复显示
|
||||
if (StringUtils.isNotBlank(slmModelId) && !slmModelId.equals(llmModelId)) {
|
||||
if (!typeConfig.containsKey(slmModelId)) {
|
||||
ModelConfigEntity slmModel = modelConfigService.getModelByIdFromCache(slmModelId);
|
||||
if (slmModel != null && slmModel.getConfigJson() != null) {
|
||||
typeConfig.put(slmModel.getId(), slmModel.getConfigJson());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
result.put(modelTypes[i], typeConfig);
|
||||
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
package xiaozhi.modules.correctword.controller;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
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.PutMapping;
|
||||
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 io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.Parameters;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.AllArgsConstructor;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.utils.Result;
|
||||
import xiaozhi.modules.correctword.dto.CorrectWordFileCreateDTO;
|
||||
import xiaozhi.modules.correctword.service.CorrectWordFileService;
|
||||
import xiaozhi.modules.correctword.vo.CorrectWordFileVO;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/correct-word")
|
||||
@Tag(name = "替换词管理")
|
||||
@AllArgsConstructor
|
||||
public class CorrectWordController {
|
||||
|
||||
private final CorrectWordFileService correctWordFileService;
|
||||
|
||||
@PostMapping("/file")
|
||||
@Operation(summary = "创建替换词文件")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<CorrectWordFileVO> createFile(@Valid @RequestBody CorrectWordFileCreateDTO dto) {
|
||||
CorrectWordFileVO vo = correctWordFileService.createFile(dto);
|
||||
return new Result<CorrectWordFileVO>().ok(vo);
|
||||
}
|
||||
|
||||
@PutMapping("/file/{fileId}")
|
||||
@Operation(summary = "修改替换词文件")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<Void> updateFile(@PathVariable String fileId, @Valid @RequestBody CorrectWordFileCreateDTO dto) {
|
||||
correctWordFileService.updateFile(fileId, dto);
|
||||
return new Result<>();
|
||||
}
|
||||
|
||||
@GetMapping("/file/list")
|
||||
@Operation(summary = "分页获取当前用户替换词文件列表")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
@Parameters({
|
||||
@Parameter(name = Constant.PAGE, description = "当前页码,从1开始", required = true),
|
||||
@Parameter(name = Constant.LIMIT, description = "每页显示记录数", required = true),
|
||||
})
|
||||
public Result<PageData<CorrectWordFileVO>> listFiles(
|
||||
@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
|
||||
PageData<CorrectWordFileVO> page = correctWordFileService.listFiles(params);
|
||||
return new Result<PageData<CorrectWordFileVO>>().ok(page);
|
||||
}
|
||||
|
||||
@GetMapping("/file/select")
|
||||
@Operation(summary = "智能体获取当前用户替换词文件列表")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<List<CorrectWordFileVO>> listAllFiles() {
|
||||
List<CorrectWordFileVO> list = correctWordFileService.listAllFiles();
|
||||
return new Result<List<CorrectWordFileVO>>().ok(list);
|
||||
}
|
||||
|
||||
@GetMapping("/file/download/{fileId}")
|
||||
@Operation(summary = "下载替换词文件")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public ResponseEntity<byte[]> downloadFile(@PathVariable String fileId) {
|
||||
CorrectWordFileVO vo = correctWordFileService.getFileContent(fileId);
|
||||
if (vo == null || vo.getContent() == null || vo.getContent().isEmpty()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
byte[] bytes = String.join("\n", vo.getContent()).getBytes(StandardCharsets.UTF_8);
|
||||
String encodedFileName = URLEncoder.encode(vo.getFileName(), StandardCharsets.UTF_8).replace("+", "%20");
|
||||
String asciiFileName = vo.getFileName().replaceAll("[^\\x00-\\x7F]", "_");
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION,
|
||||
"attachment; filename=\"" + asciiFileName + "\"; filename*=UTF-8''" + encodedFileName)
|
||||
.contentType(MediaType.APPLICATION_OCTET_STREAM)
|
||||
.body(bytes);
|
||||
}
|
||||
|
||||
@DeleteMapping("/file/{fileId}")
|
||||
@Operation(summary = "删除替换词文件")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<Void> deleteFile(@PathVariable String fileId) {
|
||||
correctWordFileService.deleteFile(fileId);
|
||||
return new Result<>();
|
||||
}
|
||||
|
||||
@PostMapping("/file/batch-delete")
|
||||
@Operation(summary = "批量删除替换词文件")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<Void> batchDeleteFiles(@RequestBody List<String> fileIds) {
|
||||
if (fileIds == null || fileIds.isEmpty()) {
|
||||
return new Result<>();
|
||||
}
|
||||
correctWordFileService.batchDeleteFiles(fileIds);
|
||||
return new Result<>();
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package xiaozhi.modules.correctword.dao;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import xiaozhi.common.dao.BaseDao;
|
||||
import xiaozhi.modules.correctword.entity.CorrectWordFileEntity;
|
||||
|
||||
@Mapper
|
||||
public interface CorrectWordFileDao extends BaseDao<CorrectWordFileEntity> {
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package xiaozhi.modules.correctword.dao;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import xiaozhi.common.dao.BaseDao;
|
||||
import xiaozhi.modules.correctword.entity.CorrectWordItemEntity;
|
||||
|
||||
@Mapper
|
||||
public interface CorrectWordItemDao extends BaseDao<CorrectWordItemEntity> {
|
||||
|
||||
int batchInsert(@Param("list") List<CorrectWordItemEntity> items);
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package xiaozhi.modules.correctword.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Schema(description = "创建替换词文件DTO")
|
||||
public class CorrectWordFileCreateDTO {
|
||||
|
||||
@NotBlank(message = "文件名不能为空")
|
||||
@Schema(description = "文件名")
|
||||
private String fileName;
|
||||
|
||||
@NotEmpty(message = "替换词内容不能为空")
|
||||
@Schema(description = "替换词内容,每条格式:原词|替换词")
|
||||
private List<String> content;
|
||||
|
||||
@Schema(description = "文件大小(字节),不能超过1MB")
|
||||
private Long fileSize;
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package xiaozhi.modules.correctword.entity;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@TableName("ai_agent_correct_word_file")
|
||||
@Schema(description = "智能体替换词文件")
|
||||
public class CorrectWordFileEntity {
|
||||
|
||||
@TableId(type = IdType.ASSIGN_UUID)
|
||||
@Schema(description = "替换词文件ID")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "原始文件名")
|
||||
private String fileName;
|
||||
|
||||
@Schema(description = "替换词数量")
|
||||
private Integer wordCount;
|
||||
|
||||
@Schema(description = "文件原始内容(用于下载)")
|
||||
private String content;
|
||||
|
||||
@Schema(description = "创建者")
|
||||
private Long creator;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
private Date createdAt;
|
||||
|
||||
@Schema(description = "更新者")
|
||||
private Long updater;
|
||||
|
||||
@Schema(description = "更新时间")
|
||||
private Date updatedAt;
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package xiaozhi.modules.correctword.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@TableName("ai_agent_correct_word_item")
|
||||
@Schema(description = "替换词词条")
|
||||
public class CorrectWordItemEntity {
|
||||
|
||||
@TableId(type = IdType.ASSIGN_UUID)
|
||||
@Schema(description = "词条ID")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "所属文件ID")
|
||||
private String fileId;
|
||||
|
||||
@Schema(description = "原词")
|
||||
private String sourceWord;
|
||||
|
||||
@Schema(description = "替换词")
|
||||
private String targetWord;
|
||||
}
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
package xiaozhi.modules.correctword.service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.modules.correctword.dto.CorrectWordFileCreateDTO;
|
||||
import xiaozhi.modules.correctword.vo.CorrectWordFileVO;
|
||||
import xiaozhi.modules.correctword.vo.CorrectWordSimpleVO;
|
||||
|
||||
public interface CorrectWordFileService {
|
||||
|
||||
/**
|
||||
* 创建替换词文件
|
||||
*
|
||||
* @param dto 创建参数
|
||||
* @return 文件VO
|
||||
*/
|
||||
CorrectWordFileVO createFile(CorrectWordFileCreateDTO dto);
|
||||
|
||||
/**
|
||||
* 修改替换词文件(全量替换词条)
|
||||
*
|
||||
* @param fileId 文件ID
|
||||
* @param dto 修改参数
|
||||
*/
|
||||
void updateFile(String fileId, CorrectWordFileCreateDTO dto);
|
||||
|
||||
/**
|
||||
* 获取当前用户的替换词文件列表
|
||||
*
|
||||
* @param params 分页参数
|
||||
* @return 分页数据
|
||||
*/
|
||||
PageData<CorrectWordFileVO> listFiles(Map<String, Object> params);
|
||||
|
||||
/**
|
||||
* 获取当前用户的替换词文件列表(不分页,用于下拉选择)
|
||||
*
|
||||
* @return 文件列表
|
||||
*/
|
||||
List<CorrectWordFileVO> listAllFiles();
|
||||
|
||||
/**
|
||||
* 获取文件原始内容(用于下载)
|
||||
*
|
||||
* @param fileId 文件ID
|
||||
* @return 文件实体
|
||||
*/
|
||||
CorrectWordFileVO getFileContent(String fileId);
|
||||
|
||||
/**
|
||||
* 删除替换词文件及其所有词条和关联记录
|
||||
*
|
||||
* @param fileId 文件ID
|
||||
*/
|
||||
void deleteFile(String fileId);
|
||||
|
||||
/**
|
||||
* 删除智能体关联的替换词文件关联记录(不删文件本身)
|
||||
*
|
||||
* @param agentId 智能体ID
|
||||
*/
|
||||
void deleteMappingsByAgentId(String agentId);
|
||||
|
||||
/**
|
||||
* 获取智能体的所有替换词条(精简版,供设备端使用)
|
||||
*
|
||||
* @param agentId 智能体ID
|
||||
* @return 替换词列表
|
||||
*/
|
||||
List<CorrectWordSimpleVO> getAllItemsByAgentId(String agentId);
|
||||
|
||||
/**
|
||||
* 获取智能体关联的替换词文件ID列表
|
||||
*
|
||||
* @param agentId 智能体ID
|
||||
* @return 文件ID列表
|
||||
*/
|
||||
List<String> getAgentCorrectWordFileIds(String agentId);
|
||||
|
||||
/**
|
||||
* 保存智能体关联的替换词文件(全量替换)
|
||||
*
|
||||
* @param agentId 智能体ID
|
||||
* @param fileIds 文件ID列表
|
||||
*/
|
||||
void saveAgentCorrectWords(String agentId, List<String> fileIds);
|
||||
|
||||
/**
|
||||
* 批量删除替换词文件
|
||||
*
|
||||
* @param fileIds 文件ID列表
|
||||
*/
|
||||
void batchDeleteFiles(List<String> fileIds);
|
||||
}
|
||||
+296
@@ -0,0 +1,296 @@
|
||||
package xiaozhi.modules.correctword.service.impl;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import xiaozhi.common.exception.ErrorCode;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||
import xiaozhi.common.utils.ConvertUtils;
|
||||
import xiaozhi.modules.agent.dao.AgentCorrectWordMappingDao;
|
||||
import xiaozhi.modules.correctword.dao.CorrectWordFileDao;
|
||||
import xiaozhi.modules.correctword.dao.CorrectWordItemDao;
|
||||
import xiaozhi.modules.correctword.dto.CorrectWordFileCreateDTO;
|
||||
import xiaozhi.modules.agent.entity.AgentCorrectWordMappingEntity;
|
||||
import xiaozhi.modules.correctword.entity.CorrectWordFileEntity;
|
||||
import xiaozhi.modules.correctword.entity.CorrectWordItemEntity;
|
||||
import xiaozhi.modules.correctword.service.CorrectWordFileService;
|
||||
import xiaozhi.modules.correctword.vo.CorrectWordFileVO;
|
||||
import xiaozhi.modules.correctword.vo.CorrectWordSimpleVO;
|
||||
import xiaozhi.modules.security.user.SecurityUser;
|
||||
|
||||
@Service
|
||||
@AllArgsConstructor
|
||||
public class CorrectWordFileServiceImpl extends BaseServiceImpl<CorrectWordFileDao, CorrectWordFileEntity>
|
||||
implements CorrectWordFileService {
|
||||
|
||||
private final CorrectWordFileDao correctWordFileDao;
|
||||
private final CorrectWordItemDao correctWordItemDao;
|
||||
private final AgentCorrectWordMappingDao agentCorrectWordMappingDao;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public CorrectWordFileVO createFile(CorrectWordFileCreateDTO dto) {
|
||||
// 校验文件大小不能超过1MB
|
||||
if (dto.getFileSize() != null && dto.getFileSize() > 1024 * 1024) {
|
||||
throw new RenException(ErrorCode.FILE_SIZE_OVER_LIMIT);
|
||||
}
|
||||
|
||||
// 校验文件名是否重复
|
||||
Long userId = SecurityUser.getUserId();
|
||||
LambdaQueryWrapper<CorrectWordFileEntity> nameWrapper = new LambdaQueryWrapper<>();
|
||||
nameWrapper.eq(CorrectWordFileEntity::getCreator, userId)
|
||||
.eq(CorrectWordFileEntity::getFileName, dto.getFileName());
|
||||
if (correctWordFileDao.selectCount(nameWrapper) > 0) {
|
||||
throw new RenException(ErrorCode.CORRECT_WORD_FILE_NAME_EXISTS);
|
||||
}
|
||||
|
||||
List<CorrectWordItemEntity> items = parseContent(dto.getContent());
|
||||
|
||||
// 保存文件记录
|
||||
CorrectWordFileEntity fileEntity = new CorrectWordFileEntity();
|
||||
fileEntity.setFileName(dto.getFileName());
|
||||
fileEntity.setWordCount(items.size());
|
||||
fileEntity.setContent(String.join("\n", dto.getContent()));
|
||||
fileEntity.setCreator(SecurityUser.getUserId());
|
||||
fileEntity.setCreatedAt(new Date());
|
||||
correctWordFileDao.insert(fileEntity);
|
||||
|
||||
// 设置fileId并批量保存词条
|
||||
String fileId = fileEntity.getId();
|
||||
for (CorrectWordItemEntity item : items) {
|
||||
item.setFileId(fileId);
|
||||
}
|
||||
if (!items.isEmpty()) {
|
||||
correctWordItemDao.batchInsert(items);
|
||||
}
|
||||
|
||||
return toVO(fileEntity);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void updateFile(String fileId, CorrectWordFileCreateDTO dto) {
|
||||
CorrectWordFileEntity fileEntity = correctWordFileDao.selectById(fileId);
|
||||
if (fileEntity == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 校验文件名是否重复(排除自身)
|
||||
Long userId = SecurityUser.getUserId();
|
||||
LambdaQueryWrapper<CorrectWordFileEntity> nameWrapper = new LambdaQueryWrapper<>();
|
||||
nameWrapper.eq(CorrectWordFileEntity::getCreator, userId)
|
||||
.eq(CorrectWordFileEntity::getFileName, dto.getFileName())
|
||||
.ne(CorrectWordFileEntity::getId, fileId);
|
||||
if (correctWordFileDao.selectCount(nameWrapper) > 0) {
|
||||
throw new RenException("文件名已存在:" + dto.getFileName());
|
||||
}
|
||||
|
||||
// 先删除旧词条
|
||||
LambdaQueryWrapper<CorrectWordItemEntity> deleteWrapper = new LambdaQueryWrapper<>();
|
||||
deleteWrapper.eq(CorrectWordItemEntity::getFileId, fileId);
|
||||
correctWordItemDao.delete(deleteWrapper);
|
||||
|
||||
// 解析新词条并批量保存
|
||||
List<CorrectWordItemEntity> items = parseContent(dto.getContent());
|
||||
if (!items.isEmpty()) {
|
||||
for (CorrectWordItemEntity item : items) {
|
||||
item.setFileId(fileId);
|
||||
}
|
||||
correctWordItemDao.batchInsert(items);
|
||||
}
|
||||
|
||||
// 更新文件记录
|
||||
fileEntity.setFileName(dto.getFileName());
|
||||
fileEntity.setWordCount(items.size());
|
||||
fileEntity.setContent(String.join("\n", dto.getContent()));
|
||||
fileEntity.setUpdater(SecurityUser.getUserId());
|
||||
fileEntity.setUpdatedAt(new Date());
|
||||
correctWordFileDao.updateById(fileEntity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageData<CorrectWordFileVO> listFiles(Map<String, Object> params) {
|
||||
Long userId = SecurityUser.getUserId();
|
||||
IPage<CorrectWordFileEntity> page = getPage(params, "created_at", false);
|
||||
LambdaQueryWrapper<CorrectWordFileEntity> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(CorrectWordFileEntity::getCreator, userId)
|
||||
.orderByDesc(CorrectWordFileEntity::getCreatedAt);
|
||||
correctWordFileDao.selectPage(page, wrapper);
|
||||
List<CorrectWordFileVO> voList = toVOList(page.getRecords());
|
||||
return new PageData<>(voList, page.getTotal());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CorrectWordFileVO> listAllFiles() {
|
||||
Long userId = SecurityUser.getUserId();
|
||||
LambdaQueryWrapper<CorrectWordFileEntity> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(CorrectWordFileEntity::getCreator, userId)
|
||||
.orderByDesc(CorrectWordFileEntity::getCreatedAt);
|
||||
List<CorrectWordFileEntity> entities = correctWordFileDao.selectList(wrapper);
|
||||
return toVOList(entities);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CorrectWordFileVO getFileContent(String fileId) {
|
||||
CorrectWordFileEntity entity = correctWordFileDao.selectById(fileId);
|
||||
return toVO(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void deleteFile(String fileId) {
|
||||
if (fileId == null || fileId.trim().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
// 先删除关联表记录
|
||||
agentCorrectWordMappingDao.deleteByFileId(fileId);
|
||||
// 删除词条
|
||||
LambdaQueryWrapper<CorrectWordItemEntity> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(CorrectWordItemEntity::getFileId, fileId);
|
||||
correctWordItemDao.delete(wrapper);
|
||||
// 删除文件
|
||||
correctWordFileDao.deleteById(fileId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void deleteMappingsByAgentId(String agentId) {
|
||||
agentCorrectWordMappingDao.deleteByAgentId(agentId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CorrectWordSimpleVO> getAllItemsByAgentId(String agentId) {
|
||||
// 通过关联表获取文件ID列表
|
||||
List<AgentCorrectWordMappingEntity> mappings = agentCorrectWordMappingDao.selectByAgentId(agentId);
|
||||
if (mappings == null || mappings.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
List<String> fileIds = mappings.stream()
|
||||
.map(AgentCorrectWordMappingEntity::getFileId)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 根据文件ID列表查询词条
|
||||
LambdaQueryWrapper<CorrectWordItemEntity> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.in(CorrectWordItemEntity::getFileId, fileIds);
|
||||
List<CorrectWordItemEntity> entities = correctWordItemDao.selectList(wrapper);
|
||||
return ConvertUtils.sourceToTarget(entities, CorrectWordSimpleVO.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getAgentCorrectWordFileIds(String agentId) {
|
||||
List<AgentCorrectWordMappingEntity> mappings = agentCorrectWordMappingDao.selectByAgentId(agentId);
|
||||
if (mappings == null || mappings.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
return mappings.stream()
|
||||
.map(AgentCorrectWordMappingEntity::getFileId)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void saveAgentCorrectWords(String agentId, List<String> fileIds) {
|
||||
// 先删除旧的关联记录
|
||||
agentCorrectWordMappingDao.deleteByAgentId(agentId);
|
||||
|
||||
if (fileIds == null || fileIds.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 批量插入新的关联记录
|
||||
Long userId = SecurityUser.getUserId();
|
||||
Date now = new Date();
|
||||
List<AgentCorrectWordMappingEntity> mappings = new ArrayList<>();
|
||||
for (String fileId : fileIds) {
|
||||
AgentCorrectWordMappingEntity mapping = new AgentCorrectWordMappingEntity();
|
||||
mapping.setAgentId(agentId);
|
||||
mapping.setFileId(fileId);
|
||||
mapping.setCreator(userId);
|
||||
mapping.setCreatedAt(now);
|
||||
mapping.setUpdater(userId);
|
||||
mapping.setUpdatedAt(now);
|
||||
mappings.add(mapping);
|
||||
}
|
||||
agentCorrectWordMappingDao.batchInsertMapping(mappings);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void batchDeleteFiles(List<String> fileIds) {
|
||||
if (fileIds == null || fileIds.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
for (String fileId : fileIds) {
|
||||
if (fileId == null || fileId.trim().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
deleteFile(fileId.trim());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析替换词内容,每条格式:原词|替换词
|
||||
*/
|
||||
private List<CorrectWordItemEntity> parseContent(List<String> lines) {
|
||||
List<CorrectWordItemEntity> items = new ArrayList<>();
|
||||
if (lines == null || lines.isEmpty()) {
|
||||
return items;
|
||||
}
|
||||
for (String line : lines) {
|
||||
line = line.trim();
|
||||
if (line.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
int idx = line.indexOf('|');
|
||||
if (idx <= 0 || idx >= line.length() - 1) {
|
||||
continue;
|
||||
}
|
||||
String sourceWord = line.substring(0, idx).trim();
|
||||
String targetWord = line.substring(idx + 1).trim();
|
||||
if (sourceWord.isEmpty() || targetWord.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
CorrectWordItemEntity item = new CorrectWordItemEntity();
|
||||
item.setSourceWord(sourceWord);
|
||||
item.setTargetWord(targetWord);
|
||||
items.add(item);
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
private CorrectWordFileVO toVO(CorrectWordFileEntity entity) {
|
||||
if (entity == null) {
|
||||
return null;
|
||||
}
|
||||
CorrectWordFileVO vo = new CorrectWordFileVO();
|
||||
vo.setId(entity.getId());
|
||||
vo.setFileName(entity.getFileName());
|
||||
vo.setWordCount(entity.getWordCount());
|
||||
vo.setContent(entity.getContent() != null
|
||||
? Arrays.asList(entity.getContent().split("\n"))
|
||||
: new ArrayList<>());
|
||||
vo.setCreatedAt(entity.getCreatedAt());
|
||||
vo.setUpdatedAt(entity.getUpdatedAt());
|
||||
return vo;
|
||||
}
|
||||
|
||||
private List<CorrectWordFileVO> toVOList(List<CorrectWordFileEntity> entities) {
|
||||
if (entities == null || entities.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
return entities.stream().map(this::toVO).collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package xiaozhi.modules.correctword.vo;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Schema(description = "替换词文件列表VO")
|
||||
public class CorrectWordFileVO {
|
||||
|
||||
@Schema(description = "替换词文件ID")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "原始文件名")
|
||||
private String fileName;
|
||||
|
||||
@Schema(description = "替换词数量")
|
||||
private Integer wordCount;
|
||||
|
||||
@Schema(description = "替换词内容,每行一条")
|
||||
private List<String> content;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
private Date createdAt;
|
||||
|
||||
@Schema(description = "更新时间")
|
||||
private Date updatedAt;
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package xiaozhi.modules.correctword.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Schema(description = "替换词精简VO(设备端使用)")
|
||||
public class CorrectWordSimpleVO {
|
||||
|
||||
@Schema(description = "原词")
|
||||
private String sourceWord;
|
||||
|
||||
@Schema(description = "替换词")
|
||||
private String targetWord;
|
||||
}
|
||||
+75
-30
@@ -4,6 +4,7 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
@@ -38,6 +39,8 @@ import cn.hutool.crypto.digest.DigestUtil;
|
||||
import cn.hutool.http.ContentType;
|
||||
import cn.hutool.http.Header;
|
||||
import cn.hutool.http.HttpRequest;
|
||||
import cn.hutool.json.JSONArray;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.AllArgsConstructor;
|
||||
@@ -700,40 +703,82 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
// 构建完整的URL
|
||||
String url = StrUtil.format("http://{}/api/commands/{}", mqttGatewayUrl, clientId);
|
||||
|
||||
// 构建请求体
|
||||
Map<String, Object> payload = MapUtil
|
||||
.builder(new HashMap<String, Object>())
|
||||
.put("jsonrpc", "2.0")
|
||||
.put("id", 2)
|
||||
.put("method", "tools/list")
|
||||
.put("params", MapUtil.builder(new HashMap<String, Object>())
|
||||
.put("withUserTools", true)
|
||||
.build())
|
||||
.build();
|
||||
// 存储所有工具列表
|
||||
List<Object> allTools = new ArrayList<>();
|
||||
String cursor = null;
|
||||
|
||||
Map<String, Object> requestBody = MapUtil
|
||||
.builder(new HashMap<String, Object>())
|
||||
.put("type", "mcp")
|
||||
.put("payload", payload)
|
||||
.build();
|
||||
|
||||
// 发送请求
|
||||
String resultMessage = HttpRequest.post(url)
|
||||
.header(Header.CONTENT_TYPE, ContentType.JSON.getValue())
|
||||
.header(Header.AUTHORIZATION, "Bearer " + generateBearerToken())
|
||||
.body(JSONUtil.toJsonStr(requestBody))
|
||||
.timeout(10000) // 超时,毫秒
|
||||
.execute().body();
|
||||
|
||||
// 解析响应
|
||||
if (StringUtils.isNotBlank(resultMessage)) {
|
||||
cn.hutool.json.JSONObject jsonObject = JSONUtil.parseObj(resultMessage);
|
||||
if (jsonObject.getBool("success", false)) {
|
||||
return jsonObject.get("data");
|
||||
// 循环获取分页数据
|
||||
while (true) {
|
||||
// 构建params
|
||||
Map<String, Object> paramsMap = MapUtil.builder(new HashMap<String, Object>())
|
||||
.put("withUserTools", true)
|
||||
.build();
|
||||
// 如果有cursor,添加到请求参数中
|
||||
if (StringUtils.isNotBlank(cursor)) {
|
||||
paramsMap.put("cursor", cursor);
|
||||
}
|
||||
|
||||
// 构建请求体
|
||||
Map<String, Object> payload = MapUtil
|
||||
.builder(new HashMap<String, Object>())
|
||||
.put("jsonrpc", "2.0")
|
||||
.put("id", 2)
|
||||
.put("method", "tools/list")
|
||||
.put("params", paramsMap)
|
||||
.build();
|
||||
|
||||
Map<String, Object> requestBody = MapUtil
|
||||
.builder(new HashMap<String, Object>())
|
||||
.put("type", "mcp")
|
||||
.put("payload", payload)
|
||||
.build();
|
||||
|
||||
// 发送请求
|
||||
String resultMessage = HttpRequest.post(url)
|
||||
.header(Header.CONTENT_TYPE, ContentType.JSON.getValue())
|
||||
.header(Header.AUTHORIZATION, "Bearer " + generateBearerToken())
|
||||
.body(JSONUtil.toJsonStr(requestBody))
|
||||
.timeout(10000) // 超时,毫秒
|
||||
.execute().body();
|
||||
|
||||
// 解析响应
|
||||
if (StringUtils.isBlank(resultMessage)) {
|
||||
break;
|
||||
}
|
||||
|
||||
JSONObject jsonObject = JSONUtil.parseObj(resultMessage);
|
||||
if (!jsonObject.getBool("success", false)) {
|
||||
break;
|
||||
}
|
||||
|
||||
JSONObject data = jsonObject.getJSONObject("data");
|
||||
if (data == null) {
|
||||
break;
|
||||
}
|
||||
|
||||
// 获取当前页的工具列表
|
||||
JSONArray tools = data.getJSONArray("tools");
|
||||
if (tools != null && !tools.isEmpty()) {
|
||||
allTools.addAll(tools);
|
||||
}
|
||||
|
||||
// 获取下一页的cursor
|
||||
String nextCursor = data.getStr("nextCursor");
|
||||
if (StringUtils.isBlank(nextCursor)) {
|
||||
// 没有下一页了
|
||||
break;
|
||||
}
|
||||
cursor = nextCursor;
|
||||
}
|
||||
|
||||
return null;
|
||||
// 构建返回结果
|
||||
if (allTools.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Map<String, Object> resultData = new HashMap<>();
|
||||
resultData.put("tools", allTools);
|
||||
return resultData;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+1
-1
@@ -66,7 +66,7 @@ public class OtaServiceImpl extends BaseServiceImpl<OtaDao, OtaEntity> implement
|
||||
// 同类固件只保留最新的一条
|
||||
List<OtaEntity> otaList = baseDao.selectList(queryWrapper);
|
||||
if (otaList != null && otaList.size() > 0) {
|
||||
OtaEntity otaBefore = otaList.getFirst();
|
||||
OtaEntity otaBefore = otaList.get(0);
|
||||
entity.setId(otaBefore.getId());
|
||||
baseDao.updateById(entity);
|
||||
return true;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package xiaozhi.modules.knowledge.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
/**
|
||||
* 知识库模块定时任务配置
|
||||
* 启用 Spring Schedule 能力
|
||||
*/
|
||||
@Configuration
|
||||
@EnableScheduling
|
||||
public class RAGTaskConfig {
|
||||
}
|
||||
+11
-5
@@ -26,6 +26,7 @@ import xiaozhi.common.utils.Result;
|
||||
import xiaozhi.common.utils.ToolUtil;
|
||||
import xiaozhi.modules.knowledge.dto.KnowledgeBaseDTO;
|
||||
import xiaozhi.modules.knowledge.service.KnowledgeBaseService;
|
||||
import xiaozhi.modules.knowledge.service.KnowledgeManagerService;
|
||||
import xiaozhi.modules.model.entity.ModelConfigEntity;
|
||||
import xiaozhi.modules.security.user.SecurityUser;
|
||||
|
||||
@@ -36,6 +37,7 @@ import xiaozhi.modules.security.user.SecurityUser;
|
||||
public class KnowledgeBaseController {
|
||||
|
||||
private final KnowledgeBaseService knowledgeBaseService;
|
||||
private final KnowledgeManagerService knowledgeManagerService;
|
||||
|
||||
@GetMapping
|
||||
@Operation(summary = "分页查询知识库列表")
|
||||
@@ -96,6 +98,8 @@ public class KnowledgeBaseController {
|
||||
throw new RenException(ErrorCode.NO_PERMISSION);
|
||||
}
|
||||
|
||||
// [FIX] 注入 ID,防止 Service 层找不到记录
|
||||
knowledgeBaseDTO.setId(existingKnowledgeBase.getId());
|
||||
knowledgeBaseDTO.setDatasetId(datasetId);
|
||||
KnowledgeBaseDTO resp = knowledgeBaseService.update(knowledgeBaseDTO);
|
||||
return new Result<KnowledgeBaseDTO>().ok(resp);
|
||||
@@ -117,7 +121,8 @@ public class KnowledgeBaseController {
|
||||
throw new RenException(ErrorCode.NO_PERMISSION);
|
||||
}
|
||||
|
||||
knowledgeBaseService.deleteByDatasetId(datasetId);
|
||||
// [Architecture Fix] 通过编排层级联删除,防止孤儿数据并解决循环依赖
|
||||
knowledgeManagerService.deleteDatasetWithFiles(datasetId);
|
||||
return new Result<>();
|
||||
}
|
||||
|
||||
@@ -133,15 +138,16 @@ public class KnowledgeBaseController {
|
||||
// 获取当前登录用户ID
|
||||
Long currentUserId = SecurityUser.getUserId();
|
||||
List<String> idList = Arrays.asList(ids.split(","));
|
||||
List<KnowledgeBaseDTO> knowledgeBaseDTOs = Optional.ofNullable(knowledgeBaseService.getByDatasetIdList(idList)).orElseGet(ArrayList::new);
|
||||
List<KnowledgeBaseDTO> knowledgeBaseDTOs = Optional.ofNullable(knowledgeBaseService.getByDatasetIdList(idList))
|
||||
.orElseGet(ArrayList::new);
|
||||
if (ToolUtil.isNotEmpty(knowledgeBaseDTOs)) {
|
||||
knowledgeBaseDTOs.forEach(item->{
|
||||
knowledgeBaseDTOs.forEach(item -> {
|
||||
// 检查权限:用户只能删除自己创建的知识库
|
||||
if (item.getCreator() == null || !item.getCreator().equals(currentUserId)) {
|
||||
throw new RenException(ErrorCode.NO_PERMISSION);
|
||||
}
|
||||
//删除
|
||||
knowledgeBaseService.deleteByDatasetId(item.getDatasetId());
|
||||
// [Architecture Fix] 通过编排层级联删除
|
||||
knowledgeManagerService.deleteDatasetWithFiles(item.getDatasetId());
|
||||
});
|
||||
}
|
||||
return new Result<>();
|
||||
|
||||
+59
-54
@@ -4,6 +4,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.springdoc.core.annotations.ParameterObject;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
@@ -11,7 +12,6 @@ import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.AllArgsConstructor;
|
||||
import xiaozhi.common.exception.ErrorCode;
|
||||
@@ -20,6 +20,9 @@ import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.utils.Result;
|
||||
import xiaozhi.modules.knowledge.dto.KnowledgeBaseDTO;
|
||||
import xiaozhi.modules.knowledge.dto.KnowledgeFilesDTO;
|
||||
import xiaozhi.modules.knowledge.dto.document.ChunkDTO;
|
||||
import xiaozhi.modules.knowledge.dto.document.DocumentDTO;
|
||||
import xiaozhi.modules.knowledge.dto.document.RetrievalDTO;
|
||||
import xiaozhi.modules.knowledge.service.KnowledgeBaseService;
|
||||
import xiaozhi.modules.knowledge.service.KnowledgeFilesService;
|
||||
import xiaozhi.modules.security.user.SecurityUser;
|
||||
@@ -57,13 +60,13 @@ public class KnowledgeFilesController {
|
||||
public Result<PageData<KnowledgeFilesDTO>> getPageList(
|
||||
@PathVariable("dataset_id") String datasetId,
|
||||
@RequestParam(required = false) String name,
|
||||
@RequestParam(required = false) Integer status,
|
||||
@RequestParam(required = false) String status,
|
||||
@RequestParam(required = false, defaultValue = "1") Integer page,
|
||||
@RequestParam(required = false, defaultValue = "10") Integer page_size) {
|
||||
// 验证知识库权限
|
||||
validateKnowledgeBasePermission(datasetId);
|
||||
|
||||
//组装参数
|
||||
// 组装参数
|
||||
KnowledgeFilesDTO knowledgeFilesDTO = new KnowledgeFilesDTO();
|
||||
knowledgeFilesDTO.setDatasetId(datasetId);
|
||||
knowledgeFilesDTO.setName(name);
|
||||
@@ -77,12 +80,12 @@ public class KnowledgeFilesController {
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<PageData<KnowledgeFilesDTO>> getPageListByStatus(
|
||||
@PathVariable("dataset_id") String datasetId,
|
||||
@PathVariable("status") Integer status,
|
||||
@PathVariable("status") String status,
|
||||
@RequestParam(required = false, defaultValue = "1") Integer page,
|
||||
@RequestParam(required = false, defaultValue = "10") Integer page_size) {
|
||||
// 验证知识库权限
|
||||
validateKnowledgeBasePermission(datasetId);
|
||||
//组装参数
|
||||
// 组装参数
|
||||
KnowledgeFilesDTO knowledgeFilesDTO = new KnowledgeFilesDTO();
|
||||
knowledgeFilesDTO.setDatasetId(datasetId);
|
||||
knowledgeFilesDTO.setStatus(status);
|
||||
@@ -111,16 +114,29 @@ public class KnowledgeFilesController {
|
||||
return new Result<KnowledgeFilesDTO>().ok(resp);
|
||||
}
|
||||
|
||||
@DeleteMapping("/documents/{document_id}")
|
||||
@Operation(summary = "删除单个文档")
|
||||
@Parameter(name = "document_id", description = "文档ID", required = true)
|
||||
@DeleteMapping("/documents")
|
||||
@Operation(summary = "批量删除文档")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<Void> delete(@PathVariable("dataset_id") String datasetId,
|
||||
@RequestBody DocumentDTO.BatchIdReq req) {
|
||||
// 验证知识库权限
|
||||
validateKnowledgeBasePermission(datasetId);
|
||||
|
||||
knowledgeFilesService.deleteDocuments(datasetId, req);
|
||||
return new Result<>();
|
||||
}
|
||||
|
||||
@DeleteMapping("/documents/{document_id}")
|
||||
@Operation(summary = "删除单个文档")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<Void> deleteSingle(@PathVariable("dataset_id") String datasetId,
|
||||
@PathVariable("document_id") String documentId) {
|
||||
// 验证知识库权限
|
||||
validateKnowledgeBasePermission(datasetId);
|
||||
|
||||
knowledgeFilesService.deleteByDocumentId(documentId, datasetId);
|
||||
DocumentDTO.BatchIdReq req = new DocumentDTO.BatchIdReq();
|
||||
req.setIds(java.util.Collections.singletonList(documentId));
|
||||
knowledgeFilesService.deleteDocuments(datasetId, req);
|
||||
return new Result<>();
|
||||
}
|
||||
|
||||
@@ -148,64 +164,53 @@ public class KnowledgeFilesController {
|
||||
@GetMapping("/documents/{document_id}/chunks")
|
||||
@Operation(summary = "列出指定文档的切片")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<Map<String, Object>> listChunks(@PathVariable("dataset_id") String datasetId,
|
||||
public Result<ChunkDTO.ListVO> listChunks(
|
||||
@PathVariable("dataset_id") String datasetId,
|
||||
@PathVariable("document_id") String documentId,
|
||||
@RequestParam(required = false) String keywords,
|
||||
@RequestParam(required = false, defaultValue = "1") Integer page,
|
||||
@RequestParam(required = false, defaultValue = "1024") Integer page_size,
|
||||
@RequestParam(required = false) String id) {
|
||||
// 验证知识库权限
|
||||
@ParameterObject ChunkDTO.ListReq req) {
|
||||
|
||||
// 验证权限 (内部已包含知识库存在性校验与归属权校验)
|
||||
validateKnowledgeBasePermission(datasetId);
|
||||
|
||||
Map<String, Object> result = knowledgeFilesService.listChunks(datasetId, documentId, keywords, page, page_size, id);
|
||||
return new Result<Map<String, Object>>().ok(result);
|
||||
// 设置默认值
|
||||
if (req.getPage() == null)
|
||||
req.setPage(1);
|
||||
if (req.getPageSize() == null)
|
||||
req.setPageSize(50);
|
||||
|
||||
// 调用服务层获取强类型切片列表
|
||||
ChunkDTO.ListVO result = knowledgeFilesService.listChunks(datasetId, documentId, req);
|
||||
return new Result<ChunkDTO.ListVO>().ok(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 召回测试
|
||||
*/
|
||||
@PostMapping("/retrieval-test")
|
||||
@Operation(summary = "召回测试")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<Map<String, Object>> retrievalTest(@PathVariable("dataset_id") String datasetId,
|
||||
@RequestBody Map<String, Object> params) {
|
||||
public Result<RetrievalDTO.ResultVO> retrievalTest(
|
||||
@PathVariable("dataset_id") String datasetId,
|
||||
@RequestBody RetrievalDTO.TestReq req) {
|
||||
|
||||
// 验证知识库权限
|
||||
validateKnowledgeBasePermission(datasetId);
|
||||
|
||||
try {
|
||||
// 提取参数
|
||||
String question = (String) params.get("question");
|
||||
if (question == null || question.trim().isEmpty()) {
|
||||
return new Result<Map<String, Object>>().error("问题不能为空");
|
||||
}
|
||||
|
||||
List<String> datasetIds = (List<String>) params.get("dataset_ids");
|
||||
List<String> documentIds = (List<String>) params.get("document_ids");
|
||||
Integer page = (Integer) params.get("page");
|
||||
Integer pageSize = (Integer) params.get("page_size");
|
||||
Float similarityThreshold = (Float) params.get("similarity_threshold");
|
||||
Float vectorSimilarityWeight = (Float) params.get("vector_similarity_weight");
|
||||
Integer topK = (Integer) params.get("top_k");
|
||||
String rerankId = (String) params.get("rerank_id");
|
||||
Boolean keyword = (Boolean) params.get("keyword");
|
||||
Boolean highlight = (Boolean) params.get("highlight");
|
||||
List<String> crossLanguages = (List<String>) params.get("cross_languages");
|
||||
Map<String, Object> metadataCondition = (Map<String, Object>) params.get("metadata_condition");
|
||||
|
||||
// 如果未指定数据集ID,使用当前数据集
|
||||
if (datasetIds == null || datasetIds.isEmpty()) {
|
||||
datasetIds = java.util.Arrays.asList(datasetId);
|
||||
}
|
||||
|
||||
Map<String, Object> result = knowledgeFilesService.retrievalTest(
|
||||
question, datasetIds, documentIds, page, pageSize, similarityThreshold,
|
||||
vectorSimilarityWeight, topK, rerankId, keyword, highlight, crossLanguages, metadataCondition);
|
||||
|
||||
return new Result<Map<String, Object>>().ok(result);
|
||||
} catch (Exception e) {
|
||||
return new Result<Map<String, Object>>().error("召回测试失败: " + e.getMessage());
|
||||
// 业务下沉逻辑:如果未指定知识库ID,则设为当前路径中的 datasetId
|
||||
if (req.getDatasetIds() == null || req.getDatasetIds().isEmpty()) {
|
||||
req.setDatasetIds(java.util.Arrays.asList(datasetId));
|
||||
}
|
||||
|
||||
// [Reinforce] 强管控分页参数,防止 RAGFlow 端出现 Negative Slicing 报错
|
||||
if (req.getPage() == null || req.getPage() < 1) {
|
||||
req.setPage(1);
|
||||
}
|
||||
if (req.getPageSize() == null || req.getPageSize() < 1) {
|
||||
req.setPageSize(100);
|
||||
}
|
||||
|
||||
// 调用检索服务,返回强类型聚合对象
|
||||
RetrievalDTO.ResultVO result = knowledgeFilesService.retrievalTest(req);
|
||||
return new Result<RetrievalDTO.ResultVO>().ok(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析JSON字符串为Map对象
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package xiaozhi.modules.knowledge.dao;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import xiaozhi.common.dao.BaseDao;
|
||||
import xiaozhi.modules.knowledge.entity.DocumentEntity;
|
||||
|
||||
/**
|
||||
* 文档 DAO
|
||||
*/
|
||||
@Mapper
|
||||
public interface DocumentDao extends BaseDao<DocumentEntity> {
|
||||
}
|
||||
@@ -19,4 +19,17 @@ public interface KnowledgeBaseDao extends BaseDao<KnowledgeBaseEntity> {
|
||||
*/
|
||||
void deletePluginMappingByKnowledgeBaseId(@Param("knowledgeBaseId") String knowledgeBaseId);
|
||||
|
||||
/**
|
||||
* 通用维度原子更新知识库统计信息
|
||||
*
|
||||
* @param datasetId 数据集ID
|
||||
* @param docDelta 文档数增量
|
||||
* @param chunkDelta 分块数增量
|
||||
* @param tokenDelta Token数增量
|
||||
*/
|
||||
void updateStatsAfterChange(@Param("datasetId") String datasetId,
|
||||
@Param("docDelta") Integer docDelta,
|
||||
@Param("chunkDelta") Long chunkDelta,
|
||||
@Param("tokenDelta") Long tokenDelta);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package xiaozhi.modules.knowledge.dto;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 文档 DTO
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "知识库文档")
|
||||
public class DocumentDTO implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "本地ID")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "知识库ID")
|
||||
private String datasetId;
|
||||
|
||||
@Schema(description = "RAGFlow文档ID")
|
||||
private String documentId;
|
||||
|
||||
@Schema(description = "文档名称")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "文件大小")
|
||||
private Long size;
|
||||
|
||||
@Schema(description = "文件类型")
|
||||
private String type;
|
||||
|
||||
@Schema(description = "分块方法")
|
||||
private String chunkMethod;
|
||||
|
||||
@Schema(description = "解析配置")
|
||||
private Map<String, Object> parserConfig;
|
||||
|
||||
@Schema(description = "处理状态 (1:解析中 3:成功 4:失败)")
|
||||
private Integer status;
|
||||
|
||||
@Schema(description = "错误信息")
|
||||
private String error;
|
||||
|
||||
@Schema(description = "分块数量")
|
||||
private Integer chunkCount;
|
||||
|
||||
@Schema(description = "Token数量")
|
||||
private Long tokenCount;
|
||||
|
||||
@Schema(description = "是否启用")
|
||||
private Integer enabled;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
private Date createdAt;
|
||||
|
||||
@Schema(description = "更新时间")
|
||||
private Date updatedAt;
|
||||
|
||||
@Schema(description = "上传进度 (虚拟字段)")
|
||||
private Double progress;
|
||||
|
||||
@Schema(description = "缩略图/预览图 (虚拟字段)")
|
||||
private String thumbnail;
|
||||
}
|
||||
@@ -26,9 +26,30 @@ public class KnowledgeBaseDTO implements Serializable {
|
||||
@Schema(description = "知识库名称")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "知识库头像(Base64)")
|
||||
private String avatar;
|
||||
|
||||
@Schema(description = "知识库描述")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "嵌入模型名称")
|
||||
private String embeddingModel;
|
||||
|
||||
@Schema(description = "权限设置: me/team")
|
||||
private String permission;
|
||||
|
||||
@Schema(description = "分块方法")
|
||||
private String chunkMethod;
|
||||
|
||||
@Schema(description = "解析器配置(JSON String)")
|
||||
private String parserConfig;
|
||||
|
||||
@Schema(description = "分块总数")
|
||||
private Long chunkCount;
|
||||
|
||||
@Schema(description = "总Token数")
|
||||
private Long tokenNum;
|
||||
|
||||
@Schema(description = "状态(0:禁用 1:启用)")
|
||||
private Integer status;
|
||||
|
||||
|
||||
+27
-4
@@ -6,10 +6,12 @@ import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Schema(description = "知识库文档")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class KnowledgeFilesDTO implements Serializable {
|
||||
|
||||
@Serial
|
||||
@@ -35,7 +37,19 @@ public class KnowledgeFilesDTO implements Serializable {
|
||||
@Schema(description = "文件路径")
|
||||
private String filePath;
|
||||
|
||||
@Schema(description = "元数据字段")
|
||||
@Schema(description = "解析进度 (0.0 ~ 1.0)")
|
||||
private Double progress;
|
||||
|
||||
@Schema(description = "缩略图 (Base64 或 URL)")
|
||||
private String thumbnail;
|
||||
|
||||
@Schema(description = "解析耗时 (单位: 秒)")
|
||||
private Double processDuration;
|
||||
|
||||
@Schema(description = "来源类型 (local, s3, url 等)")
|
||||
private String sourceType;
|
||||
|
||||
@Schema(description = "元数据字段 (Map 格式)")
|
||||
private Map<String, Object> metaFields;
|
||||
|
||||
@Schema(description = "分块方法")
|
||||
@@ -44,10 +58,10 @@ public class KnowledgeFilesDTO implements Serializable {
|
||||
@Schema(description = "解析器配置")
|
||||
private Map<String, Object> parserConfig;
|
||||
|
||||
@Schema(description = "状态")
|
||||
private Integer status;
|
||||
@Schema(description = "可用状态 (1: 启用/正常, 0: 禁用/失效)")
|
||||
private String status;
|
||||
|
||||
@Schema(description = "文档解析状态")
|
||||
@Schema(description = "运行状态 (UNSTART/RUNNING/CANCEL/DONE/FAIL)")
|
||||
private String run;
|
||||
|
||||
@Schema(description = "创建者")
|
||||
@@ -62,6 +76,15 @@ public class KnowledgeFilesDTO implements Serializable {
|
||||
@Schema(description = "更新时间")
|
||||
private Date updatedAt;
|
||||
|
||||
@Schema(description = "分块数量")
|
||||
private Integer chunkCount;
|
||||
|
||||
@Schema(description = "Token数量")
|
||||
private Long tokenCount;
|
||||
|
||||
@Schema(description = "解析错误信息")
|
||||
private String error;
|
||||
|
||||
// 文档解析状态常量定义
|
||||
private static final Integer STATUS_UNSTART = 0;
|
||||
private static final Integer STATUS_RUNNING = 1;
|
||||
|
||||
@@ -0,0 +1,421 @@
|
||||
package xiaozhi.modules.knowledge.dto.agent;
|
||||
|
||||
import lombok.*;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import jakarta.validation.constraints.*;
|
||||
|
||||
@Schema(description = "智能体 (Agent) 管理聚合 DTO")
|
||||
public class AgentDTO {
|
||||
|
||||
// ========== 1. Agent 管理 (CRUD) - 对应 RAGFlow_Agent接口详解 ==========
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "Agent 创建请求")
|
||||
public static class CreateReq implements Serializable {
|
||||
@Schema(description = "Agent 标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "My Agent")
|
||||
@NotBlank(message = "Agent 标题不能为空")
|
||||
@JsonProperty("title")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "DSL 定义 (画布 JSON)", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotNull(message = "DSL 定义不能为空")
|
||||
@JsonProperty("dsl")
|
||||
private Map<String, Object> dsl;
|
||||
|
||||
@Schema(description = "描述", example = "这是一个测试 Agent")
|
||||
@JsonProperty("description")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "头像 URL", example = "http://example.com/avatar.png")
|
||||
@JsonProperty("avatar")
|
||||
private String avatar;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "Agent 更新请求")
|
||||
public static class UpdateReq implements Serializable {
|
||||
@Schema(description = "Agent 标题", example = "Updated Agent")
|
||||
@JsonProperty("title")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "DSL 定义 (画布 JSON)")
|
||||
@JsonProperty("dsl")
|
||||
private Map<String, Object> dsl;
|
||||
|
||||
@Schema(description = "描述")
|
||||
@JsonProperty("description")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "头像 URL")
|
||||
@JsonProperty("avatar")
|
||||
private String avatar;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "Agent 列表请求")
|
||||
public static class ListReq implements Serializable {
|
||||
@Schema(description = "页码", defaultValue = "1")
|
||||
@JsonProperty("page")
|
||||
@Builder.Default
|
||||
private Integer page = 1;
|
||||
|
||||
@Schema(description = "每页大小", defaultValue = "10")
|
||||
@JsonProperty("page_size")
|
||||
@Builder.Default
|
||||
private Integer pageSize = 10;
|
||||
|
||||
@Schema(description = "排序字段", defaultValue = "update_time")
|
||||
@JsonProperty("orderby")
|
||||
@Builder.Default
|
||||
private String orderby = "update_time";
|
||||
|
||||
@Schema(description = "是否降序", defaultValue = "true")
|
||||
@JsonProperty("desc")
|
||||
@Builder.Default
|
||||
private Boolean desc = true;
|
||||
|
||||
@Schema(description = "Agent ID 过滤")
|
||||
@JsonProperty("id")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "标题模糊搜索")
|
||||
@JsonProperty("title")
|
||||
private String title;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "Agent 响应对象")
|
||||
public static class AgentVO implements Serializable {
|
||||
@Schema(description = "Agent ID")
|
||||
@JsonProperty("id")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "标题")
|
||||
@JsonProperty("title")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "描述")
|
||||
@JsonProperty("description")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "头像")
|
||||
@JsonProperty("avatar")
|
||||
private String avatar;
|
||||
|
||||
@Schema(description = "DSL 定义")
|
||||
@JsonProperty("dsl")
|
||||
private Map<String, Object> dsl;
|
||||
|
||||
@Schema(description = "创建者 ID")
|
||||
@JsonProperty("user_id")
|
||||
private String userId;
|
||||
|
||||
@Schema(description = "画布分类")
|
||||
@JsonProperty("canvas_category")
|
||||
private String canvasCategory;
|
||||
|
||||
@Schema(description = "创建时间 (时间戳)")
|
||||
@JsonProperty("create_time")
|
||||
private Long createTime;
|
||||
|
||||
@Schema(description = "更新时间 (时间戳)")
|
||||
@JsonProperty("update_time")
|
||||
private Long updateTime;
|
||||
}
|
||||
|
||||
// ========== 2. Webhook 调试与追踪 - 对应 RAGFlow_Agent接口详解 ==========
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "Webhook 触发请求 (参数动态)")
|
||||
public static class WebhookTriggerReq implements Serializable {
|
||||
@Schema(description = "输入变量", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotNull(message = "输入变量不能为空")
|
||||
@JsonProperty("inputs")
|
||||
private Map<String, Object> inputs;
|
||||
|
||||
@Schema(description = "查询词", example = "Hello")
|
||||
@JsonProperty("query")
|
||||
private String query;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "Webhook 追踪请求")
|
||||
public static class WebhookTraceReq implements Serializable {
|
||||
@Schema(description = "时间戳游标", example = "1700000000.0")
|
||||
@JsonProperty("since_ts")
|
||||
private Double sinceTs;
|
||||
|
||||
@Schema(description = "Webhook ID")
|
||||
@JsonProperty("webhook_id")
|
||||
private String webhookId;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "Webhook 追踪响应")
|
||||
public static class WebhookTraceVO implements Serializable {
|
||||
@Schema(description = "Webhook ID")
|
||||
@JsonProperty("webhook_id")
|
||||
private String webhookId;
|
||||
|
||||
@Schema(description = "是否结束")
|
||||
@JsonProperty("finished")
|
||||
private Boolean finished;
|
||||
|
||||
@Schema(description = "下一次查询的时间戳游标")
|
||||
@JsonProperty("next_since_ts")
|
||||
private Double nextSinceTs;
|
||||
|
||||
@Schema(description = "事件列表")
|
||||
@JsonProperty("events")
|
||||
private List<TraceEvent> events;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "追踪事件项")
|
||||
public static class TraceEvent implements Serializable {
|
||||
@Schema(description = "时间戳")
|
||||
@JsonProperty("ts")
|
||||
private Double ts;
|
||||
|
||||
@Schema(description = "事件类型")
|
||||
@JsonProperty("event")
|
||||
private String event;
|
||||
|
||||
@Schema(description = "事件数据")
|
||||
@JsonProperty("data")
|
||||
private Object data;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 3. Agent 会话 (Session) - 对应 RAGFlow_Agent_Dify接口详解 ==========
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "Session 创建请求")
|
||||
public static class SessionCreateReq implements Serializable {
|
||||
@Schema(description = "用户 ID")
|
||||
@JsonProperty("user_id")
|
||||
private String userId;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "Session 列表请求")
|
||||
public static class SessionListReq implements Serializable {
|
||||
@Schema(description = "页码", defaultValue = "1")
|
||||
@JsonProperty("page")
|
||||
@Builder.Default
|
||||
private Integer page = 1;
|
||||
|
||||
@Schema(description = "每页大小", defaultValue = "10")
|
||||
@JsonProperty("page_size")
|
||||
@Builder.Default
|
||||
private Integer pageSize = 10;
|
||||
|
||||
@Schema(description = "排序字段", defaultValue = "create_time")
|
||||
@JsonProperty("orderby")
|
||||
@Builder.Default
|
||||
private String orderby = "create_time";
|
||||
|
||||
@Schema(description = "是否降序", defaultValue = "true")
|
||||
@JsonProperty("desc")
|
||||
@Builder.Default
|
||||
private Boolean desc = true;
|
||||
|
||||
@Schema(description = "Session ID")
|
||||
@JsonProperty("id")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "用户 ID")
|
||||
@JsonProperty("user_id")
|
||||
private String userId;
|
||||
|
||||
@Schema(description = "是否返回 DSL")
|
||||
@JsonProperty("dsl")
|
||||
@Builder.Default
|
||||
private Boolean dsl = false;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "Session 批量删除请求")
|
||||
public static class SessionBatchDeleteReq implements Serializable {
|
||||
@Schema(description = "会话 ID 列表", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@JsonProperty("ids")
|
||||
@NotEmpty(message = "ID列表不能为空")
|
||||
private List<String> ids;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "Session 响应对象")
|
||||
public static class SessionVO implements Serializable {
|
||||
@Schema(description = "Session ID")
|
||||
@JsonProperty("id")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "Agent ID")
|
||||
@JsonProperty("agent_id")
|
||||
private String agentId;
|
||||
|
||||
@Schema(description = "用户 ID")
|
||||
@JsonProperty("user_id")
|
||||
private String userId;
|
||||
|
||||
@Schema(description = "来源")
|
||||
@JsonProperty("source")
|
||||
private String source;
|
||||
|
||||
@Schema(description = "DSL 定义")
|
||||
@JsonProperty("dsl")
|
||||
private Map<String, Object> dsl;
|
||||
|
||||
@Schema(description = "消息列表")
|
||||
@JsonProperty("messages")
|
||||
private List<Map<String, Object>> messages;
|
||||
}
|
||||
|
||||
// ========== 4. Agent 对话 (Completion) - 对应 RAGFlow_Agent_Dify接口详解 ==========
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "Completion 对话请求")
|
||||
public static class CompletionReq implements Serializable {
|
||||
@Schema(description = "会话 ID", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotBlank(message = "会话 ID 不能为空")
|
||||
@JsonProperty("session_id")
|
||||
private String sessionId;
|
||||
|
||||
@Schema(description = "用户问题")
|
||||
@JsonProperty("question")
|
||||
private String question;
|
||||
|
||||
@Schema(description = "是否流式返回", defaultValue = "true")
|
||||
@JsonProperty("stream")
|
||||
@Builder.Default
|
||||
private Boolean stream = true;
|
||||
|
||||
@Schema(description = "是否返回追踪信息", defaultValue = "false")
|
||||
@JsonProperty("return_trace")
|
||||
@Builder.Default
|
||||
private Boolean returnTrace = false;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "Completion 对话响应")
|
||||
public static class CompletionVO implements Serializable {
|
||||
@Schema(description = "会话 ID")
|
||||
@JsonProperty("id")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "回复内容")
|
||||
@JsonProperty("content")
|
||||
private String content;
|
||||
|
||||
@Schema(description = "引用来源")
|
||||
@JsonProperty("reference")
|
||||
private Map<String, Object> reference;
|
||||
|
||||
@Schema(description = "追踪信息")
|
||||
@JsonProperty("trace")
|
||||
private List<Object> trace;
|
||||
}
|
||||
|
||||
// ========== 5. Dify 兼容检索 - 对应 RAGFlow_Agent_Dify接口详解 ==========
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "Dify 兼容检索请求")
|
||||
public static class DifyRetrievalReq implements Serializable {
|
||||
@Schema(description = "知识库 ID")
|
||||
@JsonProperty("knowledge_id")
|
||||
private String knowledgeId;
|
||||
|
||||
@Schema(description = "查询词")
|
||||
@JsonProperty("query")
|
||||
private String query;
|
||||
|
||||
@Schema(description = "检索设置")
|
||||
@JsonProperty("retrieval_setting")
|
||||
private Map<String, Object> retrievalSetting;
|
||||
|
||||
@Schema(description = "元数据过滤条件")
|
||||
@JsonProperty("metadata_condition")
|
||||
private Map<String, Object> metadataCondition;
|
||||
|
||||
@Schema(description = "是否使用知识图谱")
|
||||
@JsonProperty("use_kg")
|
||||
private Boolean useKg;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "Dify 兼容检索响应")
|
||||
public static class DifyRetrievalVO implements Serializable {
|
||||
@Schema(description = "检索结果列表")
|
||||
@JsonProperty("records")
|
||||
private List<Record> records;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "检索记录")
|
||||
public static class Record implements Serializable {
|
||||
@Schema(description = "内容")
|
||||
@JsonProperty("content")
|
||||
private String content;
|
||||
|
||||
@Schema(description = "相似度分数")
|
||||
@JsonProperty("score")
|
||||
private Double score;
|
||||
|
||||
@Schema(description = "标题")
|
||||
@JsonProperty("title")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "元数据")
|
||||
@JsonProperty("metadata")
|
||||
private Map<String, Object> metadata;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package xiaozhi.modules.knowledge.dto.bot;
|
||||
|
||||
import lombok.*;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import jakarta.validation.constraints.*;
|
||||
|
||||
@Schema(description = "外部机器人 (Bot) 聚合 DTO")
|
||||
public class BotDTO {
|
||||
|
||||
// ========== 1. SearchBot (检索机器人) ==========
|
||||
|
||||
// 对应 /api/v1/searchbots/ask
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "SearchBot 提问请求")
|
||||
public static class SearchAskReq implements Serializable {
|
||||
@Schema(description = "用户问题", requiredMode = Schema.RequiredMode.REQUIRED, example = "What is RAG?")
|
||||
@NotBlank(message = "问题不能为空")
|
||||
@JsonProperty("question")
|
||||
private String question;
|
||||
|
||||
@Schema(description = "是否返回引用", defaultValue = "false")
|
||||
@JsonProperty("quote")
|
||||
@Builder.Default
|
||||
private Boolean quote = false;
|
||||
|
||||
@Schema(description = "是否流式返回", defaultValue = "true")
|
||||
@JsonProperty("stream")
|
||||
@Builder.Default
|
||||
private Boolean stream = true;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "SearchBot 提问响应")
|
||||
public static class SearchAskVO implements Serializable {
|
||||
@Schema(description = "回答内容")
|
||||
@JsonProperty("answer")
|
||||
private String answer;
|
||||
|
||||
@Schema(description = "引用来源 (Value 结构通常对应 RetrievalDTO.HitVO)")
|
||||
@JsonProperty("reference")
|
||||
private Map<String, Object> reference;
|
||||
}
|
||||
|
||||
// 对应 /api/v1/searchbots/related_questions
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "相关问题请求")
|
||||
public static class RelatedQuestionReq implements Serializable {
|
||||
@Schema(description = "用户问题", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotBlank(message = "问题不能为空")
|
||||
@JsonProperty("question")
|
||||
private String question;
|
||||
}
|
||||
|
||||
// 对应 /api/v1/searchbots/mindmap
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "思维导图请求")
|
||||
public static class MindMapReq implements Serializable {
|
||||
@Schema(description = "用户问题", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotBlank(message = "问题不能为空")
|
||||
@JsonProperty("question")
|
||||
private String question;
|
||||
}
|
||||
|
||||
// ========== 2. AgentBot (嵌入式 Agent) ==========
|
||||
|
||||
// 对应 /api/v1/agentbots/{id}/inputs
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "AgentBot 输入参数请求")
|
||||
public static class AgentInputsReq implements Serializable {
|
||||
}
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "AgentBot 输入参数定义响应")
|
||||
public static class AgentInputsVO implements Serializable {
|
||||
@Schema(description = "表单变量定义列表")
|
||||
@JsonProperty("variables")
|
||||
private List<Map<String, Object>> variables;
|
||||
}
|
||||
|
||||
// 对应 /api/v1/agentbots/{id}/completions
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "AgentBot 对话请求")
|
||||
public static class AgentCompletionReq implements Serializable {
|
||||
@Schema(description = "输入参数值")
|
||||
@JsonProperty("inputs")
|
||||
private Map<String, Object> inputs;
|
||||
|
||||
@Schema(description = "用户查询", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotBlank(message = "查询内容不能为空")
|
||||
@JsonProperty("question")
|
||||
private String question;
|
||||
|
||||
@Schema(description = "是否流式返回", defaultValue = "true")
|
||||
@JsonProperty("stream")
|
||||
@Builder.Default
|
||||
private Boolean stream = true;
|
||||
|
||||
@Schema(description = "会话 ID")
|
||||
@JsonProperty("session_id")
|
||||
private String sessionId;
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package xiaozhi.modules.knowledge.dto.chat;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 聊天对话请求 DTO (OpenAI 兼容格式)
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "聊天对话请求")
|
||||
public class ChatCompletionRequest implements Serializable {
|
||||
|
||||
@Schema(description = "模型标识 (对应 agent_id 或 bot_id)", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@JsonProperty("model")
|
||||
private String model;
|
||||
|
||||
@Schema(description = "对话消息列表", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@JsonProperty("messages")
|
||||
private List<Message> messages;
|
||||
|
||||
@Schema(description = "是否流式返回", defaultValue = "false")
|
||||
@JsonProperty("stream")
|
||||
private Boolean stream = false;
|
||||
|
||||
@Schema(description = "温度系数 (0-1)", defaultValue = "0.7")
|
||||
@JsonProperty("temperature")
|
||||
private Double temperature;
|
||||
|
||||
@Schema(description = "Session ID (可选,用于延续会话)")
|
||||
@JsonProperty("session_id")
|
||||
private String sessionId;
|
||||
|
||||
@Schema(description = "其他RAGFlow特定参数 (可选)")
|
||||
private Map<String, Object> extra;
|
||||
|
||||
@Data
|
||||
public static class Message implements Serializable {
|
||||
@Schema(description = "角色 (system, user, assistant)", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private String role;
|
||||
|
||||
@Schema(description = "内容", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private String content;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,523 @@
|
||||
package xiaozhi.modules.knowledge.dto.chat;
|
||||
|
||||
import lombok.*;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import jakarta.validation.constraints.*;
|
||||
|
||||
/**
|
||||
* 对话管理聚合 DTO
|
||||
* <p>
|
||||
* 容器类,内含对话助手、会话和消息的所有请求/响应对象。
|
||||
* </p>
|
||||
*/
|
||||
@Schema(description = "对话管理聚合 DTO")
|
||||
public class ChatDTO {
|
||||
|
||||
// ========== 1. 对话助手 (Assistant/Bot) 相关 ==========
|
||||
|
||||
/**
|
||||
* 提示词配置
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "提示词配置")
|
||||
public static class PromptConfig implements Serializable {
|
||||
|
||||
@Schema(description = "系统提示词", example = "你是一个专业的客服助手...")
|
||||
@JsonProperty("prompt")
|
||||
private String systemPrompt;
|
||||
|
||||
@Schema(description = "开场白", example = "您好,我是您的智能助手,请问有什么可以帮您?")
|
||||
private String opener;
|
||||
|
||||
@Schema(description = "空结果回复", example = "抱歉,我没有找到相关信息。")
|
||||
@JsonProperty("empty_response")
|
||||
private String emptyResponse;
|
||||
|
||||
@Schema(description = "是否展示引用", example = "true")
|
||||
@JsonProperty("show_quote")
|
||||
private Boolean quote;
|
||||
|
||||
@Schema(description = "是否启用 TTS", example = "false")
|
||||
private Boolean tts;
|
||||
|
||||
@Schema(description = "相似度阈值 (0.0 - 1.0)", example = "0.2")
|
||||
@JsonProperty("similarity_threshold")
|
||||
private Float similarityThreshold;
|
||||
|
||||
@Schema(description = "关键词相似度权重 (0.0 - 1.0)", example = "0.7")
|
||||
@JsonProperty("keywords_similarity_weight")
|
||||
private Float vectorSimilarityWeight;
|
||||
|
||||
@Schema(description = "检索 Top N", example = "6")
|
||||
@JsonProperty("top_n")
|
||||
private Integer topK;
|
||||
|
||||
@Schema(description = "Rerank 模型", example = "rerank_model_001")
|
||||
@JsonProperty("rerank_model")
|
||||
private String rerankId;
|
||||
|
||||
@Schema(description = "是否启用多轮对话优化", example = "false")
|
||||
@JsonProperty("refine_multiturn")
|
||||
private Boolean refineMultigraph;
|
||||
|
||||
@Schema(description = "变量列表")
|
||||
private List<Map<String, Object>> variables;
|
||||
}
|
||||
|
||||
/**
|
||||
* LLM 配置
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "LLM 模型配置")
|
||||
public static class LLMConfig implements Serializable {
|
||||
|
||||
@NotBlank(message = "模型名称不能为空")
|
||||
@Schema(description = "模型名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "gpt-4")
|
||||
@JsonProperty("model_name")
|
||||
private String modelName;
|
||||
|
||||
@Schema(description = "温度参数 (0.0 - 2.0)", example = "0.7")
|
||||
private Float temperature;
|
||||
|
||||
@Schema(description = "Top P 采样", example = "0.9")
|
||||
@JsonProperty("top_p")
|
||||
private Float topP;
|
||||
|
||||
@Schema(description = "最大 Token 数", example = "4096")
|
||||
@JsonProperty("max_tokens")
|
||||
private Integer maxTokens;
|
||||
|
||||
@Schema(description = "存在惩罚", example = "0.0")
|
||||
@JsonProperty("presence_penalty")
|
||||
private Float presencePenalty;
|
||||
|
||||
@Schema(description = "频率惩罚", example = "0.0")
|
||||
@JsonProperty("frequency_penalty")
|
||||
private Float frequencyPenalty;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建助手请求
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "创建助手请求")
|
||||
public static class AssistantCreateReq implements Serializable {
|
||||
|
||||
@NotBlank(message = "助手名称不能为空")
|
||||
@Schema(description = "助手名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "智能客服助手")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "助手头像 (Base64 编码)", example = "")
|
||||
private String avatar;
|
||||
|
||||
@Schema(description = "关联的知识库 ID 列表", example = "[\"kb_001\", \"kb_002\"]")
|
||||
@JsonProperty("dataset_ids")
|
||||
private List<String> datasetIds;
|
||||
|
||||
@Schema(description = "助手描述", example = "这是一个智能客服助手")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "LLM 模型配置")
|
||||
@JsonProperty("llm")
|
||||
private LLMConfig llm;
|
||||
|
||||
@Schema(description = "提示词配置")
|
||||
@JsonProperty("prompt")
|
||||
private PromptConfig promptConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新助手请求
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "更新助手请求")
|
||||
public static class AssistantUpdateReq implements Serializable {
|
||||
|
||||
@Schema(description = "助手名称", example = "智能客服助手 V2")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "助手头像 (Base64 编码)", example = "")
|
||||
private String avatar;
|
||||
|
||||
@Schema(description = "关联的知识库 ID 列表", example = "[\"kb_001\", \"kb_002\"]")
|
||||
@JsonProperty("dataset_ids")
|
||||
private List<String> datasetIds;
|
||||
|
||||
@Schema(description = "助手描述", example = "这是一个智能客服助手")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "LLM 模型配置")
|
||||
@JsonProperty("llm")
|
||||
private LLMConfig llm;
|
||||
|
||||
@Schema(description = "提示词配置")
|
||||
@JsonProperty("prompt")
|
||||
private PromptConfig promptConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询助手列表请求
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "查询助手列表请求")
|
||||
public static class AssistantListReq implements Serializable {
|
||||
|
||||
@Schema(description = "页码 (从 1 开始)", example = "1")
|
||||
private Integer page;
|
||||
|
||||
@Schema(description = "每页数量", example = "30")
|
||||
@JsonProperty("page_size")
|
||||
private Integer pageSize;
|
||||
|
||||
@Schema(description = "按名称过滤 (模糊匹配)", example = "客服")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "排序字段: create_time / update_time", example = "create_time")
|
||||
private String orderby;
|
||||
|
||||
@Schema(description = "是否降序", example = "true")
|
||||
private Boolean desc;
|
||||
|
||||
@Schema(description = "按 ID 精确筛选", example = "assistant_001")
|
||||
private String id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 助手详情 VO
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "助手详情 VO")
|
||||
public static class AssistantVO implements Serializable {
|
||||
|
||||
@Schema(description = "助手 ID", example = "assistant_001")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "租户 ID", example = "tenant_001")
|
||||
@JsonProperty("tenant_id")
|
||||
private String tenantId;
|
||||
|
||||
@Schema(description = "助手名称", example = "智能客服助手")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "助手头像", example = "")
|
||||
private String avatar;
|
||||
|
||||
@Schema(description = "关联的知识库 ID 列表")
|
||||
@JsonProperty("dataset_ids")
|
||||
private List<String> datasetIds;
|
||||
|
||||
@Schema(description = "关联的知识库列表 (详情)")
|
||||
private List<SimpleDatasetVO> datasets;
|
||||
|
||||
@Schema(description = "助手描述")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "LLM 模型配置")
|
||||
@JsonProperty("llm")
|
||||
private LLMConfig llm;
|
||||
|
||||
@Schema(description = "提示词配置")
|
||||
@JsonProperty("prompt")
|
||||
private PromptConfig promptConfig;
|
||||
|
||||
@Schema(description = "创建时间 (时间戳)", example = "1700000000000")
|
||||
@JsonProperty("create_time")
|
||||
private Long createTime;
|
||||
|
||||
@Schema(description = "更新时间 (时间戳)", example = "1700000001000")
|
||||
@JsonProperty("update_time")
|
||||
private Long updateTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除助手请求
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "删除助手请求")
|
||||
public static class AssistantDeleteReq implements Serializable {
|
||||
|
||||
@Schema(description = "要删除的助手 ID 列表", example = "[\"assistant_001\", \"assistant_002\"]")
|
||||
private List<String> ids;
|
||||
}
|
||||
|
||||
// ========== 2. 会话 (Session) 相关 ==========
|
||||
|
||||
/**
|
||||
* 创建会话请求
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "创建会话请求")
|
||||
public static class SessionCreateReq implements Serializable {
|
||||
|
||||
@Schema(description = "会话名称", example = "技术咨询会话")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "用户 ID", example = "user_001")
|
||||
@JsonProperty("user_id")
|
||||
private String userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新会话请求
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "更新会话请求")
|
||||
public static class SessionUpdateReq implements Serializable {
|
||||
|
||||
@Schema(description = "会话名称", example = "技术咨询会话 - 更新")
|
||||
private String name;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询会话列表请求
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "查询会话列表请求")
|
||||
public static class SessionListReq implements Serializable {
|
||||
|
||||
@Schema(description = "助手 ID", example = "assistant_001")
|
||||
@JsonProperty("assistant_id")
|
||||
private String assistantId;
|
||||
|
||||
@Schema(description = "页码 (从 1 开始)", example = "1")
|
||||
private Integer page;
|
||||
|
||||
@Schema(description = "每页数量", example = "30")
|
||||
@JsonProperty("page_size")
|
||||
private Integer pageSize;
|
||||
|
||||
@Schema(description = "按名称过滤", example = "技术")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "排序字段", example = "create_time")
|
||||
private String orderby;
|
||||
|
||||
@Schema(description = "是否降序", example = "true")
|
||||
private Boolean desc;
|
||||
|
||||
@Schema(description = "会话 ID 精确筛选", example = "session_001")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "用户标识筛选", example = "user_001")
|
||||
@JsonProperty("user_id")
|
||||
private String userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 会话详情 VO
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "会话详情 VO")
|
||||
public static class SessionVO implements Serializable {
|
||||
|
||||
@Schema(description = "会话 ID", example = "session_001")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "助手 ID", example = "assistant_001")
|
||||
@JsonProperty("chat_id")
|
||||
private String chatId;
|
||||
|
||||
@Schema(description = "助手 ID (兼容旧版)", example = "assistant_001")
|
||||
@JsonProperty("assistant_id")
|
||||
private String assistantId;
|
||||
|
||||
@Schema(description = "会话名称", example = "技术咨询会话")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "创建时间 (时间戳)", example = "1700000000000")
|
||||
@JsonProperty("create_time")
|
||||
private Long createTime;
|
||||
|
||||
@Schema(description = "更新时间 (时间戳)", example = "1700000001000")
|
||||
@JsonProperty("update_time")
|
||||
private Long updateTime;
|
||||
|
||||
@Schema(description = "创建日期", example = "2024-05-01 10:00:00")
|
||||
@JsonProperty("create_date")
|
||||
private String createDate;
|
||||
|
||||
@Schema(description = "更新日期", example = "2024-05-01 10:00:00")
|
||||
@JsonProperty("update_date")
|
||||
private String updateDate;
|
||||
|
||||
@Schema(description = "用户 ID", example = "user_001")
|
||||
@JsonProperty("user_id")
|
||||
private String userId;
|
||||
|
||||
@Schema(description = "对话历史消息列表")
|
||||
private List<Map<String, Object>> messages;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除会话请求
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "删除会话请求")
|
||||
public static class SessionDeleteReq implements Serializable {
|
||||
|
||||
@Schema(description = "要删除的会话 ID 列表", example = "[\"session_001\", \"session_002\"]")
|
||||
private List<String> ids;
|
||||
}
|
||||
|
||||
// ========== 3. 消息/对话 (Completion) 相关 ==========
|
||||
|
||||
/**
|
||||
* 发送消息请求
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "发送消息请求")
|
||||
public static class CompletionReq implements Serializable {
|
||||
|
||||
@NotBlank(message = "问题内容不能为空")
|
||||
@Schema(description = "用户问题", requiredMode = Schema.RequiredMode.REQUIRED, example = "请介绍一下你们的产品")
|
||||
private String question;
|
||||
|
||||
@Schema(description = "是否使用流式响应 (SSE)", example = "true")
|
||||
@Builder.Default
|
||||
private Boolean stream = true;
|
||||
|
||||
@NotBlank(message = "会话 ID 不能为空")
|
||||
@Schema(description = "会话 ID (可选,不传则创建新会话)", example = "session_001")
|
||||
@JsonProperty("session_id")
|
||||
private String sessionId;
|
||||
|
||||
@Schema(description = "是否展示引用", example = "true")
|
||||
private Boolean quote;
|
||||
|
||||
@Schema(description = "指定检索的文档 ID 列表 (逗号分隔)", example = "doc_001,doc_002")
|
||||
@JsonProperty("doc_ids")
|
||||
private String docIds;
|
||||
|
||||
@Schema(description = "元数据过滤条件")
|
||||
@JsonProperty("metadata_condition")
|
||||
private Map<String, Object> metadataCondition;
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息响应 VO
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "消息响应 VO")
|
||||
public static class CompletionVO implements Serializable {
|
||||
|
||||
@Schema(description = "AI 回答内容")
|
||||
private String answer;
|
||||
|
||||
@Schema(description = "引用信息")
|
||||
private Reference reference;
|
||||
|
||||
@Schema(description = "会话 ID", example = "session_001")
|
||||
@JsonProperty("session_id")
|
||||
private String sessionId;
|
||||
|
||||
@Schema(description = "任务 ID (用于流式响应追踪)", example = "task_001")
|
||||
@JsonProperty("task_id")
|
||||
private String taskId;
|
||||
|
||||
/**
|
||||
* 引用信息 (检索命中结果)
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "引用信息")
|
||||
public static class Reference implements Serializable {
|
||||
|
||||
@Schema(description = "命中的文档块列表")
|
||||
private List<xiaozhi.modules.knowledge.dto.document.RetrievalDTO.HitVO> chunks;
|
||||
|
||||
@Schema(description = "文档聚合信息")
|
||||
@JsonProperty("doc_aggs")
|
||||
private List<DocAgg> docAggs;
|
||||
}
|
||||
|
||||
/**
|
||||
* 文档聚合信息
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "文档聚合信息")
|
||||
public static class DocAgg implements Serializable {
|
||||
|
||||
@Schema(description = "文档 ID", example = "doc_001")
|
||||
@JsonProperty("doc_id")
|
||||
private String docId;
|
||||
|
||||
@Schema(description = "文档名称", example = "产品手册.pdf")
|
||||
@JsonProperty("doc_name")
|
||||
private String docName;
|
||||
|
||||
@Schema(description = "命中次数", example = "3")
|
||||
private Integer count;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 简易知识库 VO (用于 Assistant 列表)
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "简易知识库 VO")
|
||||
public static class SimpleDatasetVO implements Serializable {
|
||||
@Schema(description = "知识库 ID")
|
||||
private String id;
|
||||
@Schema(description = "知识库名称")
|
||||
private String name;
|
||||
@Schema(description = "头像")
|
||||
private String avatar;
|
||||
@Schema(description = "分块数量")
|
||||
@JsonProperty("chunk_num")
|
||||
private Integer chunkNum;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package xiaozhi.modules.knowledge.dto.common;
|
||||
|
||||
import lombok.*;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import jakarta.validation.constraints.*;
|
||||
|
||||
@Schema(description = "通用扩展功能 DTO")
|
||||
public class CommonDTO {
|
||||
|
||||
// ========== 1. 引用详情 (detail_share_embedded) ==========
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "引用详情请求")
|
||||
public static class ReferenceDetailReq implements Serializable {
|
||||
@Schema(description = "切片 ID", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotBlank(message = "切片 ID 不能为空")
|
||||
@JsonProperty("chunk_id")
|
||||
private String chunkId;
|
||||
|
||||
@Schema(description = "知识库 ID")
|
||||
@JsonProperty("knowledge_id")
|
||||
private String knowledgeId;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "引用详情响应")
|
||||
public static class ReferenceDetailVO implements Serializable {
|
||||
@Schema(description = "切片 ID")
|
||||
@JsonProperty("chunk_id")
|
||||
private String chunkId;
|
||||
|
||||
@Schema(description = "完整内容")
|
||||
@JsonProperty("content_with_weight")
|
||||
private String contentWithWeight;
|
||||
|
||||
@Schema(description = "文档名称")
|
||||
@JsonProperty("doc_name")
|
||||
private String docName;
|
||||
|
||||
@Schema(description = "图片 ID 列表")
|
||||
@JsonProperty("img_id")
|
||||
private String imageId; // 注意:RAGFlow 有时返回 String 有时返回 List,需根据实际情况确认,暂定 String 用于 ID
|
||||
|
||||
@Schema(description = "文档 ID")
|
||||
@JsonProperty("doc_id")
|
||||
private String docId;
|
||||
}
|
||||
|
||||
// ========== 2. 通用问答 (ask_about) - 调试用 ==========
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "通用问答请求 (调试用)")
|
||||
public static class AskAboutReq implements Serializable {
|
||||
@Schema(description = "用户问题", requiredMode = Schema.RequiredMode.REQUIRED, example = "What is this dataset about?")
|
||||
@NotBlank(message = "问题不能为空")
|
||||
@JsonProperty("question")
|
||||
private String question;
|
||||
|
||||
@Schema(description = "数据集 ID 列表", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotEmpty(message = "数据集列表不能为空")
|
||||
@JsonProperty("dataset_ids")
|
||||
private List<String> datasetIds;
|
||||
}
|
||||
|
||||
// 响应通常复用 String 或者简单的 Map 结构,视具体实现而定,暂不定义专用 VO
|
||||
}
|
||||
@@ -0,0 +1,449 @@
|
||||
package xiaozhi.modules.knowledge.dto.dataset;
|
||||
|
||||
import lombok.*;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import jakarta.validation.constraints.*;
|
||||
|
||||
/**
|
||||
* 知识库管理聚合 DTO
|
||||
* <p>
|
||||
* 容器类,内含知识库模块所有请求/响应对象的静态内部类定义。
|
||||
* </p>
|
||||
*/
|
||||
@Schema(description = "知识库管理聚合 DTO")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class DatasetDTO {
|
||||
|
||||
// ========== 通用内部类 ==========
|
||||
|
||||
/**
|
||||
* 解析器配置
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "解析器配置")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class ParserConfig implements Serializable {
|
||||
|
||||
@Schema(description = "分块 token 数量", example = "128")
|
||||
@JsonProperty("chunk_token_num")
|
||||
private Integer chunkTokenNum;
|
||||
|
||||
@Schema(description = "分隔符", example = "\\n!?;。;!?")
|
||||
private String delimiter;
|
||||
|
||||
@Schema(description = "布局识别模型: DeepDOC / Simple", example = "DeepDOC")
|
||||
@JsonProperty("layout_recognize")
|
||||
private String layoutRecognize;
|
||||
|
||||
@Schema(description = "是否将 Excel 转为 HTML", example = "false")
|
||||
private Boolean html4excel;
|
||||
|
||||
@Schema(description = "自动生成关键词数量 (0 表示关闭)", example = "0")
|
||||
@JsonProperty("auto_keywords")
|
||||
private Integer autoKeywords;
|
||||
|
||||
@Schema(description = "自动生成问题数量 (0 表示关闭)", example = "0")
|
||||
@JsonProperty("auto_questions")
|
||||
private Integer autoQuestions;
|
||||
}
|
||||
|
||||
// ========== 请求类 ==========
|
||||
|
||||
/**
|
||||
* 创建知识库请求 (映射接口 1: create)
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "创建知识库请求")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class CreateReq implements Serializable {
|
||||
|
||||
@NotBlank(message = "知识库名称不能为空")
|
||||
@Schema(description = "知识库名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "my_dataset")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "知识库头像 (Base64 编码)", example = "")
|
||||
private String avatar;
|
||||
|
||||
@Schema(description = "知识库描述", example = "用于存储产品文档")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "嵌入模型名称", example = "BAAI/bge-large-zh-v1.5")
|
||||
@JsonProperty("embedding_model")
|
||||
private String embeddingModel;
|
||||
|
||||
@Schema(description = "权限设置: me / team", example = "me")
|
||||
private String permission;
|
||||
|
||||
@Schema(description = "分块方法: naive / manual / qa / table / paper / book / laws / presentation / picture / one / knowledge_graph / email", example = "naive")
|
||||
@JsonProperty("chunk_method")
|
||||
private String chunkMethod;
|
||||
|
||||
@Schema(description = "解析器配置")
|
||||
@JsonProperty("parser_config")
|
||||
private ParserConfig parserConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新知识库请求 (映射接口 4: update)
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "更新知识库请求")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class UpdateReq implements Serializable {
|
||||
|
||||
@Schema(description = "知识库名称", example = "updated_dataset")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "知识库头像 (Base64 编码)", example = "")
|
||||
private String avatar;
|
||||
|
||||
@Schema(description = "知识库描述", example = "更新后的描述")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "权限设置: me / team", example = "team")
|
||||
private String permission;
|
||||
|
||||
@Schema(description = "嵌入模型名称", example = "BAAI/bge-large-zh-v1.5")
|
||||
@JsonProperty("embedding_model")
|
||||
private String embeddingModel;
|
||||
|
||||
@Schema(description = "分块方法: naive / manual / qa / table / paper / book / laws / presentation / picture / one / knowledge_graph / email", example = "naive")
|
||||
@JsonProperty("chunk_method")
|
||||
private String chunkMethod;
|
||||
|
||||
@Schema(description = "解析器配置")
|
||||
@JsonProperty("parser_config")
|
||||
private ParserConfig parserConfig;
|
||||
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
@Schema(description = "PageRank 权重 (0-100)", example = "50")
|
||||
private Integer pagerank;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询知识库列表请求 (映射接口 3: list_datasets)
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "查询知识库列表请求")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class ListReq implements Serializable {
|
||||
|
||||
@Schema(description = "页码 (从 1 开始)", example = "1")
|
||||
private Integer page;
|
||||
|
||||
@Schema(description = "每页数量", example = "30")
|
||||
@JsonProperty("page_size")
|
||||
private Integer pageSize;
|
||||
|
||||
@Schema(description = "排序字段: create_time / update_time", example = "create_time")
|
||||
private String orderby;
|
||||
|
||||
@Schema(description = "是否降序", example = "true")
|
||||
private Boolean desc;
|
||||
|
||||
@Schema(description = "按名称过滤 (模糊匹配)", example = "my_dataset")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "按知识库 ID 过滤", example = "abc123")
|
||||
private String id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除知识库请求 (映射接口 2: delete)
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "批量删除知识库请求")
|
||||
public static class BatchIdReq implements Serializable {
|
||||
|
||||
@NotNull(message = "知识库 ID 列表不能为空")
|
||||
@Size(min = 1, message = "至少需要一个知识库 ID")
|
||||
@Schema(description = "知识库 ID 列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "[\"id1\", \"id2\"]")
|
||||
private List<String> ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行 GraphRAG 请求
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "运行 GraphRAG 请求")
|
||||
public static class RunGraphRagReq implements Serializable {
|
||||
|
||||
@Schema(description = "实体类型列表", example = "[\"person\", \"organization\"]")
|
||||
@JsonProperty("entity_types")
|
||||
private List<String> entityTypes;
|
||||
|
||||
@Schema(description = "构建方法: light / fast / full", example = "light")
|
||||
private String method;
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行 RAPTOR 请求
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "运行 RAPTOR 请求")
|
||||
public static class RunRaptorReq implements Serializable {
|
||||
|
||||
@Schema(description = "最大聚类数", example = "64")
|
||||
@JsonProperty("max_cluster")
|
||||
private Integer maxCluster;
|
||||
|
||||
@Schema(description = "自定义提示词", example = "请总结以下内容...")
|
||||
private String prompt;
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步任务 ID 响应 VO (映射接口 7/8: run_graphrag/run_raptor)
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "异步任务 ID 响应")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class TaskIdVO implements Serializable {
|
||||
|
||||
@Schema(description = "GraphRAG 任务 ID", example = "task_uuid_12345678")
|
||||
@JsonProperty("graphrag_task_id")
|
||||
private String graphragTaskId;
|
||||
|
||||
@Schema(description = "RAPTOR 任务 ID", example = "task_uuid_87654321")
|
||||
@JsonProperty("raptor_task_id")
|
||||
private String raptorTaskId;
|
||||
}
|
||||
|
||||
// ========== 响应类 ==========
|
||||
|
||||
/**
|
||||
* 知识库详情 VO (映射接口 1/3 的返回数据项)
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "知识库详情 VO")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class InfoVO implements Serializable {
|
||||
|
||||
@Schema(description = "知识库 ID", example = "abc123")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "知识库名称", example = "my_dataset")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "知识库头像 (Base64 编码)", example = "")
|
||||
private String avatar;
|
||||
|
||||
@Schema(description = "租户 ID", example = "tenant_001")
|
||||
@JsonProperty("tenant_id")
|
||||
private String tenantId;
|
||||
|
||||
@Schema(description = "知识库描述", example = "用于存储产品文档")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "嵌入模型名称", example = "BAAI/bge-large-zh-v1.5")
|
||||
@JsonProperty("embedding_model")
|
||||
private String embeddingModel;
|
||||
|
||||
@Schema(description = "权限设置: me / team", example = "me")
|
||||
private String permission;
|
||||
|
||||
@Schema(description = "分块方法", example = "naive")
|
||||
@JsonProperty("chunk_method")
|
||||
private String chunkMethod;
|
||||
|
||||
@Schema(description = "解析器配置")
|
||||
@JsonProperty("parser_config")
|
||||
private ParserConfig parserConfig;
|
||||
|
||||
@Schema(description = "分块总数", example = "1024")
|
||||
@JsonProperty("chunk_count")
|
||||
private Long chunkCount;
|
||||
|
||||
@Schema(description = "文档总数", example = "50")
|
||||
@JsonProperty("document_count")
|
||||
private Long documentCount;
|
||||
|
||||
@Schema(description = "创建时间 (时间戳)", example = "1700000000000")
|
||||
@JsonProperty("create_time")
|
||||
private Long createTime;
|
||||
|
||||
@Schema(description = "更新时间 (时间戳)", example = "1700000001000")
|
||||
@JsonProperty("update_time")
|
||||
private Long updateTime;
|
||||
|
||||
@Schema(description = "总 Token 数", example = "102400")
|
||||
@JsonProperty("token_num")
|
||||
private Long tokenNum;
|
||||
|
||||
@Schema(description = "创建日期 (格式: yyyy-MM-dd HH:mm:ss)")
|
||||
@JsonProperty("create_date")
|
||||
private String createDate;
|
||||
|
||||
@Schema(description = "最后更新日期 (格式: yyyy-MM-dd HH:mm:ss)")
|
||||
@JsonProperty("update_date")
|
||||
private String updateDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量操作响应 VO
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "批量操作响应 VO")
|
||||
public static class BatchOperationVO implements Serializable {
|
||||
|
||||
@Schema(description = "成功操作数量", example = "5")
|
||||
@JsonProperty("success_count")
|
||||
private Integer successCount;
|
||||
|
||||
@Schema(description = "错误列表")
|
||||
private List<Object> errors;
|
||||
}
|
||||
|
||||
// ========== 知识图谱相关 ==========
|
||||
|
||||
/**
|
||||
* 知识图谱数据 VO (映射接口 5: knowledge_graph)
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "知识图谱数据 VO")
|
||||
public static class GraphVO implements Serializable {
|
||||
|
||||
@Schema(description = "图谱节点列表")
|
||||
private List<Node> nodes;
|
||||
|
||||
@Schema(description = "图谱边列表")
|
||||
private List<Edge> edges;
|
||||
|
||||
@Schema(description = "思维导图数据")
|
||||
@JsonProperty("mind_map")
|
||||
private Map<String, Object> mindMap;
|
||||
|
||||
/**
|
||||
* 图谱节点
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "图谱节点")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class Node implements Serializable {
|
||||
|
||||
@Schema(description = "节点 ID", example = "node_001")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "节点标签", example = "产品")
|
||||
private String label;
|
||||
|
||||
@Schema(description = "PageRank 值", example = "0.85")
|
||||
private Double pagerank;
|
||||
|
||||
@Schema(description = "节点颜色", example = "#FF5733")
|
||||
private String color;
|
||||
|
||||
@Schema(description = "节点图片 URL", example = "https://example.com/icon.png")
|
||||
private String img;
|
||||
}
|
||||
|
||||
/**
|
||||
* 图谱边
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "图谱边")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class Edge implements Serializable {
|
||||
|
||||
@Schema(description = "源节点 ID", example = "node_001")
|
||||
private String source;
|
||||
|
||||
@Schema(description = "目标节点 ID", example = "node_002")
|
||||
private String target;
|
||||
|
||||
@Schema(description = "边权重", example = "0.75")
|
||||
private Double weight;
|
||||
|
||||
@Schema(description = "边标签 (关系描述)", example = "属于")
|
||||
private String label;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 异步任务追踪 (GraphRAG/RAPTOR) ==========
|
||||
|
||||
/**
|
||||
* 异步任务追踪 VO (映射接口 9/10: 任务进度返回)
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "异步任务追踪 VO")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class TaskTraceVO implements Serializable {
|
||||
|
||||
@Schema(description = "任务 ID", example = "task_001")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "文档 ID", example = "doc_001")
|
||||
@JsonProperty("doc_id")
|
||||
private String docId;
|
||||
|
||||
@Schema(description = "起始页码", example = "1")
|
||||
@JsonProperty("from_page")
|
||||
private Integer fromPage;
|
||||
|
||||
@Schema(description = "结束页码", example = "10")
|
||||
@JsonProperty("to_page")
|
||||
private Integer toPage;
|
||||
|
||||
@Schema(description = "进度百分比 (0.0 - 1.0)", example = "0.75")
|
||||
private Double progress;
|
||||
|
||||
@Schema(description = "进度消息", example = "正在处理第 5 页...")
|
||||
@JsonProperty("progress_msg")
|
||||
private String progressMsg;
|
||||
|
||||
@Schema(description = "创建时间 (时间戳)", example = "1700000000000")
|
||||
@JsonProperty("create_time")
|
||||
private Long createTime;
|
||||
|
||||
@Schema(description = "更新时间 (时间戳)", example = "1700000001000")
|
||||
@JsonProperty("update_time")
|
||||
private Long updateTime;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
package xiaozhi.modules.knowledge.dto.document;
|
||||
|
||||
import lombok.*;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import jakarta.validation.constraints.*;
|
||||
|
||||
/**
|
||||
* 切片管理聚合 DTO
|
||||
*/
|
||||
@Schema(description = "切片管理聚合 DTO")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class ChunkDTO {
|
||||
|
||||
/**
|
||||
* 新增切片请求参数
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "新增切片请求参数")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class AddReq implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "切片内容", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotBlank(message = "切片内容不能为空")
|
||||
private String content;
|
||||
|
||||
@Schema(description = "重要关键词列表")
|
||||
@JsonProperty("important_keywords")
|
||||
private List<String> importantKeywords;
|
||||
|
||||
@Schema(description = "预设问题列表")
|
||||
private List<String> questions;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新切片请求参数
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "更新切片请求参数")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class UpdateReq implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "新的切片内容")
|
||||
private String content;
|
||||
|
||||
@Schema(description = "更新关键词列表 (覆盖原有列表)")
|
||||
@JsonProperty("important_keywords")
|
||||
private List<String> importantKeywords;
|
||||
|
||||
@Schema(description = "启用/禁用 (true: 启用, false: 禁用)")
|
||||
private Boolean available;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取切片列表请求参数
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "获取切片列表请求参数")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class ListReq implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "页码 (默认 1)")
|
||||
private Integer page;
|
||||
|
||||
@Schema(description = "每页数量 (默认 30)")
|
||||
@JsonProperty("page_size")
|
||||
private Integer pageSize;
|
||||
|
||||
@Schema(description = "搜索关键词 (全文检索)")
|
||||
private String keywords;
|
||||
|
||||
@Schema(description = "精确切片 ID")
|
||||
private String id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除切片请求参数
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "批量删除切片请求参数")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class RemoveReq implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "切片 ID 列表", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@JsonProperty("chunk_ids")
|
||||
@NotEmpty(message = "切片ID列表不能为空")
|
||||
private List<String> chunkIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* 文档切片信息 VO
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "文档切片信息")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class InfoVO implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "切片 ID (通常为 document_id + 索引)", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private String id;
|
||||
|
||||
@Schema(description = "切片文本内容 (全文检索的主要对象)", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private String content;
|
||||
|
||||
@Schema(description = "所属文档 ID", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@JsonProperty("document_id")
|
||||
private String documentId;
|
||||
|
||||
@Schema(description = "文档名称 / 关键词")
|
||||
@JsonProperty("docnm_kwd")
|
||||
private String docnmKwd;
|
||||
|
||||
@Schema(description = "重要关键词列表 (用于关键词增强检索)")
|
||||
@JsonProperty("important_keywords")
|
||||
private List<String> importantKeywords;
|
||||
|
||||
@Schema(description = "预设问题列表 (用于 Q&A 模式增强)")
|
||||
private List<String> questions;
|
||||
|
||||
@Schema(description = "关联的图片 ID")
|
||||
@JsonProperty("image_id")
|
||||
private String imageId;
|
||||
|
||||
@Schema(description = "所属知识库 ID")
|
||||
@JsonProperty("dataset_id")
|
||||
private String datasetId;
|
||||
|
||||
@Schema(description = "切片是否可用 (true: 参与检索, false: 被禁用)")
|
||||
private Boolean available;
|
||||
|
||||
@Schema(description = "切片在原文中的位置索引列表 (RAGFlow返回嵌套数组, 如 [[start, end, filename]])")
|
||||
private List<List<Object>> positions;
|
||||
|
||||
@Schema(description = "Token ID 列表")
|
||||
@JsonProperty("token")
|
||||
private List<Integer> token;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分片列表聚合响应
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "分片列表聚合响应")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class ListVO implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "切片信息列表")
|
||||
private List<InfoVO> chunks;
|
||||
|
||||
@Schema(description = "关联的文档详细信息")
|
||||
private DocumentDTO.InfoVO doc;
|
||||
|
||||
@Schema(description = "总记录数")
|
||||
private Long total;
|
||||
}
|
||||
}
|
||||
+407
@@ -0,0 +1,407 @@
|
||||
package xiaozhi.modules.knowledge.dto.document;
|
||||
|
||||
import lombok.*;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.annotation.JsonAlias;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import jakarta.validation.constraints.*;
|
||||
|
||||
/**
|
||||
* 文档管理聚合 DTO
|
||||
*/
|
||||
@Schema(description = "文档管理聚合 DTO")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class DocumentDTO {
|
||||
|
||||
/**
|
||||
* 上传文档请求参数
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "上传文档请求参数")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class UploadReq implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "知识库 ID (必须指定归属)", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@JsonProperty("dataset_id")
|
||||
@NotBlank(message = "知识库ID不能为空")
|
||||
private String datasetId;
|
||||
|
||||
@Schema(description = "文件名 (如果指定,则覆盖原始文件名)")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "分块方法")
|
||||
@JsonProperty("chunk_method")
|
||||
private DocumentDTO.InfoVO.ChunkMethod chunkMethod;
|
||||
|
||||
@Schema(description = "解析参数配置")
|
||||
@JsonProperty("parser_config")
|
||||
private DocumentDTO.InfoVO.ParserConfig parserConfig;
|
||||
|
||||
@Schema(description = "虚拟文件夹路径 (默认为 /)")
|
||||
@JsonProperty("parent_path")
|
||||
private String parentPath;
|
||||
|
||||
@Schema(description = "元数据字段")
|
||||
@JsonProperty("meta")
|
||||
private Map<String, Object> metaFields;
|
||||
|
||||
@Schema(description = "文件二进制流 (支持 PDF, DOCX, TXT, MD 等多种格式)", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotNull(message = "上传文件不能为空")
|
||||
private org.springframework.web.multipart.MultipartFile file;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新文档请求参数
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "更新文档请求参数")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class UpdateReq implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "新文档名称 (必须包含文件后缀,且不能更改原始类型)")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "启用/禁用状态 (true: 启用, false: 禁用; 禁用后不参与检索)")
|
||||
private Boolean enabled;
|
||||
|
||||
@Schema(description = "新解析方法 (修改此项会重置解析状态)")
|
||||
@JsonProperty("chunk_method")
|
||||
private InfoVO.ChunkMethod chunkMethod;
|
||||
|
||||
@Schema(description = "新解析器详细配置 (应与 chunk_method 配套使用)")
|
||||
@JsonProperty("parser_config")
|
||||
private InfoVO.ParserConfig parserConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文档列表请求参数
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "获取文档列表请求参数")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class ListReq implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "页码 (默认: 1)")
|
||||
private Integer page;
|
||||
|
||||
@Schema(description = "每页数量 (默认: 30)")
|
||||
@JsonProperty("page_size")
|
||||
private Integer pageSize;
|
||||
|
||||
@Schema(description = "排序字段 (可选: create_time, name, size; 默认: create_time)")
|
||||
private String orderby;
|
||||
|
||||
@Schema(description = "是否降序排列 (true: 最新/最大在前; false: 最旧/最小在前; 默认: true)")
|
||||
private Boolean desc;
|
||||
|
||||
@Schema(description = "精确筛选: 文档 ID")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "精确筛选: 文档完整名称 (含后缀)")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "模糊搜索: 文档名称关键词")
|
||||
private String keywords;
|
||||
|
||||
@Schema(description = "筛选: 文件后缀列表 (如 ['pdf', 'docx'])")
|
||||
private List<String> suffix;
|
||||
|
||||
@Schema(description = "筛选: 运行状态列表")
|
||||
private List<InfoVO.RunStatus> run;
|
||||
|
||||
@Schema(description = "筛选: 起始创建时间 (时间戳, 毫秒)")
|
||||
@JsonProperty("create_time_from")
|
||||
private Long createTimeFrom;
|
||||
|
||||
@Schema(description = "筛选: 结束创建时间 (时间戳, 毫秒)")
|
||||
@JsonProperty("create_time_to")
|
||||
private Long createTimeTo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量文档操作请求参数 (用于删除、解析等)
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "批量文档操作请求参数")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class BatchIdReq implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "文档 ID 列表", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@JsonProperty("ids") // 为了兼容性,也可以考虑支持 document_ids,但这里统一叫 ids
|
||||
@JsonAlias("document_ids")
|
||||
@NotEmpty(message = "文档ID列表不能为空")
|
||||
private List<String> ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* 知识库文档信息 VO
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "知识库文档信息")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class InfoVO implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "文档 ID (唯一标识)", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private String id;
|
||||
|
||||
@Schema(description = "文档缩略图 URL (Base64 或 链接)")
|
||||
private String thumbnail;
|
||||
|
||||
@Schema(description = "所属知识库 ID", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@JsonProperty("dataset_id")
|
||||
private String datasetId;
|
||||
|
||||
@Schema(description = "文档解析方法 (决定了文档如何被切片)")
|
||||
@JsonProperty("chunk_method")
|
||||
private ChunkMethod chunkMethod;
|
||||
|
||||
@Schema(description = "关联的 ETL Pipeline ID (如有)")
|
||||
@JsonProperty("pipeline_id")
|
||||
private String pipelineId;
|
||||
|
||||
@Schema(description = "文档解析器的详细配置")
|
||||
@JsonProperty("parser_config")
|
||||
private ParserConfig parserConfig;
|
||||
|
||||
@Schema(description = "来源类型 (如 local, s3, url 等)")
|
||||
@JsonProperty("source_type")
|
||||
private String sourceType;
|
||||
|
||||
@Schema(description = "文档文件类型 (如 pdf, docx, txt)", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private String type;
|
||||
|
||||
@Schema(description = "创建者用户 ID")
|
||||
@JsonProperty("created_by")
|
||||
private String createdBy;
|
||||
|
||||
@Schema(description = "文档名称 (包含扩展名)", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private String name;
|
||||
|
||||
@Schema(description = "文件存储路径或位置标识")
|
||||
private String location;
|
||||
|
||||
@Schema(description = "文件大小 (单位: Bytes)")
|
||||
private Long size;
|
||||
|
||||
@Schema(description = "包含的 Token 总数 (解析后统计)")
|
||||
@JsonProperty("token_count")
|
||||
private Long tokenCount;
|
||||
|
||||
@Schema(description = "包含的切片 (Chunk) 总数")
|
||||
@JsonProperty("chunk_count")
|
||||
private Long chunkCount;
|
||||
|
||||
@Schema(description = "解析进度 (0.0 ~ 1.0, 1.0 表示完成)")
|
||||
private Double progress;
|
||||
|
||||
@Schema(description = "当前进度描述或错误信息")
|
||||
@JsonProperty("progress_msg")
|
||||
private String progressMsg;
|
||||
|
||||
@Schema(description = "开始处理的时间戳 (RAGFlow返回RFC1123格式)")
|
||||
@JsonProperty("process_begin_at")
|
||||
private String processBeginAt;
|
||||
|
||||
@Schema(description = "处理总耗时 (单位: 秒)")
|
||||
@JsonProperty("process_duration")
|
||||
private Double processDuration;
|
||||
|
||||
@Schema(description = "自定义元数据字段 (Key-Value 键值对)")
|
||||
@JsonProperty("meta_fields")
|
||||
private Map<String, Object> metaFields;
|
||||
|
||||
@Schema(description = "文件后缀名 (不含点)")
|
||||
private String suffix;
|
||||
|
||||
@Schema(description = "文档解析运行状态")
|
||||
private RunStatus run;
|
||||
|
||||
@Schema(description = "文档可用状态 (1: 启用/正常, 0: 禁用/失效)", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private String status;
|
||||
|
||||
@Schema(description = "创建时间 (时间戳, 毫秒)", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@JsonProperty("create_time")
|
||||
private Long createTime;
|
||||
|
||||
@Schema(description = "创建日期 (RAGFlow返回RFC1123格式)")
|
||||
@JsonProperty("create_date")
|
||||
private String createDate;
|
||||
|
||||
@Schema(description = "最后更新时间 (时间戳, 毫秒)")
|
||||
@JsonProperty("update_time")
|
||||
private Long updateTime;
|
||||
|
||||
@Schema(description = "最后更新日期 (RAGFlow返回RFC1123格式)")
|
||||
@JsonProperty("update_date")
|
||||
private String updateDate;
|
||||
|
||||
/**
|
||||
* 解析方法枚举 (ChunkMethod)
|
||||
*/
|
||||
public enum ChunkMethod {
|
||||
@Schema(description = "通用模式: 适用于大多数纯文本或混合文档")
|
||||
@JsonProperty("naive")
|
||||
NAIVE,
|
||||
@Schema(description = "手动模式: 允许用户手动编辑切片")
|
||||
@JsonProperty("manual")
|
||||
MANUAL,
|
||||
@Schema(description = "问答模式: 专门优化 Q&A 格式的文档")
|
||||
@JsonProperty("qa")
|
||||
QA,
|
||||
@Schema(description = "表格模式: 专门优化 Excel 或 CSV 等表格数据")
|
||||
@JsonProperty("table")
|
||||
TABLE,
|
||||
@Schema(description = "论文模式: 针对学术论文排版优化")
|
||||
@JsonProperty("paper")
|
||||
PAPER,
|
||||
@Schema(description = "书籍模式: 针对书籍章节结构优化")
|
||||
@JsonProperty("book")
|
||||
BOOK,
|
||||
@Schema(description = "法律法规模式: 针对法律条文结构优化")
|
||||
@JsonProperty("laws")
|
||||
LAWS,
|
||||
@Schema(description = "演示文稿模式: 针对 PPT 等演示文件优化")
|
||||
@JsonProperty("presentation")
|
||||
PRESENTATION,
|
||||
@Schema(description = "图片模式: 针对图片内容进行 OCR 和描述")
|
||||
@JsonProperty("picture")
|
||||
PICTURE,
|
||||
@Schema(description = "整体模式: 将整个文档作为一个切片")
|
||||
@JsonProperty("one")
|
||||
ONE,
|
||||
@Schema(description = "知识图谱模式: 提取实体关系构建图谱")
|
||||
@JsonProperty("knowledge_graph")
|
||||
KNOWLEDGE_GRAPH,
|
||||
@Schema(description = "邮件模式: 针对邮件格式优化")
|
||||
@JsonProperty("email")
|
||||
EMAIL;
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行状态枚举 (RunStatus)
|
||||
*/
|
||||
public enum RunStatus {
|
||||
@Schema(description = "未开始: 等待解析队列")
|
||||
@JsonProperty("UNSTART")
|
||||
UNSTART,
|
||||
@Schema(description = "进行中: 正在解析或索引")
|
||||
@JsonProperty("RUNNING")
|
||||
RUNNING,
|
||||
@Schema(description = "已取消: 用户手动取消")
|
||||
@JsonProperty("CANCEL")
|
||||
CANCEL,
|
||||
@Schema(description = "已完成: 解析成功")
|
||||
@JsonProperty("DONE")
|
||||
DONE,
|
||||
@Schema(description = "失败: 解析过程中出错")
|
||||
@JsonProperty("FAIL")
|
||||
FAIL;
|
||||
}
|
||||
|
||||
/**
|
||||
* 布局识别模型枚举
|
||||
*/
|
||||
public enum LayoutRecognize {
|
||||
@Schema(description = "深度文档理解模型: 适合复杂排版")
|
||||
@JsonProperty("DeepDOC")
|
||||
DeepDOC,
|
||||
@Schema(description = "简单规则模型: 适合纯文本")
|
||||
@JsonProperty("Simple")
|
||||
Simple;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "文档解析器参数配置")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class ParserConfig implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "切片最大 Token 数 (建议值: 512, 1024, 2048)")
|
||||
@JsonProperty("chunk_token_num")
|
||||
private Integer chunkTokenNum;
|
||||
|
||||
@Schema(description = "分段分隔符 (支持转义字符, 如 \\n)")
|
||||
private String delimiter;
|
||||
|
||||
@Schema(description = "布局识别模型 (DeepDOC/Simple)")
|
||||
@JsonProperty("layout_recognize")
|
||||
private LayoutRecognize layoutRecognize;
|
||||
|
||||
@Schema(description = "是否将 Excel 转换为 HTML 表格")
|
||||
@JsonProperty("html4excel")
|
||||
private Boolean html4excel;
|
||||
|
||||
@Schema(description = "自动提取关键词数量 (0 表示不提取)")
|
||||
@JsonProperty("auto_keywords")
|
||||
private Integer autoKeywords;
|
||||
|
||||
@Schema(description = "自动生成问题数量 (0 表示不生成)")
|
||||
@JsonProperty("auto_questions")
|
||||
private Integer autoQuestions;
|
||||
|
||||
@Schema(description = "自动生成标签数量")
|
||||
@JsonProperty("topn_tags")
|
||||
private Integer topnTags;
|
||||
|
||||
@Schema(description = "RAPTOR 高级索引配置")
|
||||
private RaptorConfig raptor;
|
||||
|
||||
@Schema(description = "GraphRAG 知识图谱配置")
|
||||
@JsonProperty("graphrag")
|
||||
private GraphRagConfig graphRag;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "RAPTOR (递归摘要索引) 配置")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class RaptorConfig implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
@Schema(description = "是否启用 RAPTOR 索引")
|
||||
@JsonProperty("use_raptor")
|
||||
private Boolean useRaptor;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "GraphRAG (图增强检索) 配置")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class GraphRagConfig implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
@Schema(description = "是否启用 GraphRAG 索引")
|
||||
@JsonProperty("use_graphrag")
|
||||
private Boolean useGraphRag;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+307
@@ -0,0 +1,307 @@
|
||||
package xiaozhi.modules.knowledge.dto.document;
|
||||
|
||||
import lombok.*;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import jakarta.validation.constraints.*;
|
||||
|
||||
/**
|
||||
* 检索与元数据管理聚合 DTO
|
||||
*/
|
||||
@Schema(description = "检索与元数据管理聚合 DTO")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class RetrievalDTO {
|
||||
|
||||
/**
|
||||
* 文档聚合信息 (VO)
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "文档聚合信息")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class DocAggVO implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "文档名称")
|
||||
@JsonProperty("doc_name")
|
||||
private String docName;
|
||||
|
||||
@Schema(description = "文档 ID")
|
||||
@JsonProperty("doc_id")
|
||||
private String docId;
|
||||
|
||||
@Schema(description = "数量")
|
||||
private Integer count;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检索测试请求参数
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "检索测试请求参数")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public static class TestReq implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "知识库 ID 列表", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@JsonProperty("dataset_ids")
|
||||
@NotEmpty(message = "知识库ID列表不能为空")
|
||||
private List<String> datasetIds;
|
||||
|
||||
@Schema(description = "文档 ID 列表 (可选,用于限定检索范围)")
|
||||
@JsonProperty("document_ids")
|
||||
private List<String> documentIds;
|
||||
|
||||
@Schema(description = "检索问题", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotBlank(message = "检索问题不能为空")
|
||||
private String question;
|
||||
|
||||
@Schema(description = "页码 (默认 1)")
|
||||
private Integer page;
|
||||
|
||||
@Schema(description = "每页数量 (默认 10)")
|
||||
@JsonProperty("page_size")
|
||||
private Integer pageSize;
|
||||
|
||||
@Schema(description = "相似度阈值 (默认 0.2)")
|
||||
@JsonProperty("similarity_threshold")
|
||||
private Float similarityThreshold;
|
||||
|
||||
@Schema(description = "向量相似度权重 (默认 0.3)")
|
||||
@JsonProperty("vector_similarity_weight")
|
||||
private Float vectorSimilarityWeight;
|
||||
|
||||
@Schema(description = "返回 Top K 切片 (默认 1024)")
|
||||
@JsonProperty("top_k")
|
||||
private Integer topK;
|
||||
|
||||
@Schema(description = "重排序模型 ID")
|
||||
@JsonProperty("rerank_id")
|
||||
private String rerankId;
|
||||
|
||||
@Schema(description = "是否高亮关键词")
|
||||
private Boolean highlight;
|
||||
|
||||
@Schema(description = "是否启用关键词检索")
|
||||
private Boolean keyword;
|
||||
|
||||
@Schema(description = "跨语言翻译列表 (可选)")
|
||||
@JsonProperty("cross_languages")
|
||||
private List<String> crossLanguages;
|
||||
|
||||
@Schema(description = "元数据过滤条件 (JSON 对象)")
|
||||
@JsonProperty("metadata_condition")
|
||||
private Map<String, Object> metadataCondition;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检索命中结果 (VO)
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "检索命中切片详情")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class HitVO implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "切片 ID", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private String id;
|
||||
|
||||
@Schema(description = "切片内容", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private String content;
|
||||
|
||||
@Schema(description = "所属文档 ID", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@JsonProperty("document_id")
|
||||
private String documentId;
|
||||
|
||||
@Schema(description = "所属知识库 ID")
|
||||
@JsonProperty("dataset_id")
|
||||
private String datasetId;
|
||||
|
||||
@Schema(description = "文档名称")
|
||||
@JsonProperty("document_name")
|
||||
private String documentName;
|
||||
|
||||
@Schema(description = "文档关键词")
|
||||
@JsonProperty("document_keyword")
|
||||
private String documentKeyword;
|
||||
|
||||
@Schema(description = "综合相似度", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private Float similarity;
|
||||
|
||||
@Schema(description = "向量相似度")
|
||||
@JsonProperty("vector_similarity")
|
||||
private Float vectorSimilarity;
|
||||
|
||||
@Schema(description = "关键词相似度")
|
||||
@JsonProperty("term_similarity")
|
||||
private Float termSimilarity;
|
||||
|
||||
@Schema(description = "索引位置")
|
||||
private Integer index;
|
||||
|
||||
@Schema(description = "高亮内容")
|
||||
private String highlight;
|
||||
|
||||
@Schema(description = "重要关键词列表")
|
||||
@JsonProperty("important_keywords")
|
||||
private List<String> importantKeywords;
|
||||
|
||||
@Schema(description = "预设问题列表")
|
||||
private List<String> questions;
|
||||
|
||||
@Schema(description = "图片 ID")
|
||||
@JsonProperty("image_id")
|
||||
private String imageId;
|
||||
|
||||
@Schema(description = "位置索引 (RAGFlow返回嵌套数组, 如 [[start, end, filename]])")
|
||||
private Object positions;
|
||||
}
|
||||
|
||||
/**
|
||||
* 知识库元数据摘要 (VO)
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "知识库元数据摘要信息")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class MetaSummaryVO implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "文档总数", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@JsonProperty("total_doc_count")
|
||||
private Long totalDocCount;
|
||||
|
||||
@Schema(description = "Token 总数", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@JsonProperty("total_token_count")
|
||||
private Long totalTokenCount;
|
||||
|
||||
@Schema(description = "文件类型分布 (key: 文件后缀, value: 数量)")
|
||||
@JsonProperty("file_type_distribution")
|
||||
private Map<String, Long> fileTypeDistribution;
|
||||
|
||||
@Schema(description = "文状态分布 (key: 状态码, value: 数量)")
|
||||
@JsonProperty("status_distribution")
|
||||
private Map<String, Long> statusDistribution;
|
||||
|
||||
@Schema(description = "自定义元数据统计 (key: 字段名, value: 数量/值)")
|
||||
@JsonProperty("custom_metadata")
|
||||
private Map<String, Object> customMetadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新元数据请求参数
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "批量更新元数据请求参数")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class MetaBatchReq implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "筛选器: 用于指定要更新的文档范围 (默认全部)")
|
||||
private Selector selector;
|
||||
|
||||
@Schema(description = "新增或更新的元数据列表")
|
||||
private List<UpdateItem> updates;
|
||||
|
||||
@Schema(description = "需要删除的元数据键列表")
|
||||
private List<DeleteItem> deletes;
|
||||
|
||||
/**
|
||||
* 文档筛选器
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "元数据更新筛选器")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class Selector implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "指定文档 ID 列表")
|
||||
@JsonProperty("document_ids")
|
||||
private List<String> documentIds;
|
||||
|
||||
@Schema(description = "元数据条件匹配 (key: 字段名, value: 匹配值)")
|
||||
@JsonProperty("metadata_condition")
|
||||
private Map<String, Object> metadataCondition;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新项
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "元数据更新项")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class UpdateItem implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "元数据键名", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private String key;
|
||||
|
||||
@Schema(description = "元数据值", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private Object value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除项
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "元数据删除项")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class DeleteItem implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "需删除的元数据键名", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private String key;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 召回测试结果聚合响应
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "召回测试结果聚合响应")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class ResultVO implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "检索命中的切片列表")
|
||||
private List<HitVO> chunks;
|
||||
|
||||
@Schema(description = "文档分布统计")
|
||||
@JsonProperty("doc_aggs")
|
||||
private List<DocAggVO> docAggs;
|
||||
|
||||
@Schema(description = "总命中记录数")
|
||||
private Long total;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,363 @@
|
||||
package xiaozhi.modules.knowledge.dto.file;
|
||||
|
||||
import lombok.*;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import jakarta.validation.constraints.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
/**
|
||||
* 文件管理聚合 DTO
|
||||
* <p>
|
||||
* 容器类,内含文件模块所有请求/响应对象的静态内部类定义。
|
||||
* </p>
|
||||
*/
|
||||
@Schema(description = "文件管理聚合 DTO")
|
||||
public class FileDTO {
|
||||
|
||||
// ========== 请求类 ==========
|
||||
|
||||
/**
|
||||
* 文件上传请求 (对应接口 1: upload)
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "文件上传请求")
|
||||
public static class UploadReq implements Serializable {
|
||||
|
||||
@NotNull(message = "文件不能为空")
|
||||
@Schema(description = "上传的文件", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private MultipartFile file;
|
||||
|
||||
@Schema(description = "父文件夹 ID (为空则上传到根目录)", example = "folder_001")
|
||||
@JsonProperty("parent_id")
|
||||
private String parentId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 新建文件夹请求 (对应接口 2: create)
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "新建文件夹请求")
|
||||
public static class CreateReq implements Serializable {
|
||||
|
||||
@NotBlank(message = "文件夹名称不能为空")
|
||||
@Schema(description = "文件夹名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "新建文件夹")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "父文件夹 ID (为空则创建在根目录)", example = "folder_001")
|
||||
@JsonProperty("parent_id")
|
||||
private String parentId;
|
||||
|
||||
@NotBlank(message = "类型不能为空")
|
||||
@Schema(description = "类型: FOLDER", requiredMode = Schema.RequiredMode.REQUIRED, example = "FOLDER")
|
||||
@Builder.Default
|
||||
private String type = "FOLDER";
|
||||
}
|
||||
|
||||
/**
|
||||
* 重命名请求 (对应接口 6: rename)
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "重命名请求")
|
||||
public static class RenameReq implements Serializable {
|
||||
|
||||
@NotBlank(message = "文件 ID 不能为空")
|
||||
@Schema(description = "文件/文件夹 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "file_001")
|
||||
@JsonProperty("file_id")
|
||||
private String fileId;
|
||||
|
||||
@NotBlank(message = "新名称不能为空")
|
||||
@Schema(description = "新名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "重命名后的文件")
|
||||
private String name;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移动请求 (对应接口 7: move)
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "移动请求")
|
||||
public static class MoveReq implements Serializable {
|
||||
|
||||
@NotEmpty(message = "源文件 ID 列表不能为空")
|
||||
@Schema(description = "源文件/文件夹 ID 列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "[\"file_001\", \"file_002\"]")
|
||||
@JsonProperty("src_file_ids")
|
||||
private List<String> srcFileIds;
|
||||
|
||||
@NotBlank(message = "目标文件夹 ID 不能为空")
|
||||
@Schema(description = "目标文件夹 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "folder_002")
|
||||
@JsonProperty("dest_file_id")
|
||||
private String destFileId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除请求 (对应接口 8: rm)
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "批量删除请求")
|
||||
public static class RemoveReq implements Serializable {
|
||||
|
||||
@NotEmpty(message = "文件 ID 列表不能为空")
|
||||
@Schema(description = "文件/文件夹 ID 列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "[\"file_001\", \"file_002\"]")
|
||||
@JsonProperty("file_ids")
|
||||
private List<String> fileIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入知识库请求 (对应接口 9: convert)
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "导入知识库请求")
|
||||
public static class ConvertReq implements Serializable {
|
||||
|
||||
@NotEmpty(message = "文件 ID 列表不能为空")
|
||||
@Schema(description = "文件 ID 列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "[\"file_001\", \"file_002\"]")
|
||||
@JsonProperty("file_ids")
|
||||
private List<String> fileIds;
|
||||
|
||||
@NotEmpty(message = "知识库 ID 列表不能为空")
|
||||
@Schema(description = "目标知识库 ID 列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "[\"kb_001\"]")
|
||||
@JsonProperty("kb_ids")
|
||||
private List<String> kbIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表查询请求 (对应接口 3: list_files)
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "列表查询请求")
|
||||
public static class ListReq implements Serializable {
|
||||
|
||||
@Schema(description = "父文件夹 ID (为空则查询根目录)", example = "folder_001")
|
||||
@JsonProperty("parent_id")
|
||||
private String parentId;
|
||||
|
||||
@Schema(description = "关键词搜索", example = "文档")
|
||||
private String keywords;
|
||||
|
||||
@Schema(description = "页码 (从 1 开始)", example = "1")
|
||||
private Integer page;
|
||||
|
||||
@Schema(description = "每页数量", example = "30")
|
||||
@JsonProperty("page_size")
|
||||
private Integer pageSize;
|
||||
|
||||
@Schema(description = "排序字段: create_time / update_time / name / size", example = "create_time")
|
||||
private String orderby;
|
||||
|
||||
@Schema(description = "是否降序", example = "true")
|
||||
private Boolean desc;
|
||||
}
|
||||
|
||||
// ========== 响应类 ==========
|
||||
|
||||
/**
|
||||
* 文件/文件夹基础信息 VO
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "文件/文件夹基础信息")
|
||||
public static class InfoVO implements Serializable {
|
||||
|
||||
@Schema(description = "文件/文件夹 ID", example = "file_001")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "父文件夹 ID", example = "folder_001")
|
||||
@JsonProperty("parent_id")
|
||||
private String parentId;
|
||||
|
||||
@Schema(description = "租户 ID", example = "tenant_001")
|
||||
@JsonProperty("tenant_id")
|
||||
private String tenantId;
|
||||
|
||||
@Schema(description = "创建者 ID", example = "user_001")
|
||||
@JsonProperty("created_by")
|
||||
private String createdBy;
|
||||
|
||||
@Schema(description = "类型: FOLDER / FILE", example = "FOLDER")
|
||||
private String type;
|
||||
|
||||
@Schema(description = "名称", example = "我的文件夹")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "路径位置", example = "/root/folder")
|
||||
private String location;
|
||||
|
||||
@Schema(description = "文件大小 (字节)", example = "1024")
|
||||
private Long size;
|
||||
|
||||
@Schema(description = "来源类型", example = "local")
|
||||
@JsonProperty("source_type")
|
||||
private String sourceType;
|
||||
|
||||
@Schema(description = "创建时间 (时间戳)", example = "1700000000000")
|
||||
@JsonProperty("create_time")
|
||||
private Long createTime;
|
||||
|
||||
@Schema(description = "创建日期 (格式化)", example = "2024-01-15 10:30:00")
|
||||
@JsonProperty("create_date")
|
||||
private String createDate;
|
||||
|
||||
@Schema(description = "更新时间 (时间戳)", example = "1700000001000")
|
||||
@JsonProperty("update_time")
|
||||
private Long updateTime;
|
||||
|
||||
@Schema(description = "更新日期 (格式化)", example = "2024-01-15 11:00:00")
|
||||
@JsonProperty("update_date")
|
||||
private String updateDate;
|
||||
|
||||
@Schema(description = "文件扩展名", example = "pdf")
|
||||
private String extension;
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表响应 VO (对应接口 3: list_files)
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "文件列表响应")
|
||||
public static class ListVO implements Serializable {
|
||||
|
||||
@Schema(description = "总记录数", example = "100")
|
||||
private Long total;
|
||||
|
||||
@Schema(description = "当前父文件夹信息")
|
||||
@JsonProperty("parent_folder")
|
||||
private InfoVO parentFolder;
|
||||
|
||||
@Schema(description = "文件/文件夹列表")
|
||||
private List<InfoVO> files;
|
||||
|
||||
@Schema(description = "面包屑导航路径")
|
||||
private List<InfoVO> breadcrumb;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换结果项 VO (对应接口 9: convert)
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "文件转换结果项")
|
||||
public static class ConvertVO implements Serializable {
|
||||
|
||||
@Schema(description = "转换记录 ID", example = "convert_001")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "源文件 ID", example = "file_001")
|
||||
@JsonProperty("file_id")
|
||||
private String fileId;
|
||||
|
||||
@Schema(description = "目标文档 ID", example = "doc_001")
|
||||
@JsonProperty("document_id")
|
||||
private String documentId;
|
||||
|
||||
@Schema(description = "创建时间 (时间戳)", example = "1700000000000")
|
||||
@JsonProperty("create_time")
|
||||
private Long createTime;
|
||||
|
||||
@Schema(description = "创建日期 (格式化)", example = "2024-01-15 10:30:00")
|
||||
@JsonProperty("create_date")
|
||||
private String createDate;
|
||||
|
||||
@Schema(description = "更新时间 (时间戳)", example = "1700000001000")
|
||||
@JsonProperty("update_time")
|
||||
private Long updateTime;
|
||||
|
||||
@Schema(description = "更新日期 (格式化)", example = "2024-01-15 11:00:00")
|
||||
@JsonProperty("update_date")
|
||||
private String updateDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换状态 VO (对应接口 10: get_convert_status)
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "文件转换状态")
|
||||
public static class ConvertStatusVO implements Serializable {
|
||||
|
||||
@Schema(description = "转换状态: pending / processing / completed / failed", example = "completed")
|
||||
private String status;
|
||||
|
||||
@Schema(description = "转换进度 (0.0 - 1.0)", example = "1.0")
|
||||
private Float progress;
|
||||
|
||||
@Schema(description = "状态消息", example = "转换完成")
|
||||
private String message;
|
||||
}
|
||||
|
||||
/**
|
||||
* 面包屑 VO (对应接口 12: all_parent_folder)
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "面包屑导航 (所有父文件夹)")
|
||||
public static class BreadcrumbVO implements Serializable {
|
||||
|
||||
@Schema(description = "父文件夹列表 (从根到当前的路径)")
|
||||
@JsonProperty("parent_folders")
|
||||
private List<InfoVO> parentFolders;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根目录信息 VO (对应接口 10: get_root_folder)
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "根目录信息")
|
||||
public static class RootFolderVO implements Serializable {
|
||||
|
||||
@Schema(description = "根文件夹信息")
|
||||
@JsonProperty("root_folder")
|
||||
private InfoVO rootFolder;
|
||||
}
|
||||
|
||||
/**
|
||||
* 父目录信息 VO (对应接口 11: get_parent_folder)
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "父目录信息")
|
||||
public static class ParentFolderVO implements Serializable {
|
||||
|
||||
@Schema(description = "父文件夹信息")
|
||||
@JsonProperty("parent_folder")
|
||||
private InfoVO parentFolder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package xiaozhi.modules.knowledge.entity;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 文档表 (Shadow DB for RAGFlow Documents)
|
||||
* 对应表名: ai_knowledge_document
|
||||
*/
|
||||
@Data
|
||||
@TableName(value = "ai_rag_knowledge_document", autoResultMap = true)
|
||||
@Schema(description = "知识库文档表")
|
||||
public class DocumentEntity implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(type = IdType.ASSIGN_UUID)
|
||||
@Schema(description = "本地唯一ID")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "知识库ID (关联 ai_rag_dataset.dataset_id)")
|
||||
private String datasetId;
|
||||
|
||||
@Schema(description = "RAGFlow文档ID (远程ID)")
|
||||
private String documentId;
|
||||
|
||||
@Schema(description = "文档名称")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "文件大小(Bytes)")
|
||||
private Long size;
|
||||
|
||||
@Schema(description = "文件类型(pdf/doc/txt等)")
|
||||
private String type;
|
||||
|
||||
@Schema(description = "分块方法")
|
||||
private String chunkMethod;
|
||||
|
||||
@Schema(description = "解析配置(JSON String)")
|
||||
private String parserConfig;
|
||||
|
||||
@Schema(description = "可用状态 (1: 启用/正常, 0: 禁用/失效)")
|
||||
private String status;
|
||||
|
||||
@Schema(description = "运行状态 (UNSTART/RUNNING/CANCEL/DONE/FAIL)")
|
||||
private String run;
|
||||
|
||||
@Schema(description = "解析进度 (0.0 ~ 1.0)")
|
||||
private Double progress;
|
||||
|
||||
@Schema(description = "缩略图 (Base64 或 URL)")
|
||||
private String thumbnail;
|
||||
|
||||
@Schema(description = "解析耗时 (单位: 秒)")
|
||||
private Double processDuration;
|
||||
|
||||
@Schema(description = "自定义元数据 (JSON 格式)")
|
||||
private String metaFields;
|
||||
|
||||
@Schema(description = "来源类型 (local, s3, url 等)")
|
||||
private String sourceType;
|
||||
|
||||
@Schema(description = "解析错误信息")
|
||||
private String error;
|
||||
|
||||
@Schema(description = "分块数量")
|
||||
private Integer chunkCount;
|
||||
|
||||
@Schema(description = "Token数量")
|
||||
private Long tokenCount;
|
||||
|
||||
@Schema(description = "是否启用 (0:禁用 1:启用)")
|
||||
private Integer enabled;
|
||||
|
||||
@Schema(description = "创建者")
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private Long creator;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private Date createdAt;
|
||||
|
||||
@Schema(description = "更新时间")
|
||||
@TableField(fill = FieldFill.UPDATE)
|
||||
private Date updatedAt;
|
||||
|
||||
@Schema(description = "最新同步时间")
|
||||
private Date lastSyncAt;
|
||||
}
|
||||
+29
-1
@@ -23,15 +23,43 @@ public class KnowledgeBaseEntity {
|
||||
@Schema(description = "知识库ID")
|
||||
private String datasetId;
|
||||
|
||||
@Schema(description = "RAG模型配置ID")
|
||||
// @Deprecated
|
||||
@Schema(description = "RAG模型配置ID (连接RAGFlow的凭证指针)")
|
||||
private String ragModelId;
|
||||
|
||||
@Schema(description = "租户ID")
|
||||
private String tenantId;
|
||||
|
||||
@Schema(description = "知识库名称")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "知识库头像(Base64)")
|
||||
private String avatar;
|
||||
|
||||
@Schema(description = "知识库描述")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "嵌入模型名称")
|
||||
private String embeddingModel;
|
||||
|
||||
@Schema(description = "权限设置: me/team")
|
||||
private String permission;
|
||||
|
||||
@Schema(description = "分块方法")
|
||||
private String chunkMethod;
|
||||
|
||||
@Schema(description = "解析器配置(JSON String)")
|
||||
private String parserConfig;
|
||||
|
||||
@Schema(description = "分块总数")
|
||||
private Long chunkCount;
|
||||
|
||||
@Schema(description = "文档总数")
|
||||
private Long documentCount;
|
||||
|
||||
@Schema(description = "总Token数")
|
||||
private Long tokenNum;
|
||||
|
||||
@Schema(description = "状态(0:禁用 1:启用)")
|
||||
private Integer status;
|
||||
|
||||
|
||||
+74
-50
@@ -3,10 +3,14 @@ package xiaozhi.modules.knowledge.rag;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import xiaozhi.modules.knowledge.dto.dataset.DatasetDTO;
|
||||
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.modules.knowledge.dto.KnowledgeFilesDTO;
|
||||
import xiaozhi.modules.knowledge.dto.document.DocumentDTO;
|
||||
import xiaozhi.modules.knowledge.dto.document.ChunkDTO;
|
||||
import xiaozhi.modules.knowledge.dto.document.RetrievalDTO;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* 知识库API适配器抽象基类
|
||||
@@ -46,35 +50,24 @@ public abstract class KnowledgeBaseAdapter {
|
||||
* @return 分页数据
|
||||
*/
|
||||
public abstract PageData<KnowledgeFilesDTO> getDocumentList(String datasetId,
|
||||
Map<String, Object> queryParams,
|
||||
Integer page,
|
||||
Integer limit);
|
||||
DocumentDTO.ListReq req);
|
||||
|
||||
/**
|
||||
* 根据文档ID获取文档详情
|
||||
*
|
||||
* @param datasetId 知识库ID
|
||||
* @return 文档详情
|
||||
* @param datasetId 知识库ID
|
||||
* @param documentId 文档ID
|
||||
* @return 文档详情 (强类型 InfoVO)
|
||||
*/
|
||||
public abstract KnowledgeFilesDTO getDocumentById(String datasetId, String documentId);
|
||||
public abstract DocumentDTO.InfoVO getDocumentById(String datasetId, String documentId);
|
||||
|
||||
/**
|
||||
* 上传文档到知识库
|
||||
*
|
||||
* @param datasetId 知识库ID
|
||||
* @param file 上传的文件
|
||||
* @param name 文档名称
|
||||
* @param metaFields 元数据字段
|
||||
* @param chunkMethod 分块方法
|
||||
* @param parserConfig 解析器配置
|
||||
* @param req 上传请求参数
|
||||
* @return 上传的文档信息
|
||||
*/
|
||||
public abstract KnowledgeFilesDTO uploadDocument(String datasetId,
|
||||
MultipartFile file,
|
||||
String name,
|
||||
Map<String, Object> metaFields,
|
||||
String chunkMethod,
|
||||
Map<String, Object> parserConfig);
|
||||
public abstract KnowledgeFilesDTO uploadDocument(DocumentDTO.UploadReq req);
|
||||
|
||||
/**
|
||||
* 根据状态分页查询文档列表
|
||||
@@ -91,12 +84,12 @@ public abstract class KnowledgeBaseAdapter {
|
||||
Integer limit);
|
||||
|
||||
/**
|
||||
* 删除文档
|
||||
* 删除文档 (支持批量删除)
|
||||
*
|
||||
* @param datasetId 知识库ID
|
||||
* @param documentId 文档ID
|
||||
* @param datasetId 知识库ID
|
||||
* @param req 包含文档ID列表的请求对象
|
||||
*/
|
||||
public abstract void deleteDocument(String datasetId, String documentId);
|
||||
public abstract void deleteDocument(String datasetId, DocumentDTO.BatchIdReq req);
|
||||
|
||||
/**
|
||||
* 解析文档(切块)
|
||||
@@ -112,32 +105,21 @@ public abstract class KnowledgeBaseAdapter {
|
||||
*
|
||||
* @param datasetId 知识库ID
|
||||
* @param documentId 文档ID
|
||||
* @param keywords 关键词过滤
|
||||
* @param page 页码
|
||||
* @param pageSize 每页数量
|
||||
* @param chunkId 切片ID
|
||||
* @return 切片列表信息
|
||||
* @param req 列表请求参数 (分页、关键词等)
|
||||
* @return 切片列表VO
|
||||
*/
|
||||
public abstract Map<String, Object> listChunks(String datasetId,
|
||||
public abstract ChunkDTO.ListVO listChunks(String datasetId,
|
||||
String documentId,
|
||||
String keywords,
|
||||
Integer page,
|
||||
Integer pageSize,
|
||||
String chunkId);
|
||||
ChunkDTO.ListReq req);
|
||||
|
||||
/**
|
||||
* 召回测试 - 从知识库中检索相关切片
|
||||
*
|
||||
* @param question 用户查询
|
||||
* @param datasetIds 数据集ID列表
|
||||
* @param documentIds 文档ID列表
|
||||
* @param retrievalParams 检索参数
|
||||
* @param req 检索测试请求参数
|
||||
* @return 召回测试结果
|
||||
*/
|
||||
public abstract Map<String, Object> retrievalTest(String question,
|
||||
List<String> datasetIds,
|
||||
List<String> documentIds,
|
||||
Map<String, Object> retrievalParams);
|
||||
public abstract RetrievalDTO.ResultVO retrievalTest(
|
||||
RetrievalDTO.TestReq req);
|
||||
|
||||
/**
|
||||
* 测试连接
|
||||
@@ -170,31 +152,73 @@ public abstract class KnowledgeBaseAdapter {
|
||||
/**
|
||||
* 创建数据集
|
||||
*
|
||||
* @param createParams 创建参数
|
||||
* @return 数据集ID
|
||||
* @param req 创建参数
|
||||
* @return 数据集详情
|
||||
*/
|
||||
public abstract String createDataset(Map<String, Object> createParams);
|
||||
public abstract DatasetDTO.InfoVO createDataset(DatasetDTO.CreateReq req);
|
||||
|
||||
/**
|
||||
* 更新数据集
|
||||
*
|
||||
* @param datasetId 数据集ID
|
||||
* @param updateParams 更新参数
|
||||
* @param datasetId 数据集ID
|
||||
* @param req 更新参数
|
||||
* @return 数据集详情
|
||||
*/
|
||||
public abstract void updateDataset(String datasetId, Map<String, Object> updateParams);
|
||||
public abstract DatasetDTO.InfoVO updateDataset(String datasetId, DatasetDTO.UpdateReq req);
|
||||
|
||||
/**
|
||||
* 删除数据集
|
||||
*
|
||||
* @param datasetId 数据集ID
|
||||
* @param req 删除请求参数(包含ID列表)
|
||||
* @return 批量操作结果
|
||||
*/
|
||||
public abstract void deleteDataset(String datasetId);
|
||||
public abstract DatasetDTO.BatchOperationVO deleteDataset(DatasetDTO.BatchIdReq req);
|
||||
|
||||
/**
|
||||
* 获取数据集的文档数量
|
||||
*
|
||||
*
|
||||
* @param datasetId 数据集ID
|
||||
* @return 文档数量
|
||||
*/
|
||||
public abstract Integer getDocumentCount(String datasetId);
|
||||
|
||||
/**
|
||||
* 获取数据集完整信息(名称、简介、文档数量等)
|
||||
* 用于检测 RAGFlow 端是否已删除、同步名称/简介变更
|
||||
*
|
||||
* @param datasetId 数据集ID
|
||||
* @return 数据集详情,若 RAGFlow 端不存在则返回 null
|
||||
*/
|
||||
public abstract DatasetDTO.InfoVO getDatasetInfo(String datasetId);
|
||||
|
||||
/**
|
||||
* 发送流式请求 (SSE)
|
||||
*
|
||||
* @param endpoint API端点
|
||||
* @param body 请求体
|
||||
* @param onData 数据回调
|
||||
*/
|
||||
public abstract void postStream(String endpoint, Object body, Consumer<String> onData);
|
||||
|
||||
/**
|
||||
* SearchBot 提问
|
||||
*
|
||||
* @param config RAG配置
|
||||
* @param body 请求体
|
||||
* @param onData 数据回调
|
||||
* @return 响应对象
|
||||
*/
|
||||
public abstract Object postSearchBotAsk(Map<String, Object> config, Object body,
|
||||
Consumer<String> onData);
|
||||
|
||||
/**
|
||||
* AgentBot 对话
|
||||
*
|
||||
* @param config RAG配置
|
||||
* @param agentId Agent ID
|
||||
* @param body 请求体
|
||||
* @param onData 数据回调
|
||||
*/
|
||||
public abstract void postAgentBotCompletion(Map<String, Object> config, String agentId, Object body,
|
||||
Consumer<String> onData);
|
||||
}
|
||||
+10
-1
@@ -22,6 +22,9 @@ public class KnowledgeBaseAdapterFactory {
|
||||
// 适配器实例缓存
|
||||
private static final Map<String, KnowledgeBaseAdapter> adapterCache = new ConcurrentHashMap<>();
|
||||
|
||||
// 最大缓存实例数,防止内存泄露 (Issue 9)
|
||||
private static final int MAX_CACHE_SIZE = 50;
|
||||
|
||||
static {
|
||||
// 注册内置适配器类型
|
||||
registerAdapter("ragflow", xiaozhi.modules.knowledge.rag.impl.RAGFlowAdapter.class);
|
||||
@@ -61,7 +64,13 @@ public class KnowledgeBaseAdapterFactory {
|
||||
// 创建新的适配器实例
|
||||
KnowledgeBaseAdapter adapter = createAdapter(adapterType, config);
|
||||
|
||||
// 缓存适配器实例
|
||||
// 缓存适配器实例 (带容量限制检查)
|
||||
if (adapterCache.size() >= MAX_CACHE_SIZE) {
|
||||
log.warn("适配器缓存已达上限 ({}),执行内存保护性清除", MAX_CACHE_SIZE);
|
||||
// 简单处理:直接清空,生产环境下建议使用 LRU
|
||||
adapterCache.clear();
|
||||
}
|
||||
|
||||
adapterCache.put(cacheKey, adapter);
|
||||
log.info("创建并缓存适配器实例: {}", cacheKey);
|
||||
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
package xiaozhi.modules.knowledge.rag;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.boot.web.client.RestTemplateBuilder;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.TimeZone;
|
||||
import java.util.Locale;
|
||||
import java.net.URLEncoder;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.net.URI;
|
||||
import java.io.OutputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import xiaozhi.common.exception.ErrorCode;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
|
||||
/**
|
||||
* RAGFlow HTTP Client
|
||||
* 统一处理HTTP通信、鉴权、超时与错误解析
|
||||
*/
|
||||
@Slf4j
|
||||
public class RAGFlowClient {
|
||||
|
||||
private final String baseUrl;
|
||||
private final String apiKey;
|
||||
private final RestTemplate restTemplate;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
// 默认超时时间 (秒)
|
||||
private static final int DEFAULT_TIMEOUT = 30;
|
||||
|
||||
public RAGFlowClient(String baseUrl, String apiKey) {
|
||||
this(baseUrl, apiKey, DEFAULT_TIMEOUT);
|
||||
}
|
||||
|
||||
public RAGFlowClient(String baseUrl, String apiKey, int timeoutSeconds) {
|
||||
this.baseUrl = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl;
|
||||
this.apiKey = apiKey;
|
||||
this.objectMapper = new ObjectMapper();
|
||||
// [Reinforce] 兼容 RAGFlow 返回的 RFC 1123 日期格式 (如: Tue, 10 Feb 2026 10:27:35 GMT)
|
||||
this.objectMapper
|
||||
.setDateFormat(new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss 'GMT'", Locale.US));
|
||||
this.objectMapper.setTimeZone(TimeZone.getTimeZone("GMT"));
|
||||
|
||||
// 优先从 Spring 上下文中获取池化的 RestTemplate Bean (Issue 3: 连接池化)
|
||||
RestTemplate pooledTemplate = null;
|
||||
try {
|
||||
pooledTemplate = xiaozhi.common.utils.SpringContextUtils.getBean(RestTemplate.class);
|
||||
} catch (Exception e) {
|
||||
log.warn("无法从 SpringContext 获取池化 RestTemplate,将退化为简单连接模式: {}", e.getMessage());
|
||||
}
|
||||
|
||||
if (false) { // Force new RestTemplate for debugging
|
||||
this.restTemplate = pooledTemplate;
|
||||
log.debug("RAGFlowClient 已成功挂载全局池化 RestTemplate");
|
||||
} else {
|
||||
// 兜底方案:配置超时并创建简单 RestTemplate
|
||||
log.info("RAGFlowClient 初始化: 使用独立 RestTemplate (Debug Mode)");
|
||||
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
|
||||
factory.setConnectTimeout(timeoutSeconds * 1000);
|
||||
factory.setReadTimeout(timeoutSeconds * 1000);
|
||||
this.restTemplate = new RestTemplate(factory);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 GET 请求
|
||||
*/
|
||||
public Map<String, Object> get(String endpoint, Map<String, Object> queryParams) {
|
||||
String url = buildUrl(endpoint, queryParams);
|
||||
log.debug("GET {}", url);
|
||||
return execute(url, HttpMethod.GET, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 POST 请求 (JSON)
|
||||
*/
|
||||
public Map<String, Object> post(String endpoint, Object body) {
|
||||
String url = buildUrl(endpoint, null);
|
||||
log.info("RAGFlow Client POST Request: URL={}, BodyType={}", url,
|
||||
body != null ? body.getClass().getName() : "null");
|
||||
try {
|
||||
return execute(url, HttpMethod.POST, body);
|
||||
} catch (Exception e) {
|
||||
log.error("RAGFlow Client POST Failed: URL={}", url, e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 DELETE 请求
|
||||
*/
|
||||
public Map<String, Object> delete(String endpoint, Object body) {
|
||||
String url = buildUrl(endpoint, null);
|
||||
log.debug("DELETE {}", url);
|
||||
return execute(url, HttpMethod.DELETE, body);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 PUT 请求
|
||||
*/
|
||||
public Map<String, Object> put(String endpoint, Object body) {
|
||||
String url = buildUrl(endpoint, null);
|
||||
log.debug("PUT {}", url);
|
||||
return execute(url, HttpMethod.PUT, body);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 Multipart 请求 (文件上传)
|
||||
*/
|
||||
public Map<String, Object> postMultipart(String endpoint, MultiValueMap<String, Object> parts) {
|
||||
String url = buildUrl(endpoint, null);
|
||||
log.debug("POST MULTIPART {}", url);
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
|
||||
headers.setBearerAuth(apiKey);
|
||||
// 为了防止中文文件名乱码,某些环境可能需要设置 Charset,但在 Multipart 中通常由 Part header 控制
|
||||
|
||||
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(parts, headers);
|
||||
|
||||
return doExecute(url, HttpMethod.POST, requestEntity);
|
||||
}
|
||||
|
||||
private Map<String, Object> execute(String url, HttpMethod method, Object body) {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.setBearerAuth(apiKey);
|
||||
// 强制 UTF-8
|
||||
headers.setAcceptCharset(Collections.singletonList(StandardCharsets.UTF_8));
|
||||
|
||||
HttpEntity<Object> requestEntity = new HttpEntity<>(body, headers);
|
||||
return doExecute(url, method, requestEntity);
|
||||
}
|
||||
|
||||
private Map<String, Object> doExecute(String url, HttpMethod method, HttpEntity<?> requestEntity) {
|
||||
try {
|
||||
ResponseEntity<String> response = restTemplate.exchange(url, method, requestEntity, String.class);
|
||||
|
||||
if (!response.getStatusCode().is2xxSuccessful()) {
|
||||
log.error("RAGFlow API Error Status: {}", response.getStatusCode());
|
||||
throw new RenException(ErrorCode.RAG_API_ERROR, "HTTP " + response.getStatusCode());
|
||||
}
|
||||
|
||||
String responseBody = response.getBody();
|
||||
if (responseBody == null) {
|
||||
throw new RenException(ErrorCode.RAG_API_ERROR, "Empty Response");
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> map = objectMapper.readValue(responseBody, Map.class);
|
||||
|
||||
Integer code = (Integer) map.get("code");
|
||||
if (code != null && code != 0) {
|
||||
String msg = (String) map.get("message");
|
||||
log.error("RAGFlow Business Error: code={}, msg={}", code, msg);
|
||||
throw new RenException(ErrorCode.RAG_API_ERROR, msg != null ? msg : "Unknown RAGFlow Error");
|
||||
}
|
||||
|
||||
// 返回 data 字段,如果 data 不存在则返回整个 map (视具体情况,通常 RAGFlow 返回 code=0, data=...)
|
||||
// 兼容性处理:如果 external caller 需要 check code,这里已经 check 过了。
|
||||
// 统一返回 wrap 了 code 的 map 还是只返回 data?
|
||||
// 根据分析报告,旧逻辑 check code==0 后取 data.
|
||||
// 这里我们返回整个 Map,让 Adapter 决定怎么取,或者我们直接在这里剥离?
|
||||
// 建议:为了灵活性,返回全量 Map,但在 Client 层做 code!=0 的抛错。
|
||||
return map;
|
||||
|
||||
} catch (RenException re) {
|
||||
throw re;
|
||||
} catch (Exception e) {
|
||||
log.error("RAGFlow Client Execute Error! URL: {}, Method: {}, Body Type: {}", url, method,
|
||||
requestEntity.getBody() != null ? requestEntity.getBody().getClass().getName() : "null");
|
||||
log.error("Full exception stack trace: ", e);
|
||||
throw new RenException(ErrorCode.RAG_API_ERROR, "Request Failed: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private String buildUrl(String endpoint, Map<String, Object> queryParams) {
|
||||
StringBuilder sb = new StringBuilder(baseUrl);
|
||||
if (!endpoint.startsWith("/")) {
|
||||
sb.append("/");
|
||||
}
|
||||
sb.append(endpoint);
|
||||
|
||||
if (queryParams != null && !queryParams.isEmpty()) {
|
||||
sb.append("?");
|
||||
queryParams.forEach((k, v) -> {
|
||||
if (v != null) {
|
||||
try {
|
||||
sb.append(k).append("=")
|
||||
.append(URLEncoder.encode(v.toString(),
|
||||
StandardCharsets.UTF_8.name()))
|
||||
.append("&");
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
log.warn("参数编码失败: k={}, v={}", k, v);
|
||||
sb.append(k).append("=").append(v).append("&");
|
||||
}
|
||||
}
|
||||
});
|
||||
// 移除最后一个 &
|
||||
sb.setLength(sb.length() - 1);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送流式 POST 请求 (SSE)
|
||||
* 使用 Java 21 HttpClient 实现
|
||||
*
|
||||
* @param endpoint API端点
|
||||
* @param body 请求体
|
||||
* @param onData 数据回调(每收到一行数据调用一次)
|
||||
*/
|
||||
public void postStream(String endpoint, Object body, Consumer<String> onData) {
|
||||
try {
|
||||
String url = buildUrl(endpoint, null);
|
||||
log.debug("POST STREAM {}", url);
|
||||
|
||||
String jsonBody = objectMapper.writeValueAsString(body);
|
||||
|
||||
HttpClient httpClient = HttpClient.newBuilder()
|
||||
.connectTimeout(Duration.ofSeconds(DEFAULT_TIMEOUT))
|
||||
.build();
|
||||
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(url))
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Authorization", "Bearer " + apiKey)
|
||||
.POST(HttpRequest.BodyPublishers.ofString(jsonBody, StandardCharsets.UTF_8))
|
||||
.build();
|
||||
|
||||
// 发送请求并处理流式响应
|
||||
httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream())
|
||||
.body()
|
||||
.transferTo(new OutputStream() {
|
||||
private final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
|
||||
|
||||
@Override
|
||||
public void write(int b) throws IOException {
|
||||
if (b == '\n') {
|
||||
String line = buffer.toString(StandardCharsets.UTF_8);
|
||||
if (!line.trim().isEmpty()) {
|
||||
onData.accept(line);
|
||||
}
|
||||
buffer.reset();
|
||||
} else {
|
||||
buffer.write(b);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("RAGFlow Stream Request Error", e);
|
||||
throw new RenException(ErrorCode.RAG_API_ERROR, "Stream Request Failed: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
+463
-893
File diff suppressed because it is too large
Load Diff
+10
@@ -93,4 +93,14 @@ public interface KnowledgeBaseService extends BaseService<KnowledgeBaseEntity> {
|
||||
* @return RAG模型列表
|
||||
*/
|
||||
List<ModelConfigEntity> getRAGModels();
|
||||
|
||||
/**
|
||||
* 更新知识库统计信息 (用于被文件服务回调)
|
||||
*
|
||||
* @param datasetId 知识库ID
|
||||
* @param docDelta 文档数增量
|
||||
* @param chunkDelta 分块数增量
|
||||
* @param tokenDelta Token数增量
|
||||
*/
|
||||
void updateStatistics(String datasetId, Integer docDelta, Long chunkDelta, Long tokenDelta);
|
||||
}
|
||||
+51
-31
@@ -7,6 +7,9 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.modules.knowledge.dto.KnowledgeFilesDTO;
|
||||
import xiaozhi.modules.knowledge.dto.document.ChunkDTO;
|
||||
import xiaozhi.modules.knowledge.dto.document.RetrievalDTO;
|
||||
import xiaozhi.modules.knowledge.dto.document.DocumentDTO;
|
||||
|
||||
/**
|
||||
* 知识库文档服务接口
|
||||
@@ -28,9 +31,9 @@ public interface KnowledgeFilesService {
|
||||
*
|
||||
* @param documentId 文档ID
|
||||
* @param datasetId 知识库ID
|
||||
* @return 文档详情
|
||||
* @return 文档详情 (强类型 InfoVO)
|
||||
*/
|
||||
KnowledgeFilesDTO getByDocumentId(String documentId, String datasetId);
|
||||
DocumentDTO.InfoVO getByDocumentId(String documentId, String datasetId);
|
||||
|
||||
/**
|
||||
* 上传文档到知识库
|
||||
@@ -48,12 +51,12 @@ public interface KnowledgeFilesService {
|
||||
Map<String, Object> parserConfig);
|
||||
|
||||
/**
|
||||
* 根据文档ID和知识库ID删除文档
|
||||
* 批量删除文档
|
||||
*
|
||||
* @param documentId 文档ID
|
||||
* @param datasetId 知识库ID
|
||||
* @param datasetId 知识库ID
|
||||
* @param req 删除请求参数 (含文档ID列表)
|
||||
*/
|
||||
void deleteByDocumentId(String documentId, String datasetId);
|
||||
void deleteDocuments(String datasetId, DocumentDTO.BatchIdReq req);
|
||||
|
||||
/**
|
||||
* 获取RAG配置信息
|
||||
@@ -77,36 +80,53 @@ public interface KnowledgeFilesService {
|
||||
*
|
||||
* @param datasetId 知识库ID
|
||||
* @param documentId 文档ID
|
||||
* @param keywords 关键词过滤
|
||||
* @param page 页码
|
||||
* @param pageSize 每页数量
|
||||
* @param chunkId 切片ID
|
||||
* @param req 切片列表请求参数
|
||||
* @return 切片列表信息
|
||||
*/
|
||||
Map<String, Object> listChunks(String datasetId, String documentId, String keywords,
|
||||
Integer page, Integer pageSize, String chunkId);
|
||||
ChunkDTO.ListVO listChunks(String datasetId, String documentId, ChunkDTO.ListReq req);
|
||||
|
||||
/**
|
||||
* 召回测试 - 从指定数据集或文档中检索相关切片
|
||||
* 召回测试
|
||||
*
|
||||
* @param question 用户查询或查询关键词
|
||||
* @param datasetIds 数据集ID列表
|
||||
* @param documentIds 文档ID列表
|
||||
* @param page 页码
|
||||
* @param pageSize 每页数量
|
||||
* @param similarityThreshold 最小相似度阈值
|
||||
* @param vectorSimilarityWeight 向量相似度权重
|
||||
* @param topK 参与向量余弦计算的切片数量
|
||||
* @param rerankId 重排模型ID
|
||||
* @param keyword 是否启用关键词匹配
|
||||
* @param highlight 是否启用高亮显示
|
||||
* @param crossLanguages 跨语言翻译列表
|
||||
* @param metadataCondition 元数据过滤条件
|
||||
* @param req 检索测试请求参数
|
||||
* @return 召回测试结果
|
||||
*/
|
||||
Map<String, Object> retrievalTest(String question, List<String> datasetIds, List<String> documentIds,
|
||||
Integer page, Integer pageSize, Float similarityThreshold,
|
||||
Float vectorSimilarityWeight, Integer topK, String rerankId,
|
||||
Boolean keyword, Boolean highlight, List<String> crossLanguages,
|
||||
Map<String, Object> metadataCondition);
|
||||
RetrievalDTO.ResultVO retrievalTest(RetrievalDTO.TestReq req);
|
||||
|
||||
/**
|
||||
* 保存文档影子记录
|
||||
*/
|
||||
boolean saveDocumentShadow(String datasetId, KnowledgeFilesDTO result, String originalName, String chunkMethod,
|
||||
Map<String, Object> parserConfig);
|
||||
|
||||
/**
|
||||
* 批量删除文档影子记录并同步统计数据
|
||||
*
|
||||
* @param documentIds 文档ID列表
|
||||
* @param datasetId 数据集ID
|
||||
* @param chunkDelta 待扣减的总分块数
|
||||
* @param tokenDelta 待扣减的总Token数
|
||||
*/
|
||||
void deleteDocumentShadows(List<String> documentIds, String datasetId, Long chunkDelta, Long tokenDelta);
|
||||
|
||||
/**
|
||||
* 根据数据集ID清理所有关联文档 (级联删除专用)
|
||||
*
|
||||
* @param datasetId 数据集ID
|
||||
*/
|
||||
void deleteDocumentsByDatasetId(String datasetId);
|
||||
|
||||
/**
|
||||
* 同步所有处于 RUNNING 状态的文档 (供定时任务调用)
|
||||
*/
|
||||
void syncRunningDocuments();
|
||||
|
||||
/**
|
||||
* 从RAGFlow全量同步文档到本地影子表
|
||||
* 拉取远端所有文档,与本地影子表对比,插入缺失的记录
|
||||
*
|
||||
* @param datasetId 数据集ID
|
||||
* @return 新同步的文档数量
|
||||
*/
|
||||
int syncDocumentsFromRAG(String datasetId);
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package xiaozhi.modules.knowledge.service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 知识库模块领域编排服务
|
||||
* 用于处理跨 KnowledgeBase 和 KnowledgeFiles 的复杂业务流程,彻底解决 Service 间的循环依赖问题。
|
||||
*/
|
||||
public interface KnowledgeManagerService {
|
||||
|
||||
/**
|
||||
* 级联删除知识库及其下属所有文档 (包括本地 DB 和 RAGFlow 远程数据)
|
||||
*
|
||||
* @param datasetId 知识库 ID
|
||||
*/
|
||||
void deleteDatasetWithFiles(String datasetId);
|
||||
|
||||
/**
|
||||
* 批量级联删除知识库
|
||||
*
|
||||
* @param datasetIds 知识库 ID 列表
|
||||
*/
|
||||
void batchDeleteDatasetsWithFiles(List<String> datasetIds);
|
||||
}
|
||||
+366
-513
File diff suppressed because it is too large
Load Diff
+695
-1012
File diff suppressed because it is too large
Load Diff
+47
@@ -0,0 +1,47 @@
|
||||
package xiaozhi.modules.knowledge.service.impl;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import xiaozhi.modules.knowledge.service.KnowledgeBaseService;
|
||||
import xiaozhi.modules.knowledge.service.KnowledgeFilesService;
|
||||
import xiaozhi.modules.knowledge.service.KnowledgeManagerService;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class KnowledgeManagerServiceImpl implements KnowledgeManagerService {
|
||||
|
||||
private final KnowledgeBaseService knowledgeBaseService;
|
||||
private final KnowledgeFilesService knowledgeFilesService;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void deleteDatasetWithFiles(String datasetId) {
|
||||
log.info("=== 级联删除开始: datasetId={} ===", datasetId);
|
||||
|
||||
// 1. 先调用文件服务,清理该数据集下的所有文档记录 (含 RAGFlow 端)
|
||||
log.info("Step 1: 清理关联文档...");
|
||||
knowledgeFilesService.deleteDocumentsByDatasetId(datasetId);
|
||||
|
||||
// 2. 再调用知识库服务,彻底注销数据集 (含 RAGFlow 端)
|
||||
log.info("Step 2: 删除数据集主体...");
|
||||
knowledgeBaseService.deleteByDatasetId(datasetId);
|
||||
|
||||
log.info("=== 级联删除成功: datasetId={} ===", datasetId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void batchDeleteDatasetsWithFiles(List<String> datasetIds) {
|
||||
if (datasetIds == null || datasetIds.isEmpty())
|
||||
return;
|
||||
log.info("=== 批量级联删除开始: count={} ===", datasetIds.size());
|
||||
for (String id : datasetIds) {
|
||||
deleteDatasetWithFiles(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package xiaozhi.modules.knowledge.task;
|
||||
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import xiaozhi.modules.knowledge.service.KnowledgeFilesService;
|
||||
|
||||
/**
|
||||
* 知识库文档状态同步定时任务
|
||||
*
|
||||
* 作用:
|
||||
* 1. 自动扫描处于 "RUNNING" (解析中) 状态的文档
|
||||
* 2. 调用 RAGFlow 接口获取最新状态
|
||||
* 3. 状态翻转 (RUNNING -> SUCCESS/FAIL) 时,同步更新数据库
|
||||
* 4. [关键] 解析成功时,补偿更新知识库的统计信息 (TokenCount)
|
||||
*/
|
||||
@Component
|
||||
@AllArgsConstructor
|
||||
@Slf4j
|
||||
public class DocumentStatusSyncTask {
|
||||
|
||||
private final KnowledgeFilesService knowledgeFilesService;
|
||||
|
||||
/**
|
||||
* 每 30 秒执行一次同步
|
||||
* 采用 fixedDelay,确保上一次执行完 30 秒后才开始下一次,防止积压
|
||||
*/
|
||||
@Scheduled(fixedDelay = 30000)
|
||||
public void syncRunningDocuments() {
|
||||
try {
|
||||
// log.debug("开始执行文档状态同步任务...");
|
||||
knowledgeFilesService.syncRunningDocuments();
|
||||
} catch (Exception e) {
|
||||
log.error("文档状态同步任务异常", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -67,4 +67,13 @@ public interface LLMService {
|
||||
* @return 是否可用
|
||||
*/
|
||||
boolean isAvailable(String modelId);
|
||||
|
||||
/**
|
||||
* 生成会话标题
|
||||
*
|
||||
* @param conversation 对话内容
|
||||
* @param modelId 模型ID
|
||||
* @return 标题(约15字)
|
||||
*/
|
||||
String generateTitle(String conversation, String modelId);
|
||||
}
|
||||
+122
-2
@@ -1,6 +1,7 @@
|
||||
package xiaozhi.modules.llm.service.impl;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -30,13 +31,38 @@ import xiaozhi.modules.model.service.ModelConfigService;
|
||||
@Service
|
||||
public class OpenAIStyleLLMServiceImpl implements LLMService {
|
||||
|
||||
// 需要禁用思考模式的平台域名及其对应参数
|
||||
private static final Map<String, Map<String, Object>> THINKING_DISABLED_DOMAINS = new LinkedHashMap<>();
|
||||
static {
|
||||
THINKING_DISABLED_DOMAINS.put("aliyuncs.com", Map.of("enable_thinking", false));
|
||||
Map<String, Object> thinkingDisabled = Map.of("thinking", Map.of("type", "disabled"));
|
||||
THINKING_DISABLED_DOMAINS.put("bigmodel.cn", thinkingDisabled);
|
||||
THINKING_DISABLED_DOMAINS.put("moonshot.cn", thinkingDisabled);
|
||||
THINKING_DISABLED_DOMAINS.put("volces.com", thinkingDisabled);
|
||||
}
|
||||
|
||||
@Autowired
|
||||
private ModelConfigService modelConfigService;
|
||||
|
||||
private final RestTemplate restTemplate = new RestTemplate();
|
||||
|
||||
/**
|
||||
* 根据域名自动禁用思考模式
|
||||
*/
|
||||
private void applyThinkingDisabled(String baseUrl, Map<String, Object> requestBody) {
|
||||
for (Map.Entry<String, Map<String, Object>> entry : THINKING_DISABLED_DOMAINS.entrySet()) {
|
||||
if (baseUrl.contains(entry.getKey())) {
|
||||
requestBody.putAll(entry.getValue());
|
||||
log.info("为域名 {} 禁用思考模式,参数: {}", baseUrl, entry.getValue());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final String DEFAULT_SUMMARY_PROMPT = "你是一个经验丰富的记忆总结者,擅长将对话内容进行总结摘要,遵循以下规则:\n1、总结用户的重要信息,以便在未来的对话中提供更个性化的服务\n2、不要重复总结,不要遗忘之前记忆,除非原来的记忆超过了1800字,否则不要遗忘、不要压缩用户的历史记忆\n3、用户操控的设备音量、播放音乐、天气、退出、不想对话等和用户本身无关的内容,这些信息不需要加入到总结中\n4、聊天内容中的今天的日期时间、今天的天气情况与用户事件无关的数据,这些信息如果当成记忆存储会影响后续对话,这些信息不需要加入到总结中\n5、不要把设备操控的成果结果和失败结果加入到总结中,也不要把用户的一些废话加入到总结中\n6、不要为了总结而总结,如果用户的聊天没有意义,请返回原来的历史记录也是可以的\n7、只需要返回总结摘要,严格控制在1800字内\n8、不要包含代码、xml,不需要解释、注释和说明,保存记忆时仅从对话提取信息,不要混入示例内容\n9、如果提供了历史记忆,请将新对话内容与历史记忆进行智能合并,保留有价值的历史信息,同时添加新的重要信息\n\n历史记忆:\n{history_memory}\n\n新对话内容:\n{conversation}";
|
||||
|
||||
private static final String DEFAULT_TITLE_PROMPT = "请根据以下对话内容,生成一个简洁的会话标题(约15字以内),只返回标题,不要包含任何解释或标点符号:\n{conversation}";
|
||||
|
||||
@Override
|
||||
public String generateSummary(String conversation) {
|
||||
return generateSummary(conversation, null, null);
|
||||
@@ -100,6 +126,9 @@ public class OpenAIStyleLLMServiceImpl implements LLMService {
|
||||
requestBody.put("temperature", temperature != null ? temperature : 0.7);
|
||||
requestBody.put("max_tokens", maxTokens != null ? maxTokens : 2000);
|
||||
|
||||
// 禁用思考模式
|
||||
applyThinkingDisabled(baseUrl, requestBody);
|
||||
|
||||
// 发送HTTP请求
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
@@ -154,10 +183,8 @@ public class OpenAIStyleLLMServiceImpl implements LLMService {
|
||||
// 从智控台获取LLM模型配置
|
||||
ModelConfigEntity llmConfig;
|
||||
if (modelId != null && !modelId.trim().isEmpty()) {
|
||||
// 通过具体模型ID获取配置
|
||||
llmConfig = modelConfigService.getModelByIdFromCache(modelId);
|
||||
} else {
|
||||
// 保持向后兼容,使用默认配置
|
||||
llmConfig = getDefaultLLMConfig();
|
||||
}
|
||||
|
||||
@@ -195,6 +222,9 @@ public class OpenAIStyleLLMServiceImpl implements LLMService {
|
||||
requestBody.put("temperature", 0.2);
|
||||
requestBody.put("max_tokens", 2000);
|
||||
|
||||
// 禁用思考模式
|
||||
applyThinkingDisabled(baseUrl, requestBody);
|
||||
|
||||
// 发送HTTP请求
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
@@ -302,4 +332,94 @@ public class OpenAIStyleLLMServiceImpl implements LLMService {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generateTitle(String conversation, String modelId) {
|
||||
if (!isAvailable()) {
|
||||
log.warn("LLM服务不可用,无法生成标题");
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
ModelConfigEntity llmConfig;
|
||||
if (modelId != null && !modelId.trim().isEmpty()) {
|
||||
llmConfig = modelConfigService.getModelByIdFromCache(modelId);
|
||||
} else {
|
||||
llmConfig = getDefaultLLMConfig();
|
||||
}
|
||||
|
||||
if (llmConfig == null || llmConfig.getConfigJson() == null) {
|
||||
log.error("未找到可用的LLM模型配置,modelId: {}", modelId);
|
||||
return null;
|
||||
}
|
||||
|
||||
JSONObject configJson = llmConfig.getConfigJson();
|
||||
String baseUrl = configJson.getStr("base_url");
|
||||
String model = configJson.getStr("model_name");
|
||||
String apiKey = configJson.getStr("api_key");
|
||||
|
||||
if (StringUtils.isBlank(baseUrl) || StringUtils.isBlank(apiKey)) {
|
||||
log.error("LLM配置不完整,baseUrl或apiKey为空");
|
||||
return null;
|
||||
}
|
||||
|
||||
String prompt = DEFAULT_TITLE_PROMPT.replace("{conversation}", conversation);
|
||||
|
||||
Map<String, Object> requestBody = new HashMap<>();
|
||||
requestBody.put("model", model != null ? model : "gpt-3.5-turbo");
|
||||
|
||||
Map<String, Object>[] messages = new Map[1];
|
||||
Map<String, Object> message = new HashMap<>();
|
||||
message.put("role", "user");
|
||||
message.put("content", prompt);
|
||||
messages[0] = message;
|
||||
|
||||
requestBody.put("messages", messages);
|
||||
requestBody.put("temperature", 0.3);
|
||||
requestBody.put("max_tokens", 50);
|
||||
|
||||
// 禁用思考模式
|
||||
applyThinkingDisabled(baseUrl, requestBody);
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.set("Authorization", "Bearer " + apiKey);
|
||||
|
||||
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(requestBody, headers);
|
||||
|
||||
String apiUrl = baseUrl;
|
||||
if (!apiUrl.endsWith("/chat/completions")) {
|
||||
if (!apiUrl.endsWith("/")) {
|
||||
apiUrl += "/";
|
||||
}
|
||||
apiUrl += "chat/completions";
|
||||
}
|
||||
|
||||
ResponseEntity<String> response = restTemplate.exchange(
|
||||
apiUrl, HttpMethod.POST, entity, String.class);
|
||||
|
||||
if (response.getStatusCode().is2xxSuccessful()) {
|
||||
JSONObject responseJson = JSONUtil.parseObj(response.getBody());
|
||||
JSONArray choices = responseJson.getJSONArray("choices");
|
||||
if (choices != null && choices.size() > 0) {
|
||||
JSONObject choice = choices.getJSONObject(0);
|
||||
JSONObject messageObj = choice.getJSONObject("message");
|
||||
String title = messageObj.getStr("content");
|
||||
if (StringUtils.isNotBlank(title)) {
|
||||
title = title.trim().replaceAll("[,。!?、:;''\"\"【】()]", "");
|
||||
if (title.length() > 15) {
|
||||
title = title.substring(0, 15);
|
||||
}
|
||||
return title;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.error("LLM API调用失败,状态码:{},响应:{}", response.getStatusCode(), response.getBody());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("调用LLM服务生成标题时发生异常,modelId: {}", modelId, e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -129,6 +129,10 @@ public class ModelController {
|
||||
if (entity == null) {
|
||||
return new Result<Void>().error("模型配置不存在");
|
||||
}
|
||||
// 不能关闭默认模型
|
||||
if (status == 0 && entity.getIsDefault() > 0) {
|
||||
return new Result<Void>().error("默认模型配置不允许关闭");
|
||||
}
|
||||
// 不更新ConfigJson字段
|
||||
entity.setConfigJson(null);
|
||||
entity.setIsEnabled(status);
|
||||
|
||||
@@ -23,6 +23,9 @@ public class VoiceDTO implements Serializable {
|
||||
@Schema(description = "音频播放地址")
|
||||
private String voiceDemo;
|
||||
|
||||
@Schema(description = "语言类型")
|
||||
private String languages;
|
||||
|
||||
@Schema(description = "是否为克隆音色")
|
||||
private Boolean isClone;
|
||||
|
||||
@@ -31,6 +34,7 @@ public class VoiceDTO implements Serializable {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.voiceDemo = null;
|
||||
this.languages = null;
|
||||
this.isClone = false; // 默认不是克隆音色
|
||||
}
|
||||
|
||||
@@ -39,6 +43,7 @@ public class VoiceDTO implements Serializable {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.voiceDemo = voiceDemo;
|
||||
this.languages = null;
|
||||
this.isClone = false;
|
||||
}
|
||||
|
||||
|
||||
+16
-1
@@ -57,7 +57,8 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
|
||||
.eq("model_type", modelType)
|
||||
.eq("is_enabled", 1)
|
||||
.like(StringUtils.isNotBlank(modelName), "model_name", modelName)
|
||||
.select("id", "model_name"));
|
||||
.select("id", "model_name")
|
||||
.orderByAsc("sort"));
|
||||
return ConvertUtils.sourceToTarget(entities, ModelBasicInfoDTO.class);
|
||||
}
|
||||
|
||||
@@ -370,6 +371,13 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
|
||||
}
|
||||
}
|
||||
|
||||
// 删除在新JSON中不存在的非敏感字段
|
||||
for (String oldKey : originalJson.keySet().toArray(new String[0])) {
|
||||
if (!modelConfigBodyDTO.getConfigJson().containsKey(oldKey) && !SensitiveDataUtils.isSensitiveField(oldKey)) {
|
||||
updatedJson.remove(oldKey);
|
||||
}
|
||||
}
|
||||
|
||||
modelConfigEntity.setConfigJson(updatedJson);
|
||||
}
|
||||
|
||||
@@ -402,6 +410,13 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 删除在新JSON中不存在的非敏感子字段
|
||||
for (String oldChildKey : originalChild.keySet().toArray(new String[0])) {
|
||||
if (!updated.containsKey(oldChildKey) && !SensitiveDataUtils.isSensitiveField(oldChildKey)) {
|
||||
originalChild.remove(oldChildKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+3
-1
@@ -95,6 +95,7 @@ public class ModelProviderServiceImpl extends BaseServiceImpl<ModelProviderDao,
|
||||
|
||||
QueryWrapper<ModelProviderEntity> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.eq("model_type", StringUtils.isBlank(modelType) ? "" : modelType);
|
||||
queryWrapper.orderByAsc("sort");
|
||||
List<ModelProviderEntity> providerEntities = modelProviderDao.selectList(queryWrapper);
|
||||
return ConvertUtils.sourceToTarget(providerEntities, ModelProviderDTO.class);
|
||||
}
|
||||
@@ -147,7 +148,8 @@ public class ModelProviderServiceImpl extends BaseServiceImpl<ModelProviderDao,
|
||||
UserDetail user = SecurityUser.getUser();
|
||||
modelProviderDTO.setUpdater(user.getId());
|
||||
modelProviderDTO.setUpdateDate(new Date());
|
||||
if (modelProviderDao.updateById(ConvertUtils.sourceToTarget(modelProviderDTO, ModelProviderEntity.class)) == 0) {
|
||||
if (modelProviderDao
|
||||
.updateById(ConvertUtils.sourceToTarget(modelProviderDTO, ModelProviderEntity.class)) == 0) {
|
||||
throw new RenException(ErrorCode.UPDATE_DATA_FAILED);
|
||||
}
|
||||
return ConvertUtils.sourceToTarget(modelProviderDTO, ModelProviderDTO.class);
|
||||
|
||||
@@ -90,6 +90,7 @@ public class ShiroConfig {
|
||||
filterMap.put("/agent/chat-history/report", "server");
|
||||
filterMap.put("/agent/chat-history/download/**", "anon");
|
||||
filterMap.put("/agent/chat-summary/**", "server");
|
||||
filterMap.put("/agent/chat-title/**", "server");
|
||||
filterMap.put("/agent/play/**", "anon");
|
||||
filterMap.put("/voiceClone/play/**", "anon");
|
||||
filterMap.put("/**", "oauth2");
|
||||
|
||||
+5
-5
@@ -56,7 +56,7 @@ public class SysUserServiceImpl extends BaseServiceImpl<SysUserDao, SysUserEntit
|
||||
if (users == null || users.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
SysUserEntity entity = users.getFirst();
|
||||
SysUserEntity entity = users.get(0);
|
||||
return ConvertUtils.sourceToTarget(entity, SysUserDTO.class);
|
||||
}
|
||||
|
||||
@@ -197,7 +197,7 @@ public class SysUserServiceImpl extends BaseServiceImpl<SysUserDao, SysUserEntit
|
||||
*/
|
||||
private String generatePassword() {
|
||||
StringBuilder password = new StringBuilder();
|
||||
|
||||
|
||||
// 确保包含至少一个数字
|
||||
password.append("0123456789".charAt(random.nextInt(10)));
|
||||
// 确保包含至少一个小写字母
|
||||
@@ -206,12 +206,12 @@ public class SysUserServiceImpl extends BaseServiceImpl<SysUserDao, SysUserEntit
|
||||
password.append("ABCDEFGHIJKLMNOPQRSTUVWXYZ".charAt(random.nextInt(26)));
|
||||
// 确保包含至少一个特殊符号
|
||||
password.append("!@#$%^&*()".charAt(random.nextInt(10)));
|
||||
|
||||
|
||||
// 生成剩余的8个字符
|
||||
for (int i = 4; i < 12; i++) {
|
||||
password.append(CHARACTERS.charAt(random.nextInt(CHARACTERS.length())));
|
||||
}
|
||||
|
||||
|
||||
// 打乱密码中字符的顺序
|
||||
char[] passwordChars = password.toString().toCharArray();
|
||||
for (int i = 0; i < passwordChars.length; i++) {
|
||||
@@ -220,7 +220,7 @@ public class SysUserServiceImpl extends BaseServiceImpl<SysUserDao, SysUserEntit
|
||||
passwordChars[i] = passwordChars[randomIndex];
|
||||
passwordChars[randomIndex] = temp;
|
||||
}
|
||||
|
||||
|
||||
return new String(passwordChars);
|
||||
}
|
||||
|
||||
|
||||
+2
@@ -129,6 +129,7 @@ public class TimbreServiceImpl extends BaseServiceImpl<TimbreDao, TimbreEntity>
|
||||
.map(entity -> {
|
||||
VoiceDTO dto = new VoiceDTO(entity.getId(), entity.getName());
|
||||
dto.setVoiceDemo(entity.getVoiceDemo());
|
||||
dto.setLanguages(entity.getLanguages()); // 设置语言类型
|
||||
dto.setIsClone(false); // 设置为普通音色
|
||||
return dto;
|
||||
})
|
||||
@@ -146,6 +147,7 @@ public class TimbreServiceImpl extends BaseServiceImpl<TimbreDao, TimbreEntity>
|
||||
voiceDTO.setName(MessageUtils.getMessage(ErrorCode.VOICE_CLONE_PREFIX) + entity.getName());
|
||||
// 保留从数据库查询到的voiceDemo字段
|
||||
voiceDTO.setVoiceDemo(entity.getVoiceDemo());
|
||||
voiceDTO.setLanguages(entity.getLanguages());
|
||||
voiceDTO.setIsClone(true); // 设置为克隆音色
|
||||
redisUtils.set(RedisKeys.getTimbreNameById(voiceDTO.getId()), voiceDTO.getName(),
|
||||
RedisUtils.NOT_EXPIRE);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user