mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-23 15:43:54 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3510282cd5 | ||
|
|
bdf7c7facf | ||
|
|
fa91c97685 | ||
|
|
a92d6ccdfb | ||
|
|
f5ce755fea | ||
|
|
550397de98 | ||
|
|
ff66b815a5 | ||
|
|
8c18e48b97 | ||
|
|
4acd02e2b0 | ||
|
|
ddfbacc717 | ||
|
|
2a4dbdb69b | ||
|
|
e9a2489e48 | ||
|
|
ce13a7e3e6 | ||
|
|
8a156e99f4 | ||
|
|
4313216816 | ||
|
|
dc7615c008 | ||
|
|
b03f9374ea | ||
|
|
cacfc9bfee | ||
|
|
ce3c8a2bd8 | ||
|
|
e1a7e50e1f | ||
|
|
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 | ||
|
|
d721270bed | ||
|
|
fe2e178b19 | ||
|
|
21337e5046 | ||
|
|
1c8b271e20 | ||
|
|
3d1160b6bb | ||
|
|
086c4a55e6 | ||
|
|
889f2f9a75 | ||
|
|
07b686be14 | ||
|
|
4d6b453485 | ||
|
|
9909045f9d | ||
|
|
b0dc4d88db | ||
|
|
bbf3858735 | ||
|
|
73573e2f12 | ||
|
|
99e136786e | ||
|
|
205be0e4ec | ||
|
|
2e64f45a31 | ||
|
|
818b4f2f6d | ||
|
|
5d72f9f79d | ||
|
|
22d0dcdf58 | ||
|
|
122d1def1d | ||
|
|
0ee349df2d | ||
|
|
4dde66339b | ||
|
|
0064f975c2 | ||
|
|
43234f4798 | ||
|
|
48034fc000 | ||
|
|
3fc33e9095 | ||
|
|
da71dce860 | ||
|
|
b7e4408a0f | ||
|
|
4f4f8ca54e | ||
|
|
99c12a0fed | ||
|
|
dcd0ef3f40 | ||
|
|
2f53b921fe | ||
|
|
0f16a8d31f | ||
|
|
3051dc8327 | ||
|
|
c313997a61 | ||
|
|
07c0c764d3 | ||
|
|
e339a501aa | ||
|
|
f2c541b4ab | ||
|
|
206af1c074 | ||
|
|
344729c9a6 | ||
|
|
3b1c2d3b1b | ||
|
|
3660af9619 | ||
|
|
4905ba836c | ||
|
|
4b573fb4e2 | ||
|
|
6ae0af278b | ||
|
|
15650e1a6c | ||
|
|
ce00c862fe | ||
|
|
a50229431b | ||
|
|
275102f5b7 | ||
|
|
ff6286ffcc | ||
|
|
974fe6c3b7 | ||
|
|
b78292b270 | ||
|
|
3559a7018c | ||
|
|
9368885355 | ||
|
|
3222d6175d | ||
|
|
28f9f23449 | ||
|
|
5e7d192ebc | ||
|
|
d6a32416d7 | ||
|
|
6a2c01763e | ||
|
|
e1dd272708 | ||
|
|
3e7bc0606a | ||
|
|
9d9ca1475d | ||
|
|
43ddc0135c | ||
|
|
98f907a97b | ||
|
|
b2ddf060c9 | ||
|
|
5e612276db | ||
|
|
cbb293e7fa | ||
|
|
fdb050f6ed | ||
|
|
f83ddf79fa | ||
|
|
95b340a7a3 | ||
|
|
d46ac89e58 | ||
|
|
80b0912f84 | ||
|
|
e0b4431064 | ||
|
|
e3f16f91cf | ||
|
|
5f39e5c308 | ||
|
|
83304c9ff4 | ||
|
|
863cfb575c | ||
|
|
2944040648 | ||
|
|
5f19b9b0d0 | ||
|
|
2948ed091c | ||
|
|
bfa638e17f | ||
|
|
97ee397b54 | ||
|
|
f7fbe7efbe | ||
|
|
ce6b0bd099 | ||
|
|
8643960104 | ||
|
|
b4549ebc59 | ||
|
|
a602cddd05 | ||
|
|
c4278864d1 | ||
|
|
7094548ecf | ||
|
|
363fffada8 | ||
|
|
c2c3eb7106 | ||
|
|
8e19a64f15 | ||
|
|
9042b7e6d7 | ||
|
|
ad1855cfb5 | ||
|
|
efa0d1095d | ||
|
|
a8c13d740b | ||
|
|
7475e9829f | ||
|
|
b7dde1dae2 | ||
|
|
778c4560fd | ||
|
|
fa31096cd8 | ||
|
|
fd1dd39463 | ||
|
|
ecd3e12a41 | ||
|
|
0a8121e849 | ||
|
|
e93f9d7111 | ||
|
|
3bdccfea62 | ||
|
|
fdf98985a1 | ||
|
|
6c3609e79e | ||
|
|
5fa50339b4 | ||
|
|
f0c95bc987 | ||
|
|
a9aa709502 | ||
|
|
8edc23b888 | ||
|
|
c4fc510a27 | ||
|
|
134000996b | ||
|
|
111909c26d | ||
|
|
f66966d72f | ||
|
|
1f6a89ada5 | ||
|
|
31a1180b79 | ||
|
|
77b4a416a9 | ||
|
|
06a2e6fbfa | ||
|
|
c8a28e98d6 | ||
|
|
564adbd851 | ||
|
|
ad93f43145 | ||
|
|
2bf1123647 | ||
|
|
a6ff6426e9 | ||
|
|
da7a6933f5 | ||
|
|
54cac0d2fd | ||
|
|
de7aedab96 | ||
|
|
7fe718c887 | ||
|
|
36eaefb8b0 | ||
|
|
3a61ec84b3 | ||
|
|
dadf05ab4e | ||
|
|
c1bf92d243 | ||
|
|
c1959e0795 | ||
|
|
40a6b7434b | ||
|
|
24519a2732 | ||
|
|
c190abe4b1 | ||
|
|
07d5b3cd2a | ||
|
|
81e923b4f7 | ||
|
|
bcb51729a5 | ||
|
|
a731c1d01f | ||
|
|
0bb5a37ace | ||
|
|
a711827996 | ||
|
|
8f854e6f74 | ||
|
|
f1b5afbf5c | ||
|
|
84f6930389 | ||
|
|
f415dd4482 | ||
|
|
927f65e106 | ||
|
|
4b6b3082c7 | ||
|
|
4c550245db | ||
|
|
341ed22370 | ||
|
|
6851ccacf3 | ||
|
|
815af687ea | ||
|
|
e9373b59c8 | ||
|
|
3f3f3fcd57 | ||
|
|
d003e66310 | ||
|
|
e6108ccbbd | ||
|
|
a01f01198c | ||
|
|
cc96eaeb8f | ||
|
|
e25ca3d1d3 | ||
|
|
9c111b2d5e | ||
|
|
436a01fb6e | ||
|
|
9b6620dd4a | ||
|
|
85919e800e | ||
|
|
a2023a65c0 | ||
|
|
619370f36c | ||
|
|
ea6c0e691f | ||
|
|
de0d82f1e8 | ||
|
|
c8a26d9c5e | ||
|
|
d89b842586 | ||
|
|
6dd3ceea7e | ||
|
|
5fe68030b0 | ||
|
|
5215105440 | ||
|
|
fb69eb751a | ||
|
|
b903d51592 | ||
|
|
d1f6774e58 | ||
|
|
9e55df4141 | ||
|
|
b169e9413f | ||
|
|
c511766e9d | ||
|
|
e6f9ceb7fa | ||
|
|
eab8b48c9c | ||
|
|
46abb1009a | ||
|
|
2a42d86db0 | ||
|
|
68b0d9654c | ||
|
|
8f96cf13ac | ||
|
|
dbd179e507 | ||
|
|
9541e85600 | ||
|
|
9ecd829bfa | ||
|
|
fd4cff73ae | ||
|
|
1d6c4751de | ||
|
|
e066c1d6a1 | ||
|
|
85f5404b3b | ||
|
|
33c5893d32 | ||
|
|
b597cb0686 | ||
|
|
c5e288ca9c | ||
|
|
38d984f225 | ||
|
|
628c642040 | ||
|
|
d217cc4dd0 | ||
|
|
4f0e54b390 | ||
|
|
d7f89be31c | ||
|
|
53bcee7032 | ||
|
|
96991ae5ef | ||
|
|
068bcde451 | ||
|
|
0dda4f5646 | ||
|
|
6c57ce9dd2 | ||
|
|
6ac67a7e41 | ||
|
|
a23a585ad8 | ||
|
|
eba9562e81 | ||
|
|
ce49b409ac | ||
|
|
7222f68d4d | ||
|
|
f3f0d62f12 | ||
|
|
6e7c86e159 | ||
|
|
f5565f6700 | ||
|
|
e8d0bb0c54 | ||
|
|
33d70ccc96 | ||
|
|
53313a26af | ||
|
|
7ba180752a | ||
|
|
19736e66ad | ||
|
|
a00f8e4b76 | ||
|
|
7d9895cf5b | ||
|
|
d5f804bbb3 | ||
|
|
33a385cfa8 | ||
|
|
109d924591 | ||
|
|
a825eb3d4c | ||
|
|
3fb40677a4 | ||
|
|
833971cd28 | ||
|
|
d14b14bce9 | ||
|
|
53e26821ad | ||
|
|
1a7c06eb81 | ||
|
|
6cdbe45313 | ||
|
|
42a5b4892d | ||
|
|
9f4508b0c7 | ||
|
|
b2123ff01a | ||
|
|
43ead841a4 | ||
|
|
33b4794e83 | ||
|
|
fb91e6b1dd | ||
|
|
4a4dbf123e | ||
|
|
0decedd6a1 | ||
|
|
d2e3a63418 | ||
|
|
15b3f0a8f5 | ||
|
|
5c261528d0 | ||
|
|
8b2bbec0b9 | ||
|
|
8b3a4ad163 | ||
|
|
b3e272281b | ||
|
|
06a90d6266 | ||
|
|
6eb7acaf73 | ||
|
|
2e092a7880 | ||
|
|
515f669546 | ||
|
|
7699596597 | ||
|
|
34dd325a1d | ||
|
|
d33cdd978d | ||
|
|
0da9b4a69c | ||
|
|
7fc2eeaaa5 | ||
|
|
508529f66e | ||
|
|
dc170edbc1 | ||
|
|
85e65cf9e8 | ||
|
|
41887ef431 | ||
|
|
5dbf796fa9 | ||
|
|
20f601e607 | ||
|
|
c8d2d2255d | ||
|
|
5eaaf9f01d | ||
|
|
d6697948c2 | ||
|
|
f88abd7638 | ||
|
|
fbfc408e94 | ||
|
|
5ac1a1d6a5 | ||
|
|
3a74b30a0e | ||
|
|
fe0a6852e9 | ||
|
|
bfa04743e2 | ||
|
|
48094f7e37 | ||
|
|
60521b0a7e | ||
|
|
401deff2c3 | ||
|
|
6ffa325b73 | ||
|
|
eeedfc8ac9 | ||
|
|
aba7172a03 | ||
|
|
192f6f198f | ||
|
|
43c9d85b8f | ||
|
|
6d5935fb27 | ||
|
|
efbef4ff5b | ||
|
|
e4c99b9351 | ||
|
|
11e328ea6a | ||
|
|
8879907e38 | ||
|
|
885d72c4f7 | ||
|
|
4538542c25 | ||
|
|
3d509d75dc | ||
|
|
0ef73d7c3e | ||
|
|
6da90c6a97 | ||
|
|
bacdb794da | ||
|
|
2ece3f399b | ||
|
|
68b539db15 | ||
|
|
7a7bfa26f6 | ||
|
|
5f229351c8 | ||
|
|
82125c4933 | ||
|
|
33f75d26c1 | ||
|
|
3c4d702bc1 | ||
|
|
db8d100edb | ||
|
|
6f7e8978ca | ||
|
|
856bf2c574 | ||
|
|
b10cfa6b24 | ||
|
|
d3d329bd43 | ||
|
|
f358011900 | ||
|
|
cd6c3e4f79 | ||
|
|
ee7342de9d | ||
|
|
6120d49a35 | ||
|
|
381f8ea578 | ||
|
|
a3e2073dc6 | ||
|
|
f7bd858fea | ||
|
|
80d10cf4b5 | ||
|
|
ba1fb16f2d | ||
|
|
5f574667d3 | ||
|
|
c470bb1db1 | ||
|
|
26ed8ca33f | ||
|
|
313f5b1982 | ||
|
|
f3f0c24e16 | ||
|
|
fe2539ac97 | ||
|
|
dd1a430c21 | ||
|
|
621634697e | ||
|
|
fb5c35e6c9 | ||
|
|
228596dbe3 | ||
|
|
74baa85340 | ||
|
|
8f6e9ca54d | ||
|
|
19420d0bb3 | ||
|
|
770c8c8a9f | ||
|
|
252a34090a | ||
|
|
bde5ef4594 | ||
|
|
6eddf081de | ||
|
|
4645c50f6d | ||
|
|
3f6e66b1ce | ||
|
|
106889b0dd | ||
|
|
785b43464b | ||
|
|
03da60dd12 | ||
|
|
ec372f65ec | ||
|
|
cfba4c45b6 | ||
|
|
657d03f49d | ||
|
|
b4dd78b2ed | ||
|
|
1ebe7b8f71 | ||
|
|
30e7a71497 | ||
|
|
ff1de6c7e3 | ||
|
|
9e39883276 | ||
|
|
0f249ae1db | ||
|
|
31ada101b0 | ||
|
|
1b52a7168b | ||
|
|
83a3d0eabe | ||
|
|
080c6b4dfb | ||
|
|
ea50288c5b | ||
|
|
5d439a4168 | ||
|
|
3c46e16494 | ||
|
|
39cd0fe1ac | ||
|
|
16c0be74c9 | ||
|
|
732828612e | ||
|
|
7344b81c98 | ||
|
|
1e5b03edb5 | ||
|
|
b15b5af349 | ||
|
|
5242430b6d | ||
|
|
c39968a5bf | ||
|
|
8b4f9ab20e | ||
|
|
527e6050ce | ||
|
|
1d162cb874 | ||
|
|
c197f5c942 | ||
|
|
3a4c0bb888 | ||
|
|
38d50f4275 | ||
|
|
25a3b648fc | ||
|
|
d3638164fe | ||
|
|
1c2a61fd34 | ||
|
|
f81c4aa970 | ||
|
|
3caa4ec6e8 | ||
|
|
5e48a2274a | ||
|
|
7f7c980522 | ||
|
|
bbab4ebc86 | ||
|
|
8bae0c8ac5 | ||
|
|
a5e4aa1300 | ||
|
|
48313bc302 | ||
|
|
f0261a8236 | ||
|
|
84c194dd34 | ||
|
|
920e24b4ff | ||
|
|
43d82343a3 | ||
|
|
320d246a83 | ||
|
|
faea50f99d | ||
|
|
ef59798d70 | ||
|
|
6781eca56a | ||
|
|
c1f0853271 | ||
|
|
d4cda5c104 | ||
|
|
f9bd5bb995 | ||
|
|
f1c2b5cb3f | ||
|
|
579af29a73 | ||
|
|
0b0fd142a3 | ||
|
|
9e20a53ca0 | ||
|
|
8768240efe | ||
|
|
b429760bd5 | ||
|
|
218cd35c3d | ||
|
|
146a039c19 | ||
|
|
9a3e1fa9c8 | ||
|
|
fa194af9bb | ||
|
|
46b3757c13 | ||
|
|
5050ea7ac1 | ||
|
|
d121b4d0e1 | ||
|
|
d45bf5f60a | ||
|
|
94a43432fc |
@@ -37,7 +37,7 @@ jobs:
|
|||||||
file: Dockerfile-server-base
|
file: Dockerfile-server-base
|
||||||
push: true
|
push: true
|
||||||
tags: ghcr.io/${{ github.repository }}:server-base
|
tags: ghcr.io/${{ github.repository }}:server-base
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64,linux/arm64
|
||||||
cache-from: type=gha,scope=server-base
|
cache-from: type=gha,scope=server-base
|
||||||
cache-to: type=gha,mode=max,scope=server-base
|
cache-to: type=gha,mode=max,scope=server-base
|
||||||
build-args: |
|
build-args: |
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ jobs:
|
|||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
${{ env.IS_VERSION == 'true' && format('ghcr.io/{0}:server_{1},ghcr.io/{0}:server_latest', github.repository, env.VERSION) || format('ghcr.io/{0}:server_latest', github.repository) }}
|
${{ env.IS_VERSION == 'true' && format('ghcr.io/{0}:server_{1},ghcr.io/{0}:server_latest', github.repository, env.VERSION) || format('ghcr.io/{0}:server_latest', github.repository) }}
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64,linux/arm64
|
||||||
cache-from: type=gha
|
cache-from: type=gha
|
||||||
cache-to: type=gha,mode=max
|
cache-to: type=gha,mode=max
|
||||||
build-args: |
|
build-args: |
|
||||||
@@ -81,7 +81,7 @@ jobs:
|
|||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
${{ env.IS_VERSION == 'true' && format('ghcr.io/{0}:web_{1},ghcr.io/{0}:web_latest', github.repository, env.VERSION) || format('ghcr.io/{0}:web_latest', github.repository) }}
|
${{ env.IS_VERSION == 'true' && format('ghcr.io/{0}:web_{1},ghcr.io/{0}:web_latest', github.repository, env.VERSION) || format('ghcr.io/{0}:web_latest', github.repository) }}
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64,linux/arm64
|
||||||
cache-from: type=gha
|
cache-from: type=gha
|
||||||
cache-to: type=gha,mode=max
|
cache-to: type=gha,mode=max
|
||||||
build-args: |
|
build-args: |
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ __pycache__/
|
|||||||
.idea/
|
.idea/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
*$py.class
|
*$py.class
|
||||||
|
.vscode
|
||||||
|
.claude
|
||||||
|
AGENTS.md
|
||||||
|
|
||||||
# C extensions
|
# C extensions
|
||||||
*.so
|
*.so
|
||||||
|
|||||||
+9
-8
@@ -1,5 +1,5 @@
|
|||||||
# 第一阶段:构建Vue前端
|
# 第一阶段:构建Vue前端
|
||||||
FROM node:18 as web-builder
|
FROM node:18 AS web-builder
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY main/manager-web/package*.json ./
|
COPY main/manager-web/package*.json ./
|
||||||
RUN npm install
|
RUN npm install
|
||||||
@@ -7,7 +7,7 @@ COPY main/manager-web .
|
|||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
# 第二阶段:构建Java后端
|
# 第二阶段:构建Java后端
|
||||||
FROM maven:3.9.4-eclipse-temurin-21 as api-builder
|
FROM maven:3.9.4-eclipse-temurin-21 AS api-builder
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY main/manager-api/pom.xml .
|
COPY main/manager-api/pom.xml .
|
||||||
COPY main/manager-api/src ./src
|
COPY main/manager-api/src ./src
|
||||||
@@ -18,18 +18,19 @@ FROM bellsoft/liberica-runtime-container:jre-21-glibc
|
|||||||
|
|
||||||
# 安装Nginx和字体库
|
# 安装Nginx和字体库
|
||||||
RUN apk update && \
|
RUN apk update && \
|
||||||
apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/testing/ \
|
apk add --no-cache --no-scripts \
|
||||||
nginx \
|
nginx \
|
||||||
bash \
|
bash \
|
||||||
fontconfig \
|
fontconfig \
|
||||||
ttf-dejavu \
|
ttf-dejavu \
|
||||||
msttcorefonts-installer \
|
&& rm -rf /var/cache/apk/* \
|
||||||
&& ACCEPT_EULA=Y apk add --no-cache msttcorefonts-installer \
|
&& mkdir -p /run/nginx /var/log/nginx /var/tmp/nginx /etc/nginx/conf.d
|
||||||
&& fc-cache -f -v \
|
|
||||||
&& rm -rf /var/cache/apk/*
|
# 复制项目自带的中文字体
|
||||||
|
COPY main/manager-web/public/generator/static/fonts/*.ttf /usr/share/fonts/
|
||||||
|
|
||||||
# 更新字体缓存
|
# 更新字体缓存
|
||||||
RUN (printf 'YES\n' | update-ms-fonts || true) && fc-cache -f -v
|
RUN fc-cache -f -v
|
||||||
|
|
||||||
# 配置Nginx
|
# 配置Nginx
|
||||||
COPY docs/docker/nginx.conf /etc/nginx/nginx.conf
|
COPY docs/docker/nginx.conf /etc/nginx/nginx.conf
|
||||||
|
|||||||
@@ -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_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_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_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">
|
<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" />
|
<img alt="GitHub Contributors" src="https://img.shields.io/github/v/release/xinnan-tech/xiaozhi-esp32-server?logo=docker" />
|
||||||
</a>
|
</a>
|
||||||
@@ -183,8 +184,8 @@ Spearheaded by Professor Siyuan Liu's Team (South China University of Technology
|
|||||||
#### 🚀 部署方式选择
|
#### 🚀 部署方式选择
|
||||||
| 部署方式 | 特点 | 适用场景 | 部署文档 | 配置要求 | 视频教程 |
|
| 部署方式 | 特点 | 适用场景 | 部署文档 | 配置要求 | 视频教程 |
|
||||||
|---------|------|---------|---------|---------|---------|
|
|---------|------|---------|---------|---------|---------|
|
||||||
| **最简化安装** | 智能对话、IOT、MCP、视觉感知 | 低配置环境,数据存储在配置文件,无需数据库 | [①Docker版](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E5%8F%AA%E8%BF%90%E8%A1%8Cserver) / [②源码部署](./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)| 如果使用`FunASR`要2核4G,如果全API,要2核2G | - |
|
| **最简化安装** | 智能对话、单智能体管理 | 低配置环境,数据存储在配置文件,无需数据库 | [①Docker版](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E5%8F%AA%E8%BF%90%E8%A1%8Cserver) / [②源码部署](./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)| 如果使用`FunASR`要2核4G,如果全API,要2核2G | - |
|
||||||
| **全模块安装** | 智能对话、IOT、MCP接入点、声纹识别、视觉感知、OTA、智控台 | 完整功能体验,数据存储在数据库 |[①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) / [②源码部署](./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) / [③源码部署自动更新教程](./docs/dev-ops-integration.md) | 如果使用`FunASR`要4核8G,如果全API,要2核4G| [本地源码启动视频教程](https://www.bilibili.com/video/BV1wBJhz4Ewe) |
|
| **全模块安装** | 智能对话、多用户管理、多智能体管理、智控台界面操作 | 完整功能体验,数据存储在数据库 |[①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) / [②源码部署](./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) / [③源码部署自动更新教程](./docs/dev-ops-integration.md) | 如果使用`FunASR`要4核8G,如果全API,要2核4G| [本地源码启动视频教程](https://www.bilibili.com/video/BV1wBJhz4Ewe) |
|
||||||
|
|
||||||
常见问题及相关教程,可参考[这个链接](./docs/FAQ.md)
|
常见问题及相关教程,可参考[这个链接](./docs/FAQ.md)
|
||||||
|
|
||||||
@@ -211,10 +212,10 @@ Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/
|
|||||||
|
|
||||||
| 模块名称 | 入门全免费设置 | 流式配置 |
|
| 模块名称 | 入门全免费设置 | 流式配置 |
|
||||||
|:---:|:---:|:---:|
|
|:---:|:---:|:---:|
|
||||||
| ASR(语音识别) | FunASR(本地) | 👍FunASR(本地GPU模式) |
|
| ASR(语音识别) | FunASR(本地) | 👍XunfeiStreamASR(讯飞流式) |
|
||||||
| LLM(大模型) | ChatGLMLLM(智谱glm-4-flash) | 👍AliLLM(qwen3-235b-a22b-instruct-2507) 或 👍DoubaoLLM(doubao-1-5-pro-32k-250115) |
|
| LLM(大模型) | glm-4-flash(智谱) | 👍qwen-flash(阿里百炼) |
|
||||||
| VLLM(视觉大模型) | ChatGLMVLLM(智谱glm-4v-flash) | 👍QwenVLVLLM(千问qwen2.5-vl-3b-instructh) |
|
| VLLM(视觉大模型) | glm-4v-flash(智谱) | 👍qwen3.5-flash(阿里百炼) |
|
||||||
| TTS(语音合成) | ✅LinkeraiTTS(灵犀流式) | 👍HuoshanDoubleStreamTTS(火山双流式语音合成) 或 👍AliyunStreamTTS(阿里云流式语音合成) |
|
| TTS(语音合成) | EdgeTTS(微软) | 👍HuoshanDoubleStreamTTS(火山流式) |
|
||||||
| Intent(意图识别) | function_call(函数调用) | function_call(函数调用) |
|
| Intent(意图识别) | function_call(函数调用) | function_call(函数调用) |
|
||||||
| Memory(记忆功能) | mem_local_short(本地短期记忆) | mem_local_short(本地短期记忆) |
|
| Memory(记忆功能) | mem_local_short(本地短期记忆) | mem_local_short(本地短期记忆) |
|
||||||
|
|
||||||
@@ -242,7 +243,7 @@ Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/
|
|||||||
| 智能对话 | 支持多种LLM(大语言模型),实现智能对话 |
|
| 智能对话 | 支持多种LLM(大语言模型),实现智能对话 |
|
||||||
| 视觉感知 | 支持多种VLLM(视觉大模型),实现多模态交互 |
|
| 视觉感知 | 支持多种VLLM(视觉大模型),实现多模态交互 |
|
||||||
| 意图识别 | 支持外挂的大模型意图识别、大模型自主函数调用,提供插件化意图处理机制 |
|
| 意图识别 | 支持外挂的大模型意图识别、大模型自主函数调用,提供插件化意图处理机制 |
|
||||||
| 记忆系统 | 支持本地短期记忆、mem0ai接口记忆,具备记忆总结功能 |
|
| 记忆系统 | 支持本地短期记忆、mem0ai接口记忆、PowerMem智能记忆,具备记忆总结功能 |
|
||||||
| 知识库 | 支持RAGFlow知识库,让大模型判断需要调度知识库后再回答 |
|
| 知识库 | 支持RAGFlow知识库,让大模型判断需要调度知识库后再回答 |
|
||||||
| 工具调用 | 支持客户端IOT协议、客户MCP协议、服务端MCP协议、MCP接入点协议、自定义工具函数 |
|
| 工具调用 | 支持客户端IOT协议、客户MCP协议、服务端MCP协议、MCP接入点协议、自定义工具函数 |
|
||||||
| 指令下发 | 依托MQTT协议,支持从智控台将MCP指令下发到ESP32设备 |
|
| 指令下发 | 依托MQTT协议,支持从智控台将MCP指令下发到ESP32设备 |
|
||||||
@@ -260,7 +261,7 @@ Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/
|
|||||||
---
|
---
|
||||||
|
|
||||||
## 产品生态 👬
|
## 产品生态 👬
|
||||||
小智是一个生态,当你使用这个产品时,也可以看看其他在这个生态圈的[优秀项目](https://github.com/78/xiaozhi-esp32?tab=readme-ov-file#%E7%9B%B8%E5%85%B3%E5%BC%80%E6%BA%90%E9%A1%B9%E7%9B%AE)
|
小智是一个生态,当你使用这个产品时,也可以看看其他在这个生态圈的[优秀项目](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)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -330,6 +331,7 @@ Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/
|
|||||||
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|
||||||
|:------:|:---------------:|:----:|:---------:|:--:|
|
|:------:|:---------------:|:----:|:---------:|:--:|
|
||||||
| Memory | mem0ai | 接口调用 | 1000次/月额度 | |
|
| Memory | mem0ai | 接口调用 | 1000次/月额度 | |
|
||||||
|
| Memory | [powermem](./docs/powermem-integration.md) | 本地总结 | 取决于LLM和DB | OceanBase开源,支持智能检索 |
|
||||||
| Memory | mem_local_short | 本地总结 | 免费 | |
|
| Memory | mem_local_short | 本地总结 | 免费 | |
|
||||||
| Memory | nomem | 无记忆模式 | 免费 | |
|
| Memory | nomem | 无记忆模式 | 免费 | |
|
||||||
|
|
||||||
|
|||||||
+10
-8
@@ -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_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_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_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">
|
<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" />
|
<img alt="GitHub Contributors" src="https://img.shields.io/github/v/release/xinnan-tech/xiaozhi-esp32-server?logo=docker" />
|
||||||
</a>
|
</a>
|
||||||
@@ -181,8 +182,8 @@ Dieses Projekt bietet zwei Bereitstellungsmethoden. Bitte wählen Sie basierend
|
|||||||
#### 🚀 Auswahl der Bereitstellungsmethode
|
#### 🚀 Auswahl der Bereitstellungsmethode
|
||||||
| Bereitstellungsmethode | Funktionen | Anwendungsszenarien | Deployment-Dokumente | Konfigurationsanforderungen | Video-Tutorials |
|
| Bereitstellungsmethode | Funktionen | Anwendungsszenarien | Deployment-Dokumente | Konfigurationsanforderungen | Video-Tutorials |
|
||||||
|---------|------|---------|---------|---------|---------|
|
|---------|------|---------|---------|---------|---------|
|
||||||
| **Vereinfachte Installation** | Intelligenter Dialog, IOT, MCP, visuelle Wahrnehmung | Umgebungen mit geringer Konfiguration, Daten in Konfigurationsdateien gespeichert, keine Datenbank erforderlich | [①Docker-Version](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E5%8F%AA%E8%BF%90%E8%A1%8Cserver) / [②Quellcode-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 Kerne 4GB bei Verwendung von `FunASR`, 2 Kerne 2GB bei allen APIs | - |
|
| **Vereinfachte Installation** | Intelligenter Dialog, Einzel-Agenten-Verwaltung | Umgebungen mit geringer Konfiguration, Daten in Konfigurationsdateien gespeichert, keine Datenbank erforderlich | [①Docker-Version](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E5%8F%AA%E8%BF%90%E8%A1%8Cserver) / [②Quellcode-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 Kerne 4GB bei Verwendung von `FunASR`, 2 Kerne 2GB bei allen APIs | - |
|
||||||
| **Vollständige Modulinstallation** | Intelligenter Dialog, IOT, MCP-Endpunkte, Stimmabdruckerkennung, visuelle Wahrnehmung, OTA, intelligente Steuerkonsole | Vollständige Funktionserfahrung, Daten in Datenbank gespeichert |[①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) / [②Quellcode-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) / [③Quellcode-Deployment Auto-Update-Tutorial](./docs/dev-ops-integration.md) | 4 Kerne 8GB bei Verwendung von `FunASR`, 2 Kerne 4GB bei allen APIs| [Video-Tutorial für lokalen Quellcode-Start](https://www.bilibili.com/video/BV1wBJhz4Ewe) |
|
| **Vollständige Modulinstallation** | Intelligenter Dialog, Mehrbenutzerverwaltung, Mehr-Agenten-Verwaltung, Intelligente Steuerkonsole-Bedienung | Vollständige Funktionserfahrung, Daten in Datenbank gespeichert |[①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) / [②Quellcode-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) / [③Quellcode-Deployment Auto-Update-Tutorial](./docs/dev-ops-integration.md) | 4 Kerne 8GB bei Verwendung von `FunASR`, 2 Kerne 4GB bei allen APIs| [Video-Tutorial für lokalen Quellcode-Start](https://www.bilibili.com/video/BV1wBJhz4Ewe) |
|
||||||
|
|
||||||
Häufige Fragen und entsprechende Tutorials finden Sie unter [diesem Link](./docs/FAQ.md)
|
Häufige Fragen und entsprechende Tutorials finden Sie unter [diesem Link](./docs/FAQ.md)
|
||||||
|
|
||||||
@@ -209,10 +210,10 @@ Websocket-Schnittstellenadresse: wss://2662r3426b.vicp.fun/xiaozhi/v1/
|
|||||||
|
|
||||||
| Modulname | Einstiegslevel Kostenlose Einstellungen | Streaming-Konfiguration |
|
| Modulname | Einstiegslevel Kostenlose Einstellungen | Streaming-Konfiguration |
|
||||||
|:---:|:---:|:---:|
|
|:---:|:---:|:---:|
|
||||||
| ASR (Spracherkennung) | FunASR (Lokal) | 👍FunASR (Lokaler GPU-Modus) |
|
| ASR (Spracherkennung) | FunASR (Lokal) | 👍XunfeiStreamASR (Xunfei-Streaming) |
|
||||||
| LLM (Großes Modell) | ChatGLMLLM (Zhipu glm-4-flash) | 👍AliLLM (qwen3-235b-a22b-instruct-2507) oder 👍DoubaoLLM (doubao-1-5-pro-32k-250115) |
|
| LLM (Großes Modell) | glm-4-flash (Zhipu) | 👍qwen-flash (Alibaba Bailian) |
|
||||||
| VLLM (Vision Large Model) | ChatGLMVLLM (Zhipu glm-4v-flash) | 👍QwenVLVLLM (Qwen qwen2.5-vl-3b-instructh) |
|
| VLLM (Vision Large Model) | glm-4v-flash (Zhipu) | 👍qwen3.5-flash (Alibaba Bailian) |
|
||||||
| TTS (Sprachsynthese) | ✅LinkeraiTTS (Lingxi-Streaming) | 👍HuoshanDoubleStreamTTS (Volcano Dual-Stream-Sprachsynthese) oder 👍AliyunStreamTTS (Alibaba Cloud Streaming-Sprachsynthese) |
|
| TTS (Sprachsynthese) | EdgeTTS (Microsoft) | 👍HuoshanDoubleStreamTTS (Volcano-Streaming) |
|
||||||
| Intent (Absichtserkennung) | function_call (Funktionsaufruf) | function_call (Funktionsaufruf) |
|
| Intent (Absichtserkennung) | function_call (Funktionsaufruf) | function_call (Funktionsaufruf) |
|
||||||
| Memory (Gedächtnisfunktion) | mem_local_short (Lokales Kurzzeitgedächtnis) | mem_local_short (Lokales Kurzzeitgedächtnis) |
|
| 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 |
|
| Intelligenter Dialog | Unterstützt mehrere LLM (große Sprachmodelle), implementiert intelligenten Dialog |
|
||||||
| Visuelle Wahrnehmung | Unterstützt mehrere VLLM (Vision Large Models), implementiert multimodale Interaktion |
|
| Visuelle Wahrnehmung | Unterstützt mehrere VLLM (Vision Large Models), implementiert multimodale Interaktion |
|
||||||
| Absichtserkennung | Unterstützt LLM-Absichtserkennung, Function Call-Funktionsaufruf, bietet plugin-basierten Absichtsverarbeitungsmechanismus |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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 👬
|
## 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#%E7%9B%B8%E5%85%B3%E5%BC%80%E6%BA%90%E9%A1%B9%E7%9B%AE) 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 |
|
| Typ | Plattformname | Verwendungsmethode | Preismodell | Hinweise |
|
||||||
|:------:|:---------------:|:----:|:---------:|:--:|
|
|:------:|:---------------:|:----:|:---------:|:--:|
|
||||||
| Memory | mem0ai | Schnittstellenaufrufe | 1000 Mal/Monat Kontingent | |
|
| 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 | mem_local_short | Lokale Zusammenfassung | Kostenlos | |
|
||||||
| Memory | nomem | Kein Gedächtnismodus | Kostenlos | |
|
| Memory | nomem | Kein Gedächtnismodus | Kostenlos | |
|
||||||
|
|
||||||
|
|||||||
+22
-20
@@ -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_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_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_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">
|
<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" />
|
<img alt="GitHub Contributors" src="https://img.shields.io/github/v/release/xinnan-tech/xiaozhi-esp32-server?logo=docker" />
|
||||||
</a>
|
</a>
|
||||||
@@ -181,9 +182,10 @@ This project provides two deployment methods. Please choose based on your specif
|
|||||||
#### 🚀 Deployment Method Selection
|
#### 🚀 Deployment Method Selection
|
||||||
| Deployment Method | Features | Applicable Scenarios | Deployment Docs | Configuration Requirements | Video Tutorials |
|
| Deployment Method | Features | Applicable Scenarios | Deployment Docs | Configuration Requirements | Video Tutorials |
|
||||||
|---------|------|---------|---------|---------|---------|
|
|---------|------|---------|---------|---------|---------|
|
||||||
| **Simplified Installation** | Intelligent dialogue, IOT, MCP, visual perception | 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 | - |
|
| **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, IOT, MCP endpoints, voiceprint recognition, visual perception, OTA, intelligent control console | 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.
|
> 💡 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.
|
||||||
|
|
||||||
@@ -208,21 +210,22 @@ Websocket Interface Address: wss://2662r3426b.vicp.fun/xiaozhi/v1/
|
|||||||
|
|
||||||
| Module Name | Entry Level Free Settings | Streaming Configuration |
|
| Module Name | Entry Level Free Settings | Streaming Configuration |
|
||||||
|:---:|:---:|:---:|
|
|:---:|:---:|:---:|
|
||||||
| ASR(Speech Recognition) | FunASR(Local) | 👍FunASRServer or 👍DoubaoStreamASR |
|
| ASR(Speech Recognition) | FunASR(Local) | 👍XunfeiStreamASR(Xunfei Streaming) |
|
||||||
| LLM(Large Model) | ChatGLMLLM(Zhipu glm-4-flash) | 👍DoubaoLLM(Volcano doubao-1-5-pro-32k-250115) |
|
| LLM(Large Model) | glm-4-flash(Zhipu) | 👍qwen-flash(Alibaba Bailian) |
|
||||||
| VLLM(Vision Large Model) | ChatGLMVLLM(Zhipu glm-4v-flash) | 👍QwenVLVLLM(Qwen qwen2.5-vl-3b-instructh) |
|
| VLLM(Vision Large Model) | glm-4v-flash(Zhipu) | 👍qwen3.5-flash(Alibaba Bailian) |
|
||||||
| TTS(Speech Synthesis) | ✅LinkeraiTTS(Lingxi streaming) | 👍HuoshanDoubleStreamTTS(Volcano dual-stream speech synthesis) |
|
| TTS(Speech Synthesis) | EdgeTTS(Microsoft) | 👍HuoshanDoubleStreamTTS(Volcano Streaming) |
|
||||||
| Intent(Intent Recognition) | function_call(Function calling) | function_call(Function calling) |
|
| 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) |
|
| 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
|
#### 🔧 Testing Tools
|
||||||
This project provides the following testing tools to help you verify the system and choose suitable models:
|
This project provides the following testing tools to help you verify the system and choose suitable models:
|
||||||
|
|
||||||
| Tool Name | Location | Usage Method | Function Description |
|
| 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 |
|
| 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 | 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) |
|
||||||
| Model Response Test Tool 2 | main》xiaozhi-server》performance_tester_vllm.py | Execute `python performance_tester_vllm.py` | Tests VLLM(vision model) response speed |
|
|
||||||
|
|
||||||
> 💡 Note: When testing model speed, only models with configured keys will be tested.
|
> 💡 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 |
|
| Intelligent Dialogue | Supports multiple LLM(large language models), implements intelligent dialogue |
|
||||||
| Visual Perception | Supports multiple VLLM(vision large models), implements multimodal interaction |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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 🚧
|
### 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!
|
If you are a software developer, here is an [Open Letter to Developers](docs/contributor_open_letter.md). Welcome to join!
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Product Ecosystem 👬
|
## 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#%E7%9B%B8%E5%85%B3%E5%BC%80%E6%BA%90%E9%A1%B9%E7%9B%AE) in this ecosystem
|
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
|
||||||
|
|
||||||
| 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. |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -276,8 +273,10 @@ Xiaozhi is an ecosystem. When using this product, you can also check out other [
|
|||||||
| Dify interface calls | Dify | - |
|
| Dify interface calls | Dify | - |
|
||||||
| FastGPT interface calls | FastGPT | - |
|
| FastGPT interface calls | FastGPT | - |
|
||||||
| Coze interface calls | Coze | - |
|
| 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 |
|
| 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) |
|
| 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 |
|
| Type | Platform Name | Usage Method | Pricing Model | Notes |
|
||||||
|:------:|:---------------:|:----:|:---------:|:--:|
|
|:------:|:---------------:|:----:|:---------:|:--:|
|
||||||
| Memory | mem0ai | Interface calls | 1000 times/month quota | |
|
| 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 | 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 | 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 | 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>
|
||||||
+10
-8
@@ -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_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_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_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">
|
<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" />
|
<img alt="GitHub Contributors" src="https://img.shields.io/github/v/release/xinnan-tech/xiaozhi-esp32-server?logo=docker" />
|
||||||
</a>
|
</a>
|
||||||
@@ -182,8 +183,8 @@ Dự án này cung cấp hai phương pháp triển khai, vui lòng chọn theo
|
|||||||
#### 🚀 Lựa chọn phương pháp triển khai
|
#### 🚀 Lựa chọn phương pháp triển khai
|
||||||
| Phương pháp triển khai | Đặc điểm | Tình huống áp dụng | Tài liệu triển khai | Yêu cầu cấu hình | Video hướng dẫn |
|
| Phương pháp triển khai | Đặc điểm | Tình huống áp dụng | Tài liệu triển khai | Yêu cầu cấu hình | Video hướng dẫn |
|
||||||
|---------|------|---------|---------|---------|---------|
|
|---------|------|---------|---------|---------|---------|
|
||||||
| **Cài đặt tối giản** | Đối thoại thông minh, IOT, MCP, cảm nhận thị giác | Môi trường cấu hình thấp, dữ liệu lưu trong tệp cấu hình, không cần cơ sở dữ liệu | [①Phiên bản Docker](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E5%8F%AA%E8%BF%90%E8%A1%8Cserver) / [②Triển khai mã nguồn](./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 nhân 4GB nếu dùng `FunASR`, 2 nhân 2GB nếu toàn API | - |
|
| **Cài đặt tối giản** | Đối thoại thông minh, quản lý đơn tác nhân | Môi trường cấu hình thấp, dữ liệu lưu trong tệp cấu hình, không cần cơ sở dữ liệu | [①Phiên bản Docker](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E5%8F%AA%E8%BF%90%E8%A1%8Cserver) / [②Triển khai mã nguồn](./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 nhân 4GB nếu dùng `FunASR`, 2 nhân 2GB nếu toàn API | - |
|
||||||
| **Cài đặt toàn bộ module** | Đối thoại thông minh, IOT, điểm truy cập MCP, nhận dạng giọng nói, cảm nhận thị giác, OTA, bảng điều khiển thông minh | Trải nghiệm đầy đủ tính năng, dữ liệu lưu trong cơ sở dữ liệu |[①Phiên bản 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) / [②Triển khai mã nguồn](./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) / [③Hướng dẫn tự động cập nhật triển khai mã nguồn](./docs/dev-ops-integration.md) | 4 nhân 8GB nếu dùng `FunASR`, 2 nhân 4GB nếu toàn API| [Video hướng dẫn khởi động mã nguồn cục bộ](https://www.bilibili.com/video/BV1wBJhz4Ewe) |
|
| **Cài đặt toàn bộ module** | Đối thoại thông minh, quản lý đa người dùng, quản lý đa tác nhân, bảng điều khiển thông minh | Trải nghiệm đầy đủ tính năng, dữ liệu lưu trong cơ sở dữ liệu |[①Phiên bản 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) / [②Triển khai mã nguồn](./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) / [③Hướng dẫn tự động cập nhật triển khai mã nguồn](./docs/dev-ops-integration.md) | 4 nhân 8GB nếu dùng `FunASR`, 2 nhân 4GB nếu toàn API| [Video hướng dẫn khởi động mã nguồn cục bộ](https://www.bilibili.com/video/BV1wBJhz4Ewe) |
|
||||||
|
|
||||||
Câu hỏi thường gặp và hướng dẫn liên quan, vui lòng tham khảo [liên kết này](./docs/FAQ.md)
|
Câu hỏi thường gặp và hướng dẫn liên quan, vui lòng tham khảo [liên kết này](./docs/FAQ.md)
|
||||||
|
|
||||||
@@ -210,10 +211,10 @@ Công cụ kiểm tra dịch vụ: https://2662r3426b.vicp.fun/test/
|
|||||||
|
|
||||||
| Tên module | Cài đặt miễn phí cho người mới | Cấu hình streaming |
|
| Tên module | Cài đặt miễn phí cho người mới | Cấu hình streaming |
|
||||||
|:---:|:---:|:---:|
|
|:---:|:---:|:---:|
|
||||||
| ASR(Nhận dạng giọng nói) | FunASR(Local) | 👍FunASR(Chế độ GPU cục bộ) |
|
| ASR(Nhận dạng giọng nói) | FunASR(Local) | 👍XunfeiStreamASR(Xunfei Streaming) |
|
||||||
| LLM(Mô hình lớn) | ChatGLMLLM(Zhipu glm-4-flash) | 👍AliLLM(qwen3-235b-a22b-instruct-2507) hoặc 👍DoubaoLLM(doubao-1-5-pro-32k-250115) |
|
| LLM(Mô hình lớn) | glm-4-flash(Zhipu) | 👍qwen-flash(Alibaba Bailian) |
|
||||||
| VLLM(Mô hình lớn thị giác) | ChatGLMVLLM(Zhipu glm-4v-flash) | 👍QwenVLVLLM(Qwen qwen2.5-vl-3b-instructh) |
|
| 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) | ✅LinkeraiTTS(Lingxi streaming) | 👍HuoshanDoubleStreamTTS(Tổng hợp giọng nói streaming kép Volcano) hoặc 👍AliyunStreamTTS(Tổng hợp giọng nói streaming Alibaba Cloud) |
|
| 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) |
|
| 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ộ) |
|
| 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 |
|
| Đố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 |
|
| 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 |
|
| 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 |
|
| 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 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 |
|
| 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 👬
|
## 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#%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
|
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ú |
|
| 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 | 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 | mem_local_short | Tóm tắt cục bộ | Miễn phí | |
|
||||||
| Memory | nomem | Chế độ không có bộ nhớ | Miễn phí | |
|
| Memory | nomem | Chế độ không có bộ nhớ | Miễn phí | |
|
||||||
|
|
||||||
|
|||||||
+8
-4
@@ -38,10 +38,10 @@ conda install conda-forge::ffmpeg
|
|||||||
|
|
||||||
| 模块名称 | 入门全免费设置 | 流式配置 |
|
| 模块名称 | 入门全免费设置 | 流式配置 |
|
||||||
|:---:|:---:|:---:|
|
|:---:|:---:|:---:|
|
||||||
| ASR(语音识别) | FunASR(本地) | 👍FunASR(本地GPU模式) |
|
| ASR(语音识别) | FunASR(本地) | 👍XunfeiStreamASR(讯飞流式) |
|
||||||
| LLM(大模型) | ChatGLMLLM(智谱glm-4-flash) | 👍AliLLM(qwen3-235b-a22b-instruct-2507) 或 👍DoubaoLLM(doubao-1-5-pro-32k-250115) |
|
| LLM(大模型) | glm-4-flash(智谱) | 👍qwen-flash(阿里百炼) |
|
||||||
| VLLM(视觉大模型) | ChatGLMVLLM(智谱glm-4v-flash) | 👍QwenVLVLLM(千问qwen2.5-vl-3b-instructh) |
|
| VLLM(视觉大模型) | glm-4v-flash(智谱) | 👍qwen3.5-flash(阿里百炼) |
|
||||||
| TTS(语音合成) | ✅LinkeraiTTS(灵犀流式) | 👍HuoshanDoubleStreamTTS(火山双流式语音合成) 或 👍AliyunStreamTTS(阿里云流式语音合成) |
|
| TTS(语音合成) | EdgeTTS(微软) | 👍HuoshanDoubleStreamTTS(火山流式) |
|
||||||
| Intent(意图识别) | function_call(函数调用) | function_call(函数调用) |
|
| Intent(意图识别) | function_call(函数调用) | function_call(函数调用) |
|
||||||
| Memory(记忆功能) | mem_local_short(本地短期记忆) | mem_local_short(本地短期记忆) |
|
| Memory(记忆功能) | mem_local_short(本地短期记忆) | mem_local_short(本地短期记忆) |
|
||||||
|
|
||||||
@@ -69,6 +69,7 @@ VAD:
|
|||||||
### 9、编译固件相关教程
|
### 9、编译固件相关教程
|
||||||
1、[如何自己编译小智固件](./firmware-build.md)<br/>
|
1、[如何自己编译小智固件](./firmware-build.md)<br/>
|
||||||
2、[如何基于虾哥编译好的固件修改OTA地址](./firmware-setting.md)<br/>
|
2、[如何基于虾哥编译好的固件修改OTA地址](./firmware-setting.md)<br/>
|
||||||
|
3、[单模块部署如何配置固件OTA自动升级](./ota-upgrade-guide.md)<br/>
|
||||||
|
|
||||||
### 10、拓展相关教程
|
### 10、拓展相关教程
|
||||||
1、[如何开启手机号码注册智控台](./ali-sms-integration.md)<br/>
|
1、[如何开启手机号码注册智控台](./ali-sms-integration.md)<br/>
|
||||||
@@ -80,6 +81,9 @@ VAD:
|
|||||||
7、[如何开启声纹识别](./voiceprint-integration.md)<br/>
|
7、[如何开启声纹识别](./voiceprint-integration.md)<br/>
|
||||||
8、[新闻插件源配置指南](./newsnow_plugin_config.md)<br/>
|
8、[新闻插件源配置指南](./newsnow_plugin_config.md)<br/>
|
||||||
9、[知识库ragflow集成指南](./ragflow-integration.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、语音克隆、本地语音部署相关教程
|
### 11、语音克隆、本地语音部署相关教程
|
||||||
1、[如何在智控台克隆音色](./huoshan-streamTTS-voice-cloning.md)<br/>
|
1、[如何在智控台克隆音色](./huoshan-streamTTS-voice-cloning.md)<br/>
|
||||||
|
|||||||
@@ -0,0 +1,224 @@
|
|||||||
|
# 上下文源使用教程
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
`上下文源`,就是为小智系统提示词的上下文添加【数据源】。
|
||||||
|
|
||||||
|
`上下文源` 在小智在唤醒那一刻,获取外部系统的数据,并将其动态注入到大模型的系统提示词(System Prompt)中。
|
||||||
|
让其做到唤醒时感知世界某个事物的状态。
|
||||||
|
|
||||||
|
它和MCP、记忆有本质的区别:`上下文源`是强制让小智感知世界的数据;`记忆(Mem)`是让他知道之前聊了什么内容;`MCP(functionc all)`是当需要调用某项能力/知识的时候使用调用。
|
||||||
|
|
||||||
|
通过这个功能,在小智唤醒的一刹那,“感知”到:
|
||||||
|
- 人体健康传感器状态(体温、血压、血氧状态等)
|
||||||
|
- 业务系统的实时数据(服务器负载、待办数据、股票信息等)
|
||||||
|
- 任何可以通过 HTTP API 获取的文本信息
|
||||||
|
|
||||||
|
**注意**:该功能只是方便小智在唤醒的时候感知事物的状态,而如果想要小智唤醒后实时获取事物的状态,建议在此功能上再结合MCP工具的调用。
|
||||||
|
|
||||||
|
## 工作原理
|
||||||
|
|
||||||
|
1. **配置源**:用户配置一个或多个 HTTP API 地址。
|
||||||
|
2. **触发请求**:当系统构建 Prompt 时,如果发现模板中包含 `{{ dynamic_context }}` 占位符,会请求所有配置的 API。
|
||||||
|
3. **自动注入**:系统会自动将 API 返回的数据格式化为 Markdown 列表,替换 `{{ dynamic_context }}` 占位符。
|
||||||
|
|
||||||
|
## 接口规范
|
||||||
|
|
||||||
|
为了让小智正确解析数据,您的 API 需要满足以下规范:
|
||||||
|
|
||||||
|
- **请求方式**:`GET`
|
||||||
|
- **请求头**:系统会自动添加 `device-id` 字段到 Request Header。
|
||||||
|
- **响应格式**:必须返回 JSON 格式,且包含 `code` 和 `data` 字段。
|
||||||
|
|
||||||
|
### 响应示例
|
||||||
|
|
||||||
|
**情况 1:返回键值对**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"msg": "success",
|
||||||
|
"data": {
|
||||||
|
"客厅温度": "26℃",
|
||||||
|
"客厅湿度": "45%",
|
||||||
|
"大门状态": "已关闭"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
*注入效果:*
|
||||||
|
```markdown
|
||||||
|
<context>
|
||||||
|
- **客厅温度:** 26℃
|
||||||
|
- **客厅湿度:** 45%
|
||||||
|
- **大门状态:** 已关闭
|
||||||
|
</context>
|
||||||
|
```
|
||||||
|
|
||||||
|
**情况 2:返回列表**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"data": [
|
||||||
|
"您有10个待办事项",
|
||||||
|
"当前汽车的行驶速度是100km每小时"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
*注入效果:*
|
||||||
|
```markdown
|
||||||
|
<context>
|
||||||
|
- 您有10个待办事项
|
||||||
|
- 当前汽车的行驶速度是100km每小时
|
||||||
|
</context>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 配置指南
|
||||||
|
|
||||||
|
### 方式 1:智控台配置(全模块部署)
|
||||||
|
|
||||||
|
1. 登录智控台,进入**角色配置**页面。
|
||||||
|
2. 找到**上下文源**配置项(点击“编辑源”按钮)。
|
||||||
|
3. 点击**添加**,输入您的 API 地址。
|
||||||
|
4. 如果 API 需要鉴权,可以在**请求头**部分添加 `Authorization` 或其他 Header。
|
||||||
|
5. 保存配置。
|
||||||
|
|
||||||
|
### 方式 2:配置文件配置(单模块部署)
|
||||||
|
|
||||||
|
编辑 `xiaozhi-server/data/.config.yaml` 文件,添加 `context_providers` 配置段:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# 上下文源配置
|
||||||
|
context_providers:
|
||||||
|
- url: "http://api.example.com/data"
|
||||||
|
headers:
|
||||||
|
Authorization: "Bearer your-token"
|
||||||
|
- url: "http://another-api.com/data"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 启用功能
|
||||||
|
|
||||||
|
默认情况下,系统的提示词模板文件(`data/.agent-base-prompt.txt`)中已经预置了 `{{ dynamic_context }}` 占位符,您无需手动添加。
|
||||||
|
|
||||||
|
**示例:**
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
<context>
|
||||||
|
【重要!以下信息已实时提供,无需调用工具查询,请直接使用:】
|
||||||
|
- **设备ID:** {{device_id}}
|
||||||
|
- **当前时间:** {{current_time}}
|
||||||
|
...
|
||||||
|
{{ dynamic_context }}
|
||||||
|
</context>
|
||||||
|
```
|
||||||
|
|
||||||
|
**注意**:如果您不需要使用此功能,可以选择**不配置任何上下文源**,也可以从提示词模板文件中**删除** `{{ dynamic_context }}` 占位符。
|
||||||
|
|
||||||
|
## 附录:Mock 测试服务示例
|
||||||
|
|
||||||
|
为了方便您测试和开发,我们提供了一个简单的 Python Mock Server 脚本。您可以运行此脚本在本地模拟 API 接口。
|
||||||
|
|
||||||
|
**mock_api_server.py**
|
||||||
|
|
||||||
|
```python
|
||||||
|
import http.server
|
||||||
|
import socketserver
|
||||||
|
import json
|
||||||
|
from urllib.parse import urlparse, parse_qs
|
||||||
|
|
||||||
|
# 设置端口号
|
||||||
|
PORT = 8081
|
||||||
|
|
||||||
|
class MockRequestHandler(http.server.SimpleHTTPRequestHandler):
|
||||||
|
def do_GET(self):
|
||||||
|
# 解析路径和参数
|
||||||
|
parsed_path = urlparse(self.path)
|
||||||
|
path = parsed_path.path
|
||||||
|
query = parse_qs(parsed_path.query)
|
||||||
|
|
||||||
|
response_data = {}
|
||||||
|
status_code = 200
|
||||||
|
|
||||||
|
print(f"收到请求: {path}, 参数: {query}")
|
||||||
|
|
||||||
|
# Case 1: 模拟健康数据 (返回字典 Dict)
|
||||||
|
# 路径参数风格: /health
|
||||||
|
# device_id 从 Header 获取
|
||||||
|
if path == "/health":
|
||||||
|
device_id = self.headers.get("device-id", "unknown_device")
|
||||||
|
print(f"device_id: {device_id}")
|
||||||
|
response_data = {
|
||||||
|
"code": 0,
|
||||||
|
"msg": "success",
|
||||||
|
"data": {
|
||||||
|
"测试设备ID": device_id,
|
||||||
|
"心率": "80 bpm",
|
||||||
|
"血压": "120/80 mmHg",
|
||||||
|
"状态": "良好"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Case 2: 模拟新闻列表 (返回列表 List)
|
||||||
|
# 无参数: /news/list
|
||||||
|
elif path == "/news/list":
|
||||||
|
response_data = {
|
||||||
|
"code": 0,
|
||||||
|
"msg": "success",
|
||||||
|
"data": [
|
||||||
|
"今日头条:Python 3.14 发布",
|
||||||
|
"科技新闻:AI 助手改变生活",
|
||||||
|
"本地新闻:明日有大雨,记得带伞"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Case 3: 模拟天气简报 (返回字符串 String)
|
||||||
|
# 无参数: /weather/simple
|
||||||
|
elif path == "/weather/simple":
|
||||||
|
response_data = {
|
||||||
|
"code": 0,
|
||||||
|
"msg": "success",
|
||||||
|
"data": "今日晴转多云,气温 20-25 度,空气质量优,适合出行。"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Case 4: 模拟设备详情 (Query参数风格)
|
||||||
|
# 参数风格: /device/info
|
||||||
|
# device_id 从 Header 获取
|
||||||
|
elif path == "/device/info":
|
||||||
|
device_id = self.headers.get("device-id", "unknown_device")
|
||||||
|
response_data = {
|
||||||
|
"code": 0,
|
||||||
|
"msg": "success",
|
||||||
|
"data": {
|
||||||
|
"查询方式": "Header参数",
|
||||||
|
"设备ID": device_id,
|
||||||
|
"电量": "85%",
|
||||||
|
"固件": "v2.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Case 5: 404 Not Found
|
||||||
|
else:
|
||||||
|
status_code = 404
|
||||||
|
response_data = {"error": "接口不存在"}
|
||||||
|
|
||||||
|
# 发送响应
|
||||||
|
self.send_response(status_code)
|
||||||
|
self.send_header('Content-type', 'application/json; charset=utf-8')
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(json.dumps(response_data, ensure_ascii=False).encode('utf-8'))
|
||||||
|
|
||||||
|
# 启动服务
|
||||||
|
# 允许地址重用,防止快速重启报错
|
||||||
|
socketserver.TCPServer.allow_reuse_address = True
|
||||||
|
with socketserver.TCPServer(("", PORT), MockRequestHandler) as httpd:
|
||||||
|
print(f"==================================================")
|
||||||
|
print(f"Mock API Server 已启动: http://localhost:{PORT}")
|
||||||
|
print(f"可用接口列表:")
|
||||||
|
print(f"1. [字典] http://localhost:{PORT}/health")
|
||||||
|
print(f"2. [列表] http://localhost:{PORT}/news/list")
|
||||||
|
print(f"3. [文本] http://localhost:{PORT}/weather/simple")
|
||||||
|
print(f"4. [参数] http://localhost:{PORT}/device/info")
|
||||||
|
print(f"==================================================")
|
||||||
|
try:
|
||||||
|
httpd.serve_forever()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n服务已停止")
|
||||||
|
```
|
||||||
@@ -23,6 +23,8 @@
|
|||||||
|
|
||||||
### 2.将音色资源ID分配给系统账号
|
### 2.将音色资源ID分配给系统账号
|
||||||
|
|
||||||
|
使用超级管理员账号登录智控台,点击顶部`参数字典`,在下拉菜单中,点击`系统功能配置`页面。在页面上勾选`音色克隆`,点击保存配置。即可在顶部菜单看到`音色克隆`按钮。
|
||||||
|
|
||||||
使用超级管理员账号登录智控台,点击顶部【音色克隆】、【音色资源】。
|
使用超级管理员账号登录智控台,点击顶部【音色克隆】、【音色资源】。
|
||||||
|
|
||||||
点击新增按钮,在【平台名称】选择“火山双流式语音合成”;
|
点击新增按钮,在【平台名称】选择“火山双流式语音合成”;
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ docker logs -f mcp-endpoint-server
|
|||||||
请你保留好上面两个`接口地址`,下一步要用到。
|
请你保留好上面两个`接口地址`,下一步要用到。
|
||||||
|
|
||||||
# 2、全模块部署时,怎么配置MCP接入点
|
# 2、全模块部署时,怎么配置MCP接入点
|
||||||
|
首先,你要开启MCP接入点功能。在智控台,点击顶部`参数字典`,在下拉菜单中,点击`系统功能配置`页面。在页面上勾选`MCP接入点`,点击`保存配置`。在`角色配置`页面,点击`编辑功能`按钮,即可看到`mcp接入点`功能。
|
||||||
|
|
||||||
如果你是全模块部署,使用管理员账号,登录智控台,点击顶部`参数字典`,选择`参数管理`功能。
|
如果你是全模块部署,使用管理员账号,登录智控台,点击顶部`参数字典`,选择`参数管理`功能。
|
||||||
|
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ MQTT_PORT=1883 # MQTT服务器端口
|
|||||||
UDP_PORT=8884 # UDP服务器端口
|
UDP_PORT=8884 # UDP服务器端口
|
||||||
API_PORT=8007 # 管理API端口
|
API_PORT=8007 # 管理API端口
|
||||||
MQTT_SIGNATURE_KEY=test # MQTT签名密钥
|
MQTT_SIGNATURE_KEY=test # MQTT签名密钥
|
||||||
|
SERVER_SECRET=Te1st12134 # 服务器密钥,请保持和智控台(server.secret)一致或者和xiaozhi-server里(server.auth_key)保持一致
|
||||||
```
|
```
|
||||||
请注意`PUBLIC_IP`配置,确保其与实际公网IP一致,如果有域名就填域名。
|
请注意`PUBLIC_IP`配置,确保其与实际公网IP一致,如果有域名就填域名。
|
||||||
|
|
||||||
@@ -85,6 +86,13 @@ MQTT_SIGNATURE_KEY=test # MQTT签名密钥
|
|||||||
- 注意不要用简单的密码,比如`123456`、`test`等。
|
- 注意不要用简单的密码,比如`123456`、`test`等。
|
||||||
- 注意不要用简单的密码,比如`123456`、`test`等。
|
- 注意不要用简单的密码,比如`123456`、`test`等。
|
||||||
|
|
||||||
|
`SERVER_SECRET` 是用生成websocket连接的认证信息。
|
||||||
|
|
||||||
|
1、如果你是全模块部署,且你的智控台的参数管理里`server.auth.enabled`设置成了`true`,那么,`SERVER_SECRET`需要和智控台(`server.secret`)保持一致。
|
||||||
|
|
||||||
|
2、如果你是单模块部署,且你在配置文件里把`server.auth.enabled`设置成了`true`,那么,`SERVER_SECRET`需要和配置文件里(`server.auth_key`)保持一致。
|
||||||
|
|
||||||
|
|
||||||
6. 启动MQTT网关
|
6. 启动MQTT网关
|
||||||
```
|
```
|
||||||
# 启动服务
|
# 启动服务
|
||||||
@@ -119,7 +127,7 @@ pm2 restart xz-mqtt
|
|||||||
```
|
```
|
||||||
192.168.0.7:8884
|
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
|
192.168.0.7:8007
|
||||||
```
|
```
|
||||||
@@ -146,7 +154,7 @@ curl 'http://localhost:8002/xiaozhi/ota/' \
|
|||||||
pm2 logs xz-mqtt
|
pm2 logs xz-mqtt
|
||||||
```
|
```
|
||||||
|
|
||||||
## 第三部分:全模块运行实现小智硬件MQTT+UDP连接
|
## 第三部分:单模块运行xiaozhi-server实现小智硬件MQTT+UDP连接
|
||||||
|
|
||||||
打开你的`data/.config.yaml`文件,在`server`下找到`mqtt_gateway`填入你在`.env`文件中设置的`PUBLIC_IP`+`:`+`MQTT_PORT`。类似这样
|
打开你的`data/.config.yaml`文件,在`server`下找到`mqtt_gateway`填入你在`.env`文件中设置的`PUBLIC_IP`+`:`+`MQTT_PORT`。类似这样
|
||||||
```
|
```
|
||||||
@@ -176,4 +184,4 @@ curl 'http://localhost:8002/xiaozhi/ota/' \
|
|||||||
唤醒后留意mqtt-gateway的日志,确认是否有连接成功的日志。
|
唤醒后留意mqtt-gateway的日志,确认是否有连接成功的日志。
|
||||||
```
|
```
|
||||||
pm2 logs xz-mqtt
|
pm2 logs xz-mqtt
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
# 单模块部署固件OTA自动升级配置指南
|
||||||
|
|
||||||
|
本教程将指导你如何在**单模块部署**场景下配置固件OTA自动升级功能,实现设备固件的自动更新。
|
||||||
|
|
||||||
|
如果你已经使用**全模块部署**,请忽略本教程。
|
||||||
|
|
||||||
|
## 功能介绍
|
||||||
|
|
||||||
|
在单模块部署中,xiaozhi-server内置了OTA固件管理功能,可以自动检测设备版本并下发升级固件。系统会根据设备型号和当前版本,自动匹配并推送最新的固件版本。
|
||||||
|
|
||||||
|
## 前提条件
|
||||||
|
|
||||||
|
- 你已经成功进行**单模块部署**并运行xiaozhi-server
|
||||||
|
- 设备能够正常连接到服务器
|
||||||
|
|
||||||
|
## 第一步 准备固件文件
|
||||||
|
|
||||||
|
### 1. 创建固件存放目录
|
||||||
|
|
||||||
|
固件文件需要放在`data/bin/`目录下。如果该目录不存在,请手动创建:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p data/bin
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 固件文件命名规则
|
||||||
|
|
||||||
|
固件文件必须遵循以下命名格式:
|
||||||
|
|
||||||
|
```
|
||||||
|
{设备型号}_{版本号}.bin
|
||||||
|
```
|
||||||
|
|
||||||
|
**命名规则说明:**
|
||||||
|
- `设备型号`:设备的型号名称,例如 `lichuang-dev`、`bread-compact-wifi` 等
|
||||||
|
- `版本号`:固件版本号,必须以数字开头,支持数字、字母、点号、下划线和短横线,例如 `1.6.6`、`2.0.0` 等
|
||||||
|
- 文件扩展名必须是 `.bin`
|
||||||
|
|
||||||
|
**命名示例:**
|
||||||
|
```
|
||||||
|
bread-compact-wifi_1.6.6.bin
|
||||||
|
lichuang-dev_2.0.0.bin
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 放置固件文件
|
||||||
|
|
||||||
|
将准备好的固件文件(.bin文件)复制到`data/bin/`目录下:
|
||||||
|
|
||||||
|
重要的事情说三遍:升级的bin文件是`xiaozhi.bin`,不是全量固件文件`merged-binary.bin`!
|
||||||
|
|
||||||
|
重要的事情说三遍:升级的bin文件是`xiaozhi.bin`,不是全量固件文件`merged-binary.bin`!
|
||||||
|
|
||||||
|
重要的事情说三遍:升级的bin文件是`xiaozhi.bin`,不是全量固件文件`merged-binary.bin`!
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp xiaozhi.bin data/bin/设备型号_版本号.bin
|
||||||
|
```
|
||||||
|
|
||||||
|
例如:
|
||||||
|
```bash
|
||||||
|
cp xiaozhi.bin data/bin/bread-compact-wifi_1.6.6.bin
|
||||||
|
```
|
||||||
|
|
||||||
|
## 第二步 配置公网访问地址(仅公网部署需要)
|
||||||
|
|
||||||
|
**注意:此步骤仅适用于单模块公网部署的场景。**
|
||||||
|
|
||||||
|
如果你的xiaozhi-server是公网部署(使用公网IP或域名),**必须**配置`server.vision_explain`参数,因为OTA固件下载地址会使用该配置的域名和端口。
|
||||||
|
|
||||||
|
如果你是局域网部署,可以跳过此步骤。
|
||||||
|
|
||||||
|
### 为什么要配置这个参数?
|
||||||
|
|
||||||
|
在单模块部署中,系统生成固件下载地址时,会使用`vision_explain`配置的域名和端口作为基础地址。如果不配置或配置错误,设备将无法访问固件下载地址。
|
||||||
|
|
||||||
|
### 配置方法
|
||||||
|
|
||||||
|
打开`data/.config.yaml`文件,找到`server`配置段,设置`vision_explain`参数:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
server:
|
||||||
|
vision_explain: http://你的域名或IP:端口号/mcp/vision/explain
|
||||||
|
```
|
||||||
|
|
||||||
|
**配置示例:**
|
||||||
|
|
||||||
|
局域网部署(默认):
|
||||||
|
```yaml
|
||||||
|
server:
|
||||||
|
vision_explain: http://192.168.1.100:8003/mcp/vision/explain
|
||||||
|
```
|
||||||
|
|
||||||
|
公网域名部署:
|
||||||
|
```yaml
|
||||||
|
server:
|
||||||
|
vision_explain: http://yourdomain.com:8003/mcp/vision/explain
|
||||||
|
```
|
||||||
|
|
||||||
|
### 注意事项
|
||||||
|
|
||||||
|
- 域名或IP必须是设备能够访问的地址
|
||||||
|
- 如果使用Docker部署,不能使用Docker内部地址(如127.0.0.1或localhost)
|
||||||
|
- 如果你使用了nginx反向代理,请填写对外的地址和端口号,不是本项目运行的端口号
|
||||||
|
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### 1. 设备收不到固件更新
|
||||||
|
|
||||||
|
**可能原因和解决方法:**
|
||||||
|
|
||||||
|
- 检查固件文件命名是否符合规则:`{型号}_{版本号}.bin`
|
||||||
|
- 检查固件文件是否正确放置在`data/bin/`目录
|
||||||
|
- 检查设备型号是否与固件文件名中的型号匹配
|
||||||
|
- 检查固件版本号是否高于设备当前版本
|
||||||
|
- 查看服务器日志,确认OTA请求是否正常处理
|
||||||
|
|
||||||
|
### 2. 设备报告下载地址无法访问
|
||||||
|
|
||||||
|
**可能原因和解决方法:**
|
||||||
|
|
||||||
|
- 检查`server.vision_explain`配置的域名或IP是否正确
|
||||||
|
- 确认端口号配置正确(默认8003)
|
||||||
|
- 如果是公网部署,确保设备能够访问该公网地址
|
||||||
|
- 如果是Docker部署,确保不是使用了内部地址(127.0.0.1)
|
||||||
|
- 检查防火墙是否开放了对应端口
|
||||||
|
- 如果你使用了nginx反向代理,请填写对外的地址和端口号,不是本项目运行的端口号
|
||||||
|
|
||||||
|
### 3. 如何确认设备当前版本
|
||||||
|
|
||||||
|
查看OTA请求日志,日志中会显示设备上报的版本号:
|
||||||
|
|
||||||
|
```
|
||||||
|
[ota_handler] - 设备 AA:BB:CC:DD:EE:FF 固件已是最新: 1.6.6
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 固件文件放置后没有生效
|
||||||
|
|
||||||
|
系统有30秒的缓存时间(默认),可以:
|
||||||
|
- 等待30秒后再让设备发起OTA请求
|
||||||
|
- 重启xiaozhi-server服务
|
||||||
|
- 调整`firmware_cache_ttl`配置为更短的时间
|
||||||
@@ -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/)
|
||||||
|
|
||||||
@@ -156,6 +156,14 @@ services:
|
|||||||
|
|
||||||
编辑`ragflow/docker`文件夹下的`.env`文件,找到以下配置,逐个搜索,逐个修改!逐个搜索,逐个修改!
|
编辑`ragflow/docker`文件夹下的`.env`文件,找到以下配置,逐个搜索,逐个修改!逐个搜索,逐个修改!
|
||||||
|
|
||||||
|
下面对于`.env`文件的修改,60%的人会忽略`MYSQL_USER`配置导致ragflow启动不成功,因此,需要强调三次:
|
||||||
|
|
||||||
|
强调第一次:如果你的`.env`文件如果没有`MYSQL_USER`配置,请在配置文件增加这项!
|
||||||
|
|
||||||
|
强调第二次:如果你的`.env`文件如果没有`MYSQL_USER`配置,请在配置文件增加这项!
|
||||||
|
|
||||||
|
强调第三次:如果你的`.env`文件如果没有`MYSQL_USER`配置,请在配置文件增加这项!
|
||||||
|
|
||||||
``` env
|
``` env
|
||||||
# 端口设置
|
# 端口设置
|
||||||
SVR_WEB_HTTP_PORT=8008 # HTTP端口
|
SVR_WEB_HTTP_PORT=8008 # HTTP端口
|
||||||
@@ -230,9 +238,11 @@ docker-compose -f docker-compose.yml up -d
|
|||||||
在弹框中,点击"Create new Key"按钮,生成一个API Key。复制这个`API Key`,你稍后会用到。
|
在弹框中,点击"Create new Key"按钮,生成一个API Key。复制这个`API Key`,你稍后会用到。
|
||||||
|
|
||||||
# 第二步 配置到智控台
|
# 第二步 配置到智控台
|
||||||
确保你的智控台版本是`0.8.7`或以上。使用超级管理员账号登录到智控台。在顶部导航栏中,点击`模型配置`,在左侧导航栏中,点击`知识库`。
|
确保你的智控台版本是`0.8.7`或以上。使用超级管理员账号登录到智控台。
|
||||||
|
|
||||||
在列表中找到`RAG_RAGFlow`,点击`编辑`按钮。
|
首先,你要先开启知识库功能。在顶部导航栏中,点击`参数字典`,在下拉菜单中,点击`系统功能配置`页面。在页面上勾选`知识库`,点击`保存配置`。即可在导航栏看到`知识库`功能。
|
||||||
|
|
||||||
|
在顶部导航栏中,点击`模型配置`,在左侧导航栏中,点击`知识库`。在列表中找到`RAG_RAGFlow`,点击`编辑`按钮。
|
||||||
|
|
||||||
在`服务地址`中,填写`http://你的ragflow服务的局域网IP:8008`,例如我的ragflow服务的局域网IP是`192.168.1.100`,那么我就填写`http://192.168.1.100:8008`。
|
在`服务地址`中,填写`http://你的ragflow服务的局域网IP:8008`,例如我的ragflow服务的局域网IP是`192.168.1.100`,那么我就填写`http://192.168.1.100:8008`。
|
||||||
|
|
||||||
|
|||||||
@@ -164,6 +164,8 @@ http://192.168.1.25:8005/voiceprint/health?key=abcd
|
|||||||
# 2、全模块部署时,怎么配置声纹识别
|
# 2、全模块部署时,怎么配置声纹识别
|
||||||
|
|
||||||
## 第一步 配置接口
|
## 第一步 配置接口
|
||||||
|
首先,你要开启声纹识别功能。在智控台,点击顶部`参数字典`,在下拉菜单中,点击`系统功能配置`页面。在页面上勾选`声纹识别`,点击`保存配置`。即可在新建智能体的卡片上看到`声纹识别`按钮。
|
||||||
|
|
||||||
如果你是全模块部署,使用管理员账号,登录智控台,点击顶部`参数字典`,选择`参数管理`功能。
|
如果你是全模块部署,使用管理员账号,登录智控台,点击顶部`参数字典`,选择`参数管理`功能。
|
||||||
|
|
||||||
然后搜索参数`server.voice_print`,此时,它的值应该是`null`值。
|
然后搜索参数`server.voice_print`,此时,它的值应该是`null`值。
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ package xiaozhi.common.config;
|
|||||||
|
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.http.client.JdkClientHttpRequestFactory;
|
||||||
import org.springframework.web.client.RestTemplate;
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* RestTemplate配置
|
* RestTemplate配置
|
||||||
@@ -12,6 +14,8 @@ public class RestTemplateConfig {
|
|||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public RestTemplate restTemplate() {
|
public RestTemplate restTemplate() {
|
||||||
return new RestTemplate();
|
JdkClientHttpRequestFactory factory = new JdkClientHttpRequestFactory();
|
||||||
|
factory.setReadTimeout(Duration.ofSeconds(30));
|
||||||
|
return new RestTemplate(factory);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -43,7 +43,7 @@ public class SwaggerConfig {
|
|||||||
public GroupedOpenApi oatApi() {
|
public GroupedOpenApi oatApi() {
|
||||||
return GroupedOpenApi.builder()
|
return GroupedOpenApi.builder()
|
||||||
.group("ota")
|
.group("ota")
|
||||||
.pathsToMatch("/ota/**")
|
.pathsToMatch("/ota/**", "/otaMag/**")
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,6 +79,22 @@ public class SwaggerConfig {
|
|||||||
.build();
|
.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
|
@Bean
|
||||||
public OpenAPI customOpenAPI() {
|
public OpenAPI customOpenAPI() {
|
||||||
return new OpenAPI().info(new Info()
|
return new OpenAPI().info(new Info()
|
||||||
|
|||||||
@@ -141,11 +141,31 @@ public interface Constant {
|
|||||||
*/
|
*/
|
||||||
String SERVER_MQTT_SECRET = "server.mqtt_signature_key";
|
String SERVER_MQTT_SECRET = "server.mqtt_signature_key";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WebSocket认证开关
|
||||||
|
*/
|
||||||
|
String SERVER_AUTH_ENABLED = "server.auth.enabled";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 无记忆
|
* 无记忆
|
||||||
*/
|
*/
|
||||||
String MEMORY_NO_MEM = "Memory_nomem";
|
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";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 火山引擎双声道语音克隆
|
* 火山引擎双声道语音克隆
|
||||||
*/
|
*/
|
||||||
@@ -299,7 +319,7 @@ public interface Constant {
|
|||||||
/**
|
/**
|
||||||
* 版本号
|
* 版本号
|
||||||
*/
|
*/
|
||||||
public static final String VERSION = "0.8.8";
|
public static final String VERSION = "0.9.3";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 无效固件URL
|
* 无效固件URL
|
||||||
|
|||||||
@@ -228,4 +228,32 @@ public interface ErrorCode {
|
|||||||
|
|
||||||
// 智能体模板相关错误码(补充)
|
// 智能体模板相关错误码(补充)
|
||||||
int AGENT_TEMPLATE_NOT_FOUND = 10183; // 默认智能体未找到
|
int AGENT_TEMPLATE_NOT_FOUND = 10183; // 默认智能体未找到
|
||||||
|
|
||||||
|
// 知识库适配器相关错误码
|
||||||
|
int RAG_ADAPTER_TYPE_NOT_SUPPORTED = 10184; // 不支持的适配器类型
|
||||||
|
int RAG_CONFIG_VALIDATION_FAILED = 10185; // RAG配置验证失败
|
||||||
|
int RAG_ADAPTER_CREATION_FAILED = 10186; // 适配器创建失败
|
||||||
|
int RAG_ADAPTER_INIT_FAILED = 10187; // 适配器初始化失败
|
||||||
|
int RAG_ADAPTER_CONNECTION_FAILED = 10188; // 适配器连接测试失败
|
||||||
|
int RAG_ADAPTER_OPERATION_FAILED = 10189; // 适配器操作失败
|
||||||
|
int RAG_ADAPTER_NOT_FOUND = 10190; // 适配器未找到
|
||||||
|
int RAG_ADAPTER_CACHE_ERROR = 10191; // 适配器缓存错误
|
||||||
|
int RAG_ADAPTER_TYPE_NOT_FOUND = 10192; // 适配器类型未找到
|
||||||
|
|
||||||
|
// 设备工具相关错误码
|
||||||
|
int DEVICE_ID_NOT_NULL = 10193; // 设备ID不能为空
|
||||||
|
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工具列表
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,23 +13,25 @@ public class RenException extends RuntimeException {
|
|||||||
private String msg;
|
private String msg;
|
||||||
|
|
||||||
public RenException(int code) {
|
public RenException(int code) {
|
||||||
|
super(MessageUtils.getMessage(code));
|
||||||
this.code = code;
|
this.code = code;
|
||||||
this.msg = MessageUtils.getMessage(code);
|
this.msg = MessageUtils.getMessage(code);
|
||||||
}
|
}
|
||||||
|
|
||||||
public RenException(int code, String... params) {
|
public RenException(int code, String... params) {
|
||||||
|
super(MessageUtils.getMessage(code, params));
|
||||||
this.code = code;
|
this.code = code;
|
||||||
this.msg = MessageUtils.getMessage(code, params);
|
this.msg = MessageUtils.getMessage(code, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
public RenException(int code, Throwable e) {
|
public RenException(int code, Throwable e) {
|
||||||
super(e);
|
super(MessageUtils.getMessage(code), e);
|
||||||
this.code = code;
|
this.code = code;
|
||||||
this.msg = MessageUtils.getMessage(code);
|
this.msg = MessageUtils.getMessage(code);
|
||||||
}
|
}
|
||||||
|
|
||||||
public RenException(int code, Throwable e, String... params) {
|
public RenException(int code, Throwable e, String... params) {
|
||||||
super(e);
|
super(MessageUtils.getMessage(code, params), e);
|
||||||
this.code = code;
|
this.code = code;
|
||||||
this.msg = MessageUtils.getMessage(code, params);
|
this.msg = MessageUtils.getMessage(code, params);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -159,4 +159,33 @@ public class RedisKeys {
|
|||||||
public static String getKnowledgeBaseCacheKey(String datasetId) {
|
public static String getKnowledgeBaseCacheKey(String datasetId) {
|
||||||
return "knowledge:base:" + datasetId;
|
return "knowledge:base:" + datasetId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取临时注册设备标记key
|
||||||
|
*/
|
||||||
|
public static String getTmpRegisterMacKey(String deviceId) {
|
||||||
|
return "tmp_register_mac:" + deviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OTA绑定设备
|
||||||
|
*/
|
||||||
|
public static String getOtaActivationCode(String activationCode) {
|
||||||
|
return "ota:activation:code:" + activationCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OTA获取设备mac相关信息
|
||||||
|
*/
|
||||||
|
public static String getOtaDeviceActivationInfo(String deviceId) {
|
||||||
|
return "ota:activation:data:" + deviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OTA上传次数
|
||||||
|
*/
|
||||||
|
public static String getOtaUploadCountKey(Long username) {
|
||||||
|
return "ota:upload:count:" + username;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,28 +12,29 @@ import xiaozhi.modules.sys.service.SysParamsService;
|
|||||||
* 封装了重复的SM2解密、验证码提取和验证逻辑
|
* 封装了重复的SM2解密、验证码提取和验证逻辑
|
||||||
*/
|
*/
|
||||||
public class Sm2DecryptUtil {
|
public class Sm2DecryptUtil {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 验证码长度
|
* 验证码长度
|
||||||
*/
|
*/
|
||||||
private static final int CAPTCHA_LENGTH = 5;
|
private static final int CAPTCHA_LENGTH = 5;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解密SM2加密内容,提取验证码并验证
|
* 解密SM2加密内容,提取验证码并验证
|
||||||
|
*
|
||||||
* @param encryptedPassword SM2加密的密码字符串
|
* @param encryptedPassword SM2加密的密码字符串
|
||||||
* @param captchaId 验证码ID
|
* @param captchaId 验证码ID
|
||||||
* @param captchaService 验证码服务
|
* @param captchaService 验证码服务
|
||||||
* @param sysParamsService 系统参数服务
|
* @param sysParamsService 系统参数服务
|
||||||
* @return 解密后的实际密码
|
* @return 解密后的实际密码
|
||||||
*/
|
*/
|
||||||
public static String decryptAndValidateCaptcha(String encryptedPassword, String captchaId,
|
public static String decryptAndValidateCaptcha(String encryptedPassword, String captchaId,
|
||||||
CaptchaService captchaService, SysParamsService sysParamsService) {
|
CaptchaService captchaService, SysParamsService sysParamsService) {
|
||||||
// 获取SM2私钥
|
// 获取SM2私钥
|
||||||
String privateKeyStr = sysParamsService.getValue(Constant.SM2_PRIVATE_KEY, true);
|
String privateKeyStr = sysParamsService.getValue(Constant.SM2_PRIVATE_KEY, true);
|
||||||
if (StringUtils.isBlank(privateKeyStr)) {
|
if (StringUtils.isBlank(privateKeyStr)) {
|
||||||
throw new RenException(ErrorCode.SM2_KEY_NOT_CONFIGURED);
|
throw new RenException(ErrorCode.SM2_KEY_NOT_CONFIGURED);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用SM2私钥解密密码
|
// 使用SM2私钥解密密码
|
||||||
String decryptedContent;
|
String decryptedContent;
|
||||||
try {
|
try {
|
||||||
@@ -41,19 +42,20 @@ public class Sm2DecryptUtil {
|
|||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new RenException(ErrorCode.SM2_DECRYPT_ERROR);
|
throw new RenException(ErrorCode.SM2_DECRYPT_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 分离验证码和密码:前5位是验证码,后面是密码
|
// 分离验证码和密码:前5位是验证码,后面是密码
|
||||||
if (decryptedContent.length() > CAPTCHA_LENGTH) {
|
if (decryptedContent.length() > CAPTCHA_LENGTH) {
|
||||||
String embeddedCaptcha = decryptedContent.substring(0, CAPTCHA_LENGTH);
|
String embeddedCaptcha = decryptedContent.substring(0, CAPTCHA_LENGTH);
|
||||||
String actualPassword = decryptedContent.substring(CAPTCHA_LENGTH);
|
String actualPassword = decryptedContent.substring(CAPTCHA_LENGTH);
|
||||||
|
|
||||||
// 验证嵌入的验证码是否正确
|
|
||||||
boolean embeddedCaptchaValid = captchaService.validate(captchaId, embeddedCaptcha, true);
|
boolean embeddedCaptchaValid = captchaService.validate(captchaId, embeddedCaptcha, true);
|
||||||
if (!embeddedCaptchaValid) {
|
if (!embeddedCaptchaValid) {
|
||||||
throw new RenException(ErrorCode.SMS_CAPTCHA_ERROR);
|
throw new RenException(ErrorCode.SMS_CAPTCHA_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
return actualPassword;
|
return actualPassword;
|
||||||
|
} else if (decryptedContent.length() > 0) {
|
||||||
|
throw new RenException(ErrorCode.SMS_CAPTCHA_ERROR);
|
||||||
} else {
|
} else {
|
||||||
throw new RenException(ErrorCode.SM2_DECRYPT_ERROR);
|
throw new RenException(ErrorCode.SM2_DECRYPT_ERROR);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package xiaozhi.common.utils;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.ReUtil;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用工具类
|
||||||
|
*/
|
||||||
|
public class ToolUtil {
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(ToolUtil.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对象是否不为空(新增)
|
||||||
|
*/
|
||||||
|
public static boolean isNotEmpty(Object o) {
|
||||||
|
return !isEmpty(o);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对象是否为空
|
||||||
|
*/
|
||||||
|
public static boolean isEmpty(Object o) {
|
||||||
|
if (o == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (o instanceof String) {
|
||||||
|
if (o.toString().trim().equals("")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else if (o instanceof List) {
|
||||||
|
if (((List) o).size() == 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else if (o instanceof Map) {
|
||||||
|
if (((Map) o).size() == 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else if (o instanceof Set) {
|
||||||
|
if (((Set) o).size() == 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else if (o instanceof Object[]) {
|
||||||
|
if (((Object[]) o).length == 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else if (o instanceof int[]) {
|
||||||
|
if (((int[]) o).length == 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else if (o instanceof long[]) {
|
||||||
|
if (((long[]) o).length == 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对象组中是否存在空对象
|
||||||
|
*/
|
||||||
|
public static boolean isOneEmpty(Object... os) {
|
||||||
|
for (Object o : os) {
|
||||||
|
if (isEmpty(o)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对象组中是否全是空对象
|
||||||
|
*/
|
||||||
|
public static boolean isAllEmpty(Object... os) {
|
||||||
|
for (Object o : os) {
|
||||||
|
if (!isEmpty(o)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
+91
-2
@@ -42,8 +42,13 @@ import xiaozhi.modules.agent.dto.AgentMemoryDTO;
|
|||||||
import xiaozhi.modules.agent.dto.AgentUpdateDTO;
|
import xiaozhi.modules.agent.dto.AgentUpdateDTO;
|
||||||
import xiaozhi.modules.agent.entity.AgentEntity;
|
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||||
import xiaozhi.modules.agent.entity.AgentTemplateEntity;
|
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.AgentChatAudioService;
|
||||||
import xiaozhi.modules.agent.service.AgentChatHistoryService;
|
import xiaozhi.modules.agent.service.AgentChatHistoryService;
|
||||||
|
import xiaozhi.modules.agent.service.AgentChatSummaryService;
|
||||||
|
import xiaozhi.modules.agent.service.AgentContextProviderService;
|
||||||
import xiaozhi.modules.agent.service.AgentPluginMappingService;
|
import xiaozhi.modules.agent.service.AgentPluginMappingService;
|
||||||
import xiaozhi.modules.agent.service.AgentService;
|
import xiaozhi.modules.agent.service.AgentService;
|
||||||
import xiaozhi.modules.agent.service.AgentTemplateService;
|
import xiaozhi.modules.agent.service.AgentTemplateService;
|
||||||
@@ -64,14 +69,21 @@ public class AgentController {
|
|||||||
private final AgentChatHistoryService agentChatHistoryService;
|
private final AgentChatHistoryService agentChatHistoryService;
|
||||||
private final AgentChatAudioService agentChatAudioService;
|
private final AgentChatAudioService agentChatAudioService;
|
||||||
private final AgentPluginMappingService agentPluginMappingService;
|
private final AgentPluginMappingService agentPluginMappingService;
|
||||||
|
private final AgentContextProviderService agentContextProviderService;
|
||||||
|
private final AgentChatSummaryService agentChatSummaryService;
|
||||||
private final RedisUtils redisUtils;
|
private final RedisUtils redisUtils;
|
||||||
|
private final AgentTagService agentTagService;
|
||||||
|
|
||||||
@GetMapping("/list")
|
@GetMapping("/list")
|
||||||
@Operation(summary = "获取用户智能体列表")
|
@Operation(summary = "获取用户智能体列表")
|
||||||
@RequiresPermissions("sys:role:normal")
|
@RequiresPermissions("sys:role:normal")
|
||||||
public Result<List<AgentDTO>> getUserAgents() {
|
public Result<List<AgentDTO>> getUserAgents(
|
||||||
|
@RequestParam(value = "keyword", required = false) String keyword,
|
||||||
|
@RequestParam(value = "searchType", defaultValue = "name") String searchType) {
|
||||||
UserDetail user = SecurityUser.getUser();
|
UserDetail user = SecurityUser.getUser();
|
||||||
List<AgentDTO> agents = agentService.getUserAgents(user.getId());
|
|
||||||
|
// 直接调用整合后的getUserAgents方法,无需再区分搜索和普通查询
|
||||||
|
List<AgentDTO> agents = agentService.getUserAgents(user.getId(), keyword, searchType);
|
||||||
return new Result<List<AgentDTO>>().ok(agents);
|
return new Result<List<AgentDTO>>().ok(agents);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,6 +129,34 @@ public class AgentController {
|
|||||||
return new Result<>();
|
return new Result<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/chat-summary/{sessionId}/save")
|
||||||
|
@Operation(summary = "根据会话ID生成聊天记录总结并保存(异步执行)")
|
||||||
|
public Result<Void> generateAndSaveChatSummary(@PathVariable String sessionId) {
|
||||||
|
try {
|
||||||
|
// 异步执行总结生成任务,立即返回成功响应
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
agentChatSummaryService.generateAndSaveChatSummary(sessionId);
|
||||||
|
System.out.println("异步执行会话 " + sessionId + " 的聊天记录总结完成");
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println("异步执行会话 " + sessionId + " 的聊天记录总结失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
|
||||||
|
// 立即返回成功响应,不等待总结生成完成
|
||||||
|
return new Result<Void>().ok(null);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return new Result<Void>().error("启动异步总结生成任务失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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}")
|
@PutMapping("/{id}")
|
||||||
@Operation(summary = "更新智能体")
|
@Operation(summary = "更新智能体")
|
||||||
@RequiresPermissions("sys:role:normal")
|
@RequiresPermissions("sys:role:normal")
|
||||||
@@ -135,6 +175,8 @@ public class AgentController {
|
|||||||
agentChatHistoryService.deleteByAgentId(id, true, true);
|
agentChatHistoryService.deleteByAgentId(id, true, true);
|
||||||
// 删除关联的插件
|
// 删除关联的插件
|
||||||
agentPluginMappingService.deleteByAgentId(id);
|
agentPluginMappingService.deleteByAgentId(id);
|
||||||
|
// 删除关联的上下文源配置
|
||||||
|
agentContextProviderService.deleteByAgentId(id);
|
||||||
// 再删除智能体
|
// 再删除智能体
|
||||||
agentService.deleteById(id);
|
agentService.deleteById(id);
|
||||||
return new Result<>();
|
return new Result<>();
|
||||||
@@ -182,6 +224,7 @@ public class AgentController {
|
|||||||
List<AgentChatHistoryDTO> result = agentChatHistoryService.getChatHistoryBySessionId(id, sessionId);
|
List<AgentChatHistoryDTO> result = agentChatHistoryService.getChatHistoryBySessionId(id, sessionId);
|
||||||
return new Result<List<AgentChatHistoryDTO>>().ok(result);
|
return new Result<List<AgentChatHistoryDTO>>().ok(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{id}/chat-history/user")
|
@GetMapping("/{id}/chat-history/user")
|
||||||
@Operation(summary = "获取智能体聊天记录(用户)")
|
@Operation(summary = "获取智能体聊天记录(用户)")
|
||||||
@RequiresPermissions("sys:role:normal")
|
@RequiresPermissions("sys:role:normal")
|
||||||
@@ -243,4 +286,50 @@ public class AgentController {
|
|||||||
.body(audioData);
|
.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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
+5
-4
@@ -11,6 +11,7 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import xiaozhi.common.exception.ErrorCode;
|
||||||
import xiaozhi.common.user.UserDetail;
|
import xiaozhi.common.user.UserDetail;
|
||||||
import xiaozhi.common.utils.Result;
|
import xiaozhi.common.utils.Result;
|
||||||
import xiaozhi.modules.agent.service.AgentMcpAccessPointService;
|
import xiaozhi.modules.agent.service.AgentMcpAccessPointService;
|
||||||
@@ -28,7 +29,7 @@ public class AgentMcpAccessPointController {
|
|||||||
/**
|
/**
|
||||||
* 获取智能体的Mcp接入点地址
|
* 获取智能体的Mcp接入点地址
|
||||||
*
|
*
|
||||||
* @param audioId 智能体id
|
* @param agentId 智能体id
|
||||||
* @return 返回错误提醒或者Mcp接入点地址
|
* @return 返回错误提醒或者Mcp接入点地址
|
||||||
*/
|
*/
|
||||||
@Operation(summary = "获取智能体的Mcp接入点地址")
|
@Operation(summary = "获取智能体的Mcp接入点地址")
|
||||||
@@ -40,11 +41,11 @@ public class AgentMcpAccessPointController {
|
|||||||
|
|
||||||
// 检查权限
|
// 检查权限
|
||||||
if (!agentService.checkAgentPermission(agentId, user.getId())) {
|
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);
|
String agentMcpAccessAddress = agentMcpAccessPointService.getAgentMcpAccessAddress(agentId);
|
||||||
if (agentMcpAccessAddress == null) {
|
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);
|
return new Result<String>().ok(agentMcpAccessAddress);
|
||||||
}
|
}
|
||||||
@@ -58,7 +59,7 @@ public class AgentMcpAccessPointController {
|
|||||||
|
|
||||||
// 检查权限
|
// 检查权限
|
||||||
if (!agentService.checkAgentPermission(agentId, user.getId())) {
|
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);
|
List<String> agentMcpToolsList = agentMcpAccessPointService.getAgentMcpToolsList(agentId);
|
||||||
return new Result<List<String>>().ok(agentMcpToolsList);
|
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> {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package xiaozhi.modules.agent.dao;
|
||||||
|
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import xiaozhi.common.dao.BaseDao;
|
||||||
|
import xiaozhi.modules.agent.entity.AgentContextProviderEntity;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface AgentContextProviderDao extends BaseDao<AgentContextProviderEntity> {
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
+18
-6
@@ -1,6 +1,9 @@
|
|||||||
package xiaozhi.modules.agent.dao;
|
package xiaozhi.modules.agent.dao;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
|
||||||
@@ -15,12 +18,6 @@ import xiaozhi.modules.agent.entity.AgentChatHistoryEntity;
|
|||||||
*/
|
*/
|
||||||
@Mapper
|
@Mapper
|
||||||
public interface AiAgentChatHistoryDao extends BaseMapper<AgentChatHistoryEntity> {
|
public interface AiAgentChatHistoryDao extends BaseMapper<AgentChatHistoryEntity> {
|
||||||
/**
|
|
||||||
* 根据智能体ID删除音频
|
|
||||||
*
|
|
||||||
* @param agentId 智能体ID
|
|
||||||
*/
|
|
||||||
void deleteAudioByAgentId(String agentId);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据智能体ID删除聊天历史记录
|
* 根据智能体ID删除聊天历史记录
|
||||||
@@ -35,4 +32,19 @@ public interface AiAgentChatHistoryDao extends BaseMapper<AgentChatHistoryEntity
|
|||||||
* @param agentId 智能体ID
|
* @param agentId 智能体ID
|
||||||
*/
|
*/
|
||||||
void deleteAudioIdByAgentId(String agentId);
|
void deleteAudioIdByAgentId(String agentId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据智能体ID获取所有音频ID列表
|
||||||
|
*
|
||||||
|
* @param agentId 智能体ID
|
||||||
|
* @return 音频ID列表
|
||||||
|
*/
|
||||||
|
List<String> getAudioIdsByAgentId(String agentId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量删除音频
|
||||||
|
*
|
||||||
|
* @param audioIds 音频ID列表
|
||||||
|
*/
|
||||||
|
void deleteAudioByIds(@Param("audioIds") List<String> audioIds);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,4 +23,9 @@ public class AgentChatSessionDTO {
|
|||||||
* 聊天条数
|
* 聊天条数
|
||||||
*/
|
*/
|
||||||
private Integer chatCount;
|
private Integer chatCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会话标题
|
||||||
|
*/
|
||||||
|
private String title;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package xiaozhi.modules.agent.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能体聊天记录总结DTO
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "智能体聊天记录总结对象")
|
||||||
|
public class AgentChatSummaryDTO {
|
||||||
|
|
||||||
|
@Schema(description = "会话ID")
|
||||||
|
private String sessionId;
|
||||||
|
|
||||||
|
@Schema(description = "智能体ID")
|
||||||
|
private String agentId;
|
||||||
|
|
||||||
|
@Schema(description = "总结内容")
|
||||||
|
private String summary;
|
||||||
|
|
||||||
|
@Schema(description = "总结状态")
|
||||||
|
private boolean success;
|
||||||
|
|
||||||
|
@Schema(description = "错误信息")
|
||||||
|
private String errorMessage;
|
||||||
|
|
||||||
|
public AgentChatSummaryDTO() {
|
||||||
|
this.success = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AgentChatSummaryDTO(String sessionId, String agentId, String summary) {
|
||||||
|
this.sessionId = sessionId;
|
||||||
|
this.agentId = agentId;
|
||||||
|
this.summary = summary;
|
||||||
|
this.success = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AgentChatSummaryDTO(String sessionId, String errorMessage) {
|
||||||
|
this.sessionId = sessionId;
|
||||||
|
this.errorMessage = errorMessage;
|
||||||
|
this.success = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
package xiaozhi.modules.agent.dto;
|
package xiaozhi.modules.agent.dto;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
import xiaozhi.modules.agent.dto.AgentTagDTO;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 智能体数据传输对象
|
* 智能体数据传输对象
|
||||||
@@ -45,4 +47,7 @@ public class AgentDTO {
|
|||||||
|
|
||||||
@Schema(description = "设备数量", example = "10")
|
@Schema(description = "设备数量", example = "10")
|
||||||
private Integer deviceCount;
|
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;
|
package xiaozhi.modules.agent.dto;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -32,6 +33,9 @@ public class AgentUpdateDTO implements Serializable {
|
|||||||
@Schema(description = "大语言模型标识", example = "llm_model_02", nullable = true)
|
@Schema(description = "大语言模型标识", example = "llm_model_02", nullable = true)
|
||||||
private String llmModelId;
|
private String llmModelId;
|
||||||
|
|
||||||
|
@Schema(description = "小模型标识", example = "slm_model_02", nullable = true)
|
||||||
|
private String slmModelId;
|
||||||
|
|
||||||
@Schema(description = "VLLM模型标识", example = "vllm_model_02", required = false)
|
@Schema(description = "VLLM模型标识", example = "vllm_model_02", required = false)
|
||||||
private String vllmModelId;
|
private String vllmModelId;
|
||||||
|
|
||||||
@@ -41,6 +45,18 @@ public class AgentUpdateDTO implements Serializable {
|
|||||||
@Schema(description = "音色标识", example = "voice_02", nullable = true)
|
@Schema(description = "音色标识", example = "voice_02", nullable = true)
|
||||||
private String ttsVoiceId;
|
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)
|
@Schema(description = "记忆模型标识", example = "mem_model_02", nullable = true)
|
||||||
private String memModelId;
|
private String memModelId;
|
||||||
|
|
||||||
@@ -69,6 +85,9 @@ public class AgentUpdateDTO implements Serializable {
|
|||||||
@Schema(description = "排序", example = "1", nullable = true)
|
@Schema(description = "排序", example = "1", nullable = true)
|
||||||
private Integer sort;
|
private Integer sort;
|
||||||
|
|
||||||
|
@Schema(description = "上下文源配置", nullable = true)
|
||||||
|
private List<ContextProviderDTO> contextProviders;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@Schema(description = "插件函数信息")
|
@Schema(description = "插件函数信息")
|
||||||
public static class FunctionInfo implements Serializable {
|
public static class FunctionInfo implements Serializable {
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package xiaozhi.modules.agent.dto;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Schema(description = "上下文源配置DTO")
|
||||||
|
public class ContextProviderDTO implements Serializable {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Schema(description = "URL地址")
|
||||||
|
private String url;
|
||||||
|
|
||||||
|
@Schema(description = "请求头")
|
||||||
|
private Map<String, Object> headers;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
+43
@@ -0,0 +1,43 @@
|
|||||||
|
package xiaozhi.modules.agent.entity;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
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 com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
import xiaozhi.modules.agent.dto.ContextProviderDTO;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@TableName(value = "ai_agent_context_provider", autoResultMap = true)
|
||||||
|
@Schema(description = "智能体上下文源配置")
|
||||||
|
public class AgentContextProviderEntity {
|
||||||
|
|
||||||
|
@TableId(type = IdType.ASSIGN_UUID)
|
||||||
|
@Schema(description = "主键")
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
@Schema(description = "智能体ID")
|
||||||
|
private String agentId;
|
||||||
|
|
||||||
|
@Schema(description = "上下文源配置")
|
||||||
|
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||||
|
private List<ContextProviderDTO> contextProviders;
|
||||||
|
|
||||||
|
@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;
|
package xiaozhi.modules.agent.entity;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.annotation.IdType;
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
@@ -36,6 +37,9 @@ public class AgentEntity {
|
|||||||
@Schema(description = "大语言模型标识")
|
@Schema(description = "大语言模型标识")
|
||||||
private String llmModelId;
|
private String llmModelId;
|
||||||
|
|
||||||
|
@Schema(description = "小模型标识")
|
||||||
|
private String slmModelId;
|
||||||
|
|
||||||
@Schema(description = "VLLM模型标识")
|
@Schema(description = "VLLM模型标识")
|
||||||
private String vllmModelId;
|
private String vllmModelId;
|
||||||
|
|
||||||
@@ -45,6 +49,18 @@ public class AgentEntity {
|
|||||||
@Schema(description = "音色标识")
|
@Schema(description = "音色标识")
|
||||||
private String ttsVoiceId;
|
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 = "记忆模型标识")
|
@Schema(description = "记忆模型标识")
|
||||||
private String memModelId;
|
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;
|
package xiaozhi.modules.agent.entity;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.annotation.IdType;
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
@@ -64,6 +65,26 @@ public class AgentTemplateEntity implements Serializable {
|
|||||||
*/
|
*/
|
||||||
private String ttsVoiceId;
|
private String ttsVoiceId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 音色语言
|
||||||
|
*/
|
||||||
|
private String ttsLanguage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TTS音量
|
||||||
|
*/
|
||||||
|
private Integer ttsVolume;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TTS语速
|
||||||
|
*/
|
||||||
|
private Integer ttsRate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TTS音调
|
||||||
|
*/
|
||||||
|
private Integer ttsPitch;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 记忆模型标识
|
* 记忆模型标识
|
||||||
*/
|
*/
|
||||||
|
|||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
package xiaozhi.modules.agent.service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能体聊天记录总结服务接口
|
||||||
|
*/
|
||||||
|
public interface AgentChatSummaryService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据会话ID生成聊天记录总结并保存到智能体记忆
|
||||||
|
*
|
||||||
|
* @param sessionId 会话ID
|
||||||
|
* @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);
|
||||||
|
}
|
||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
package xiaozhi.modules.agent.service;
|
||||||
|
|
||||||
|
import xiaozhi.common.service.BaseService;
|
||||||
|
import xiaozhi.modules.agent.entity.AgentContextProviderEntity;
|
||||||
|
|
||||||
|
public interface AgentContextProviderService extends BaseService<AgentContextProviderEntity> {
|
||||||
|
/**
|
||||||
|
* 根据智能体ID获取上下文源配置
|
||||||
|
* @param agentId 智能体ID
|
||||||
|
* @return 上下文源配置实体
|
||||||
|
*/
|
||||||
|
AgentContextProviderEntity getByAgentId(String agentId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存或更新上下文源配置
|
||||||
|
* @param entity 实体
|
||||||
|
*/
|
||||||
|
void saveOrUpdateByAgentId(AgentContextProviderEntity entity);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据智能体ID删除上下文源配置
|
||||||
|
* @param agentId 智能体ID
|
||||||
|
*/
|
||||||
|
void deleteByAgentId(String agentId);
|
||||||
|
}
|
||||||
@@ -54,9 +54,11 @@ public interface AgentService extends BaseService<AgentEntity> {
|
|||||||
* 获取用户智能体列表
|
* 获取用户智能体列表
|
||||||
*
|
*
|
||||||
* @param userId 用户ID
|
* @param userId 用户ID
|
||||||
|
* @param keyword 搜索关键词
|
||||||
|
* @param searchType 搜索类型(name - 按名称搜索,mac - 按MAC地址搜索)
|
||||||
* @return 智能体列表
|
* @return 智能体列表
|
||||||
*/
|
*/
|
||||||
List<AgentDTO> getUserAgents(Long userId);
|
List<AgentDTO> getUserAgents(Long userId, String keyword, String searchType);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据智能体ID获取设备数量
|
* 根据智能体ID获取设备数量
|
||||||
@@ -98,4 +100,6 @@ public interface AgentService extends BaseService<AgentEntity> {
|
|||||||
* @return 创建的智能体ID
|
* @return 创建的智能体ID
|
||||||
*/
|
*/
|
||||||
String createAgent(AgentCreateDTO dto);
|
String createAgent(AgentCreateDTO dto);
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
+6
-2
@@ -17,6 +17,7 @@ import xiaozhi.modules.agent.entity.AgentChatHistoryEntity;
|
|||||||
import xiaozhi.modules.agent.entity.AgentEntity;
|
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||||
import xiaozhi.modules.agent.service.AgentChatAudioService;
|
import xiaozhi.modules.agent.service.AgentChatAudioService;
|
||||||
import xiaozhi.modules.agent.service.AgentChatHistoryService;
|
import xiaozhi.modules.agent.service.AgentChatHistoryService;
|
||||||
|
import xiaozhi.modules.agent.service.AgentChatSummaryService;
|
||||||
import xiaozhi.modules.agent.service.AgentService;
|
import xiaozhi.modules.agent.service.AgentService;
|
||||||
import xiaozhi.modules.agent.service.biz.AgentChatHistoryBizService;
|
import xiaozhi.modules.agent.service.biz.AgentChatHistoryBizService;
|
||||||
import xiaozhi.modules.device.entity.DeviceEntity;
|
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||||
@@ -36,6 +37,7 @@ public class AgentChatHistoryBizServiceImpl implements AgentChatHistoryBizServic
|
|||||||
private final AgentService agentService;
|
private final AgentService agentService;
|
||||||
private final AgentChatHistoryService agentChatHistoryService;
|
private final AgentChatHistoryService agentChatHistoryService;
|
||||||
private final AgentChatAudioService agentChatAudioService;
|
private final AgentChatAudioService agentChatAudioService;
|
||||||
|
private final AgentChatSummaryService agentChatSummaryService;
|
||||||
private final RedisUtils redisUtils;
|
private final RedisUtils redisUtils;
|
||||||
private final DeviceService deviceService;
|
private final DeviceService deviceService;
|
||||||
|
|
||||||
@@ -50,7 +52,8 @@ public class AgentChatHistoryBizServiceImpl implements AgentChatHistoryBizServic
|
|||||||
public Boolean report(AgentChatHistoryReportDTO report) {
|
public Boolean report(AgentChatHistoryReportDTO report) {
|
||||||
String macAddress = report.getMacAddress();
|
String macAddress = report.getMacAddress();
|
||||||
Byte chatType = report.getChatType();
|
Byte chatType = report.getChatType();
|
||||||
Long reportTimeMillis = null != report.getReportTime() ? report.getReportTime() * 1000 : System.currentTimeMillis();
|
Long reportTimeMillis = null != report.getReportTime() ? report.getReportTime()
|
||||||
|
: System.currentTimeMillis();
|
||||||
log.info("小智设备聊天上报请求: macAddress={}, type={} reportTime={}", macAddress, chatType, reportTimeMillis);
|
log.info("小智设备聊天上报请求: macAddress={}, type={} reportTime={}", macAddress, chatType, reportTimeMillis);
|
||||||
|
|
||||||
// 根据设备MAC地址查询对应的默认智能体,判断是否需要上报
|
// 根据设备MAC地址查询对应的默认智能体,判断是否需要上报
|
||||||
@@ -105,7 +108,8 @@ public class AgentChatHistoryBizServiceImpl implements AgentChatHistoryBizServic
|
|||||||
/**
|
/**
|
||||||
* 组装上报数据
|
* 组装上报数据
|
||||||
*/
|
*/
|
||||||
private void saveChatText(AgentChatHistoryReportDTO report, String agentId, String macAddress, String audioId, Long reportTime) {
|
private void saveChatText(AgentChatHistoryReportDTO report, String agentId, String macAddress, String audioId,
|
||||||
|
Long reportTime) {
|
||||||
// 构建聊天记录实体
|
// 构建聊天记录实体
|
||||||
AgentChatHistoryEntity entity = AgentChatHistoryEntity.builder()
|
AgentChatHistoryEntity entity = AgentChatHistoryEntity.builder()
|
||||||
.macAddress(macAddress)
|
.macAddress(macAddress)
|
||||||
|
|||||||
+18
-2
@@ -5,6 +5,8 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import cn.hutool.core.collection.ListUtil;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@@ -18,12 +20,14 @@ import xiaozhi.common.constant.Constant;
|
|||||||
import xiaozhi.common.page.PageData;
|
import xiaozhi.common.page.PageData;
|
||||||
import xiaozhi.common.utils.ConvertUtils;
|
import xiaozhi.common.utils.ConvertUtils;
|
||||||
import xiaozhi.common.utils.JsonUtils;
|
import xiaozhi.common.utils.JsonUtils;
|
||||||
|
import xiaozhi.common.utils.ToolUtil;
|
||||||
import xiaozhi.modules.agent.Enums.AgentChatHistoryType;
|
import xiaozhi.modules.agent.Enums.AgentChatHistoryType;
|
||||||
import xiaozhi.modules.agent.dao.AiAgentChatHistoryDao;
|
import xiaozhi.modules.agent.dao.AiAgentChatHistoryDao;
|
||||||
import xiaozhi.modules.agent.dto.AgentChatHistoryDTO;
|
import xiaozhi.modules.agent.dto.AgentChatHistoryDTO;
|
||||||
import xiaozhi.modules.agent.dto.AgentChatSessionDTO;
|
import xiaozhi.modules.agent.dto.AgentChatSessionDTO;
|
||||||
import xiaozhi.modules.agent.entity.AgentChatHistoryEntity;
|
import xiaozhi.modules.agent.entity.AgentChatHistoryEntity;
|
||||||
import xiaozhi.modules.agent.service.AgentChatHistoryService;
|
import xiaozhi.modules.agent.service.AgentChatHistoryService;
|
||||||
|
import xiaozhi.modules.agent.service.AgentChatTitleService;
|
||||||
import xiaozhi.modules.agent.vo.AgentChatHistoryUserVO;
|
import xiaozhi.modules.agent.vo.AgentChatHistoryUserVO;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -34,9 +38,12 @@ import xiaozhi.modules.agent.vo.AgentChatHistoryUserVO;
|
|||||||
* @since 1.0.0
|
* @since 1.0.0
|
||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
public class AgentChatHistoryServiceImpl extends ServiceImpl<AiAgentChatHistoryDao, AgentChatHistoryEntity>
|
public class AgentChatHistoryServiceImpl extends ServiceImpl<AiAgentChatHistoryDao, AgentChatHistoryEntity>
|
||||||
implements AgentChatHistoryService {
|
implements AgentChatHistoryService {
|
||||||
|
|
||||||
|
private final AgentChatTitleService agentChatTitleService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PageData<AgentChatSessionDTO> getSessionListByAgentId(Map<String, Object> params) {
|
public PageData<AgentChatSessionDTO> getSessionListByAgentId(Map<String, Object> params) {
|
||||||
String agentId = (String) params.get("agentId");
|
String agentId = (String) params.get("agentId");
|
||||||
@@ -59,6 +66,7 @@ public class AgentChatHistoryServiceImpl extends ServiceImpl<AiAgentChatHistoryD
|
|||||||
dto.setSessionId((String) map.get("session_id"));
|
dto.setSessionId((String) map.get("session_id"));
|
||||||
dto.setCreatedAt((LocalDateTime) map.get("created_at"));
|
dto.setCreatedAt((LocalDateTime) map.get("created_at"));
|
||||||
dto.setChatCount(((Number) map.get("chat_count")).intValue());
|
dto.setChatCount(((Number) map.get("chat_count")).intValue());
|
||||||
|
dto.setTitle(agentChatTitleService.getTitleBySessionId(dto.getSessionId()));
|
||||||
return dto;
|
return dto;
|
||||||
}).collect(Collectors.toList());
|
}).collect(Collectors.toList());
|
||||||
|
|
||||||
@@ -84,7 +92,15 @@ public class AgentChatHistoryServiceImpl extends ServiceImpl<AiAgentChatHistoryD
|
|||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void deleteByAgentId(String agentId, Boolean deleteAudio, Boolean deleteText) {
|
public void deleteByAgentId(String agentId, Boolean deleteAudio, Boolean deleteText) {
|
||||||
if (deleteAudio) {
|
if (deleteAudio) {
|
||||||
baseMapper.deleteAudioByAgentId(agentId);
|
// 分批删除音频,避免超时
|
||||||
|
List<String> audioIds = baseMapper.getAudioIdsByAgentId(agentId);
|
||||||
|
if (ToolUtil.isNotEmpty(audioIds)) {
|
||||||
|
// 每批删除1000条
|
||||||
|
List<List<String>> batch = ListUtil.split(audioIds, 1000);
|
||||||
|
batch.forEach(dataList -> {
|
||||||
|
baseMapper.deleteAudioByIds(dataList);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (deleteAudio && !deleteText) {
|
if (deleteAudio && !deleteText) {
|
||||||
baseMapper.deleteAudioIdByAgentId(agentId);
|
baseMapper.deleteAudioIdByAgentId(agentId);
|
||||||
@@ -107,7 +123,7 @@ public class AgentChatHistoryServiceImpl extends ServiceImpl<AiAgentChatHistoryD
|
|||||||
// 添加此行,确保查询结果按照创建时间降序排列
|
// 添加此行,确保查询结果按照创建时间降序排列
|
||||||
// 使用id的原因:数据形式,id越大的创建时间就越晚,所以使用id的结果和创建时间降序排列结果一样
|
// 使用id的原因:数据形式,id越大的创建时间就越晚,所以使用id的结果和创建时间降序排列结果一样
|
||||||
// id作为降序排列的优势,性能高,有主键索引,不用在排序的时候重新进行排除扫描比较
|
// id作为降序排列的优势,性能高,有主键索引,不用在排序的时候重新进行排除扫描比较
|
||||||
.orderByDesc(AgentChatHistoryEntity::getId);
|
.orderByDesc(AgentChatHistoryEntity::getId);
|
||||||
|
|
||||||
// 构建分页查询,查询前50页数据
|
// 构建分页查询,查询前50页数据
|
||||||
Page<AgentChatHistoryEntity> pageParam = new Page<>(0, 50);
|
Page<AgentChatHistoryEntity> pageParam = new Page<>(0, 50);
|
||||||
|
|||||||
+521
@@ -0,0 +1,521 @@
|
|||||||
|
package xiaozhi.modules.agent.service.impl;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
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;
|
||||||
|
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;
|
||||||
|
import xiaozhi.modules.device.service.DeviceService;
|
||||||
|
import xiaozhi.modules.llm.service.LLMService;
|
||||||
|
import xiaozhi.modules.model.entity.ModelConfigEntity;
|
||||||
|
import xiaozhi.modules.model.service.ModelConfigService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能体聊天记录总结服务实现类
|
||||||
|
* 实现Python端mem_local_short.py中的总结逻辑
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AgentChatSummaryServiceImpl implements AgentChatSummaryService {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(AgentChatSummaryServiceImpl.class);
|
||||||
|
|
||||||
|
private final AgentChatHistoryService agentChatHistoryService;
|
||||||
|
private final AgentService agentService;
|
||||||
|
private final AgentChatTitleService agentChatTitleService;
|
||||||
|
private final DeviceService deviceService;
|
||||||
|
private final LLMService llmService;
|
||||||
|
private final ModelConfigService modelConfigService;
|
||||||
|
|
||||||
|
// 总结规则常量
|
||||||
|
private static final int MAX_SUMMARY_LENGTH = 1800; // 最大总结长度
|
||||||
|
private static final Pattern JSON_PATTERN = Pattern.compile("\\{.*?\\}", Pattern.DOTALL);
|
||||||
|
private static final Pattern DEVICE_CONTROL_PATTERN = Pattern.compile("设备控制|设备操作|控制设备|设备状态",
|
||||||
|
Pattern.CASE_INSENSITIVE);
|
||||||
|
private static final Pattern WEATHER_PATTERN = Pattern.compile("天气|温度|湿度|降雨|气象", Pattern.CASE_INSENSITIVE);
|
||||||
|
private static final Pattern DATE_PATTERN = Pattern.compile("日期|时间|星期|月份|年份", Pattern.CASE_INSENSITIVE);
|
||||||
|
|
||||||
|
private AgentChatSummaryDTO generateChatSummary(String sessionId) {
|
||||||
|
try {
|
||||||
|
System.out.println("开始生成会话 " + sessionId + " 的聊天记录总结");
|
||||||
|
|
||||||
|
// 1. 根据sessionId获取聊天记录
|
||||||
|
List<AgentChatHistoryDTO> chatHistory = getChatHistoryBySessionId(sessionId);
|
||||||
|
if (chatHistory == null || chatHistory.isEmpty()) {
|
||||||
|
return new AgentChatSummaryDTO(sessionId, "未找到该会话的聊天记录");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 获取智能体信息
|
||||||
|
String agentId = getAgentIdFromSession(sessionId, chatHistory);
|
||||||
|
if (StringUtils.isBlank(agentId)) {
|
||||||
|
return new AgentChatSummaryDTO(sessionId, "无法获取智能体信息");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 提取关键对话内容
|
||||||
|
List<String> meaningfulMessages = extractMeaningfulMessages(chatHistory);
|
||||||
|
if (meaningfulMessages.isEmpty()) {
|
||||||
|
return new AgentChatSummaryDTO(sessionId, "没有有效的对话内容可总结");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 生成总结(generateSummaryFromMessages方法已包含长度限制逻辑)
|
||||||
|
String summary = generateSummaryFromMessages(meaningfulMessages, agentId);
|
||||||
|
|
||||||
|
log.info("成功生成会话 {} 的聊天记录总结,长度: {} 字符", sessionId, summary.length());
|
||||||
|
return new AgentChatSummaryDTO(sessionId, agentId, summary);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("生成会话 {} 的聊天记录总结时发生错误: {}", sessionId, e.getMessage());
|
||||||
|
return new AgentChatSummaryDTO(sessionId, "生成总结时发生错误: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean generateAndSaveChatSummary(String sessionId) {
|
||||||
|
try {
|
||||||
|
DeviceEntity device = getDeviceBySessionId(sessionId);
|
||||||
|
if (device == null) {
|
||||||
|
log.info("未找到与会话 {} 关联的设备", sessionId);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String agentId = device.getAgentId();
|
||||||
|
String memModelId = agentService.getAgentById(agentId).getMemModelId();
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("保存会话 {} 的聊天记录总结时发生错误: {}", sessionId, e.getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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获取聊天记录
|
||||||
|
*/
|
||||||
|
private List<AgentChatHistoryDTO> getChatHistoryBySessionId(String sessionId) {
|
||||||
|
try {
|
||||||
|
// 这里需要根据sessionId获取聊天记录
|
||||||
|
// 由于现有接口需要agentId,我们需要先找到关联的agentId
|
||||||
|
String agentId = findAgentIdBySessionId(sessionId);
|
||||||
|
if (StringUtils.isBlank(agentId)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return agentChatHistoryService.getChatHistoryBySessionId(agentId, sessionId);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("获取会话 {} 的聊天记录失败: {}", sessionId, e.getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据会话ID查找关联的智能体ID
|
||||||
|
*/
|
||||||
|
private String findAgentIdBySessionId(String sessionId) {
|
||||||
|
try {
|
||||||
|
// 查询该会话的第一条记录获取agentId
|
||||||
|
QueryWrapper<AgentChatHistoryEntity> wrapper = new QueryWrapper<>();
|
||||||
|
wrapper.select("agent_id")
|
||||||
|
.eq("session_id", sessionId)
|
||||||
|
.last("LIMIT 1");
|
||||||
|
|
||||||
|
AgentChatHistoryEntity entity = agentChatHistoryService.getOne(wrapper);
|
||||||
|
return entity != null ? entity.getAgentId() : null;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("根据会话ID {} 查找智能体ID失败: {}", sessionId, e.getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从会话中获取智能体ID
|
||||||
|
*/
|
||||||
|
private String getAgentIdFromSession(String sessionId, List<AgentChatHistoryDTO> chatHistory) {
|
||||||
|
// 直接从数据库查询智能体ID
|
||||||
|
return findAgentIdBySessionId(sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提取有意义的对话内容(只提取用户消息,排除AI回复)
|
||||||
|
*/
|
||||||
|
private List<String> extractMeaningfulMessages(List<AgentChatHistoryDTO> chatHistory) {
|
||||||
|
List<String> meaningfulMessages = new ArrayList<>();
|
||||||
|
|
||||||
|
for (AgentChatHistoryDTO message : chatHistory) {
|
||||||
|
// 只处理用户消息(chatType = 1)
|
||||||
|
if (message.getChatType() != null && message.getChatType() == 1) {
|
||||||
|
String content = extractContentFromMessage(message);
|
||||||
|
if (isMeaningfulMessage(content)) {
|
||||||
|
meaningfulMessages.add(content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return meaningfulMessages;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从消息中提取内容(处理JSON格式)
|
||||||
|
*/
|
||||||
|
private String extractContentFromMessage(AgentChatHistoryDTO message) {
|
||||||
|
String content = message.getContent();
|
||||||
|
if (StringUtils.isBlank(content)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理JSON格式内容(与前端ChatHistoryDialog.vue逻辑一致)
|
||||||
|
Matcher matcher = JSON_PATTERN.matcher(content);
|
||||||
|
if (matcher.find()) {
|
||||||
|
String jsonContent = matcher.group();
|
||||||
|
// 简化处理:提取JSON中的文本内容
|
||||||
|
return extractTextFromJson(jsonContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从JSON中提取文本内容
|
||||||
|
*/
|
||||||
|
private String extractTextFromJson(String jsonContent) {
|
||||||
|
// 简化处理:提取"content"字段的值
|
||||||
|
Pattern contentPattern = Pattern.compile("\"content\"\s*:\s*\"([^\"]*)\"");
|
||||||
|
Matcher matcher = contentPattern.matcher(jsonContent);
|
||||||
|
if (matcher.find()) {
|
||||||
|
return matcher.group(1);
|
||||||
|
}
|
||||||
|
return jsonContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否为有意义的消息
|
||||||
|
*/
|
||||||
|
private boolean isMeaningfulMessage(String content) {
|
||||||
|
if (StringUtils.isBlank(content)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 排除设备控制信息
|
||||||
|
if (DEVICE_CONTROL_PATTERN.matcher(content).find()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 排除日期天气等无关内容
|
||||||
|
if (WEATHER_PATTERN.matcher(content).find() || DATE_PATTERN.matcher(content).find()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 排除过短的消息
|
||||||
|
return content.length() >= 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从消息生成总结
|
||||||
|
*/
|
||||||
|
private String generateSummaryFromMessages(List<String> messages, String agentId) {
|
||||||
|
if (messages.isEmpty()) {
|
||||||
|
return "本次对话内容较少,没有需要总结的重要信息。";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建完整的对话内容
|
||||||
|
StringBuilder conversation = new StringBuilder();
|
||||||
|
for (int i = 0; i < messages.size(); i++) {
|
||||||
|
conversation.append("消息").append(i + 1).append(": ").append(messages.get(i)).append("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取当前智能体的历史记忆
|
||||||
|
String historyMemory = getCurrentAgentMemory(agentId);
|
||||||
|
|
||||||
|
// 调用LLM服务进行智能总结,传递agentId以获取正确的模型配置
|
||||||
|
String summary = callJavaLLMForSummaryWithHistory(conversation.toString(), historyMemory, agentId);
|
||||||
|
|
||||||
|
// 应用总结规则:限制最大长度
|
||||||
|
if (summary.length() > MAX_SUMMARY_LENGTH) {
|
||||||
|
summary = summary.substring(0, MAX_SUMMARY_LENGTH) + "...";
|
||||||
|
}
|
||||||
|
|
||||||
|
return summary;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("调用Java端LLM服务失败: {}", e.getMessage());
|
||||||
|
throw new RuntimeException("LLM服务不可用,无法生成聊天总结");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前智能体的历史记忆
|
||||||
|
*/
|
||||||
|
private String getCurrentAgentMemory(String agentId) {
|
||||||
|
try {
|
||||||
|
if (StringUtils.isBlank(agentId)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取智能体信息
|
||||||
|
AgentInfoVO agentInfo = agentService.getAgentById(agentId);
|
||||||
|
if (agentInfo == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回智能体的当前总结记忆
|
||||||
|
return agentInfo.getSummaryMemory();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("获取智能体历史记忆失败,agentId: {}, 错误: {}", agentId, e.getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调用Java端LLM服务进行智能总结(支持历史记忆合并)
|
||||||
|
*/
|
||||||
|
private String callJavaLLMForSummaryWithHistory(String conversation, String historyMemory, String agentId) {
|
||||||
|
try {
|
||||||
|
String modelId = getSlmModelId(agentId);
|
||||||
|
|
||||||
|
if (StringUtils.isBlank(modelId)) {
|
||||||
|
log.info("未找到SLM模型,使用默认LLM服务");
|
||||||
|
return llmService.generateSummaryWithHistory(conversation, historyMemory, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
String summary = llmService.generateSummaryWithHistory(conversation, historyMemory, null, modelId);
|
||||||
|
|
||||||
|
if (StringUtils.isNotBlank(summary) && !summary.equals("服务暂不可用") && !summary.equals("总结生成失败")) {
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new RuntimeException("Java端LLM服务返回异常: " + summary);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("调用Java端LLM服务异常,agentId: {}, 错误: {}", agentId, e.getMessage());
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调用Java端LLM服务进行智能总结
|
||||||
|
*/
|
||||||
|
private String callJavaLLMForSummary(String conversation, String agentId) {
|
||||||
|
try {
|
||||||
|
String modelId = getSlmModelId(agentId);
|
||||||
|
|
||||||
|
if (StringUtils.isBlank(modelId)) {
|
||||||
|
log.info("未找到SLM模型,使用默认LLM服务");
|
||||||
|
return llmService.generateSummary(conversation);
|
||||||
|
}
|
||||||
|
|
||||||
|
String summary = llmService.generateSummaryWithModel(conversation, modelId);
|
||||||
|
|
||||||
|
if (StringUtils.isNotBlank(summary) && !summary.equals("服务暂不可用") && !summary.equals("总结生成失败")) {
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new RuntimeException("Java端LLM服务返回异常: " + summary);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("调用Java端LLM服务异常,agentId: {}, 错误: {}", agentId, e.getMessage());
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取记忆总结的LLM模型ID
|
||||||
|
*/
|
||||||
|
private String getMemorySummaryModelId(String agentId) {
|
||||||
|
try {
|
||||||
|
if (StringUtils.isBlank(agentId)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取智能体信息
|
||||||
|
AgentInfoVO agentInfo = agentService.getAgentById(agentId);
|
||||||
|
if (agentInfo == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取智能体的记忆模型ID
|
||||||
|
String memModelId = agentInfo.getMemModelId();
|
||||||
|
if (StringUtils.isBlank(memModelId)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取记忆模型配置
|
||||||
|
ModelConfigEntity memModelConfig = modelConfigService.getModelByIdFromCache(memModelId);
|
||||||
|
if (memModelConfig == null || memModelConfig.getConfigJson() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从记忆模型配置中提取对应的LLM模型ID
|
||||||
|
Map<String, Object> configMap = memModelConfig.getConfigJson();
|
||||||
|
String llmModelId = (String) configMap.get("llm");
|
||||||
|
|
||||||
|
if (StringUtils.isBlank(llmModelId)) {
|
||||||
|
// 如果记忆模型没有配置独立的LLM,则使用智能体的默认LLM模型
|
||||||
|
return agentInfo.getLlmModelId();
|
||||||
|
}
|
||||||
|
|
||||||
|
return llmModelId;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("获取记忆总结LLM模型ID失败,agentId: {}, 错误: {}", agentId, e.getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据会话ID获取设备信息
|
||||||
|
*/
|
||||||
|
private DeviceEntity getDeviceBySessionId(String sessionId) {
|
||||||
|
try {
|
||||||
|
// 查询该会话的第一条记录获取macAddress
|
||||||
|
QueryWrapper<AgentChatHistoryEntity> wrapper = new QueryWrapper<>();
|
||||||
|
wrapper.select("mac_address")
|
||||||
|
.eq("session_id", sessionId)
|
||||||
|
.last("LIMIT 1");
|
||||||
|
|
||||||
|
AgentChatHistoryEntity entity = agentChatHistoryService.getOne(wrapper);
|
||||||
|
if (entity != null && StringUtils.isNotBlank(entity.getMacAddress())) {
|
||||||
|
return deviceService.getDeviceByMacAddress(entity.getMacAddress());
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("根据会话ID {} 查找设备信息失败: {}", sessionId, e.getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
package xiaozhi.modules.agent.service.impl;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
|
|
||||||
|
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||||
|
import xiaozhi.modules.agent.dao.AgentContextProviderDao;
|
||||||
|
import xiaozhi.modules.agent.entity.AgentContextProviderEntity;
|
||||||
|
import xiaozhi.modules.agent.service.AgentContextProviderService;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class AgentContextProviderServiceImpl extends BaseServiceImpl<AgentContextProviderDao, AgentContextProviderEntity> implements AgentContextProviderService {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AgentContextProviderEntity getByAgentId(String agentId) {
|
||||||
|
return baseDao.selectOne(new QueryWrapper<AgentContextProviderEntity>().eq("agent_id", agentId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void saveOrUpdateByAgentId(AgentContextProviderEntity entity) {
|
||||||
|
AgentContextProviderEntity exist = getByAgentId(entity.getAgentId());
|
||||||
|
if (exist != null) {
|
||||||
|
entity.setId(exist.getId());
|
||||||
|
updateById(entity);
|
||||||
|
} else {
|
||||||
|
insert(entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deleteByAgentId(String agentId) {
|
||||||
|
baseDao.delete(new QueryWrapper<AgentContextProviderEntity>().eq("agent_id", agentId));
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
@@ -147,6 +147,7 @@ public class AgentMcpAccessPointServiceImpl implements AgentMcpAccessPointServic
|
|||||||
List<String> result = toolsList.stream()
|
List<String> result = toolsList.stream()
|
||||||
.map(tool -> (String) tool.get("name"))
|
.map(tool -> (String) tool.get("name"))
|
||||||
.filter(name -> name != null)
|
.filter(name -> name != null)
|
||||||
|
.sorted()
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
log.info("成功获取MCP工具列表,智能体ID: {}, 工具数量: {}", id, result.size());
|
log.info("成功获取MCP工具列表,智能体ID: {}, 工具数量: {}", id, result.size());
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
+171
-35
@@ -5,6 +5,7 @@ import java.util.Date;
|
|||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
@@ -28,18 +29,26 @@ import xiaozhi.common.service.impl.BaseServiceImpl;
|
|||||||
import xiaozhi.common.user.UserDetail;
|
import xiaozhi.common.user.UserDetail;
|
||||||
import xiaozhi.common.utils.ConvertUtils;
|
import xiaozhi.common.utils.ConvertUtils;
|
||||||
import xiaozhi.common.utils.JsonUtils;
|
import xiaozhi.common.utils.JsonUtils;
|
||||||
|
import xiaozhi.common.utils.ToolUtil;
|
||||||
import xiaozhi.modules.agent.dao.AgentDao;
|
import xiaozhi.modules.agent.dao.AgentDao;
|
||||||
|
import xiaozhi.modules.agent.dao.AgentTagDao;
|
||||||
import xiaozhi.modules.agent.dto.AgentCreateDTO;
|
import xiaozhi.modules.agent.dto.AgentCreateDTO;
|
||||||
import xiaozhi.modules.agent.dto.AgentDTO;
|
import xiaozhi.modules.agent.dto.AgentDTO;
|
||||||
|
import xiaozhi.modules.agent.dto.AgentTagDTO;
|
||||||
import xiaozhi.modules.agent.dto.AgentUpdateDTO;
|
import xiaozhi.modules.agent.dto.AgentUpdateDTO;
|
||||||
|
import xiaozhi.modules.agent.entity.AgentContextProviderEntity;
|
||||||
import xiaozhi.modules.agent.entity.AgentEntity;
|
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||||
import xiaozhi.modules.agent.entity.AgentPluginMapping;
|
import xiaozhi.modules.agent.entity.AgentPluginMapping;
|
||||||
|
import xiaozhi.modules.agent.entity.AgentTagEntity;
|
||||||
import xiaozhi.modules.agent.entity.AgentTemplateEntity;
|
import xiaozhi.modules.agent.entity.AgentTemplateEntity;
|
||||||
import xiaozhi.modules.agent.service.AgentChatHistoryService;
|
import xiaozhi.modules.agent.service.AgentChatHistoryService;
|
||||||
|
import xiaozhi.modules.agent.service.AgentContextProviderService;
|
||||||
import xiaozhi.modules.agent.service.AgentPluginMappingService;
|
import xiaozhi.modules.agent.service.AgentPluginMappingService;
|
||||||
import xiaozhi.modules.agent.service.AgentService;
|
import xiaozhi.modules.agent.service.AgentService;
|
||||||
|
import xiaozhi.modules.agent.service.AgentTagService;
|
||||||
import xiaozhi.modules.agent.service.AgentTemplateService;
|
import xiaozhi.modules.agent.service.AgentTemplateService;
|
||||||
import xiaozhi.modules.agent.vo.AgentInfoVO;
|
import xiaozhi.modules.agent.vo.AgentInfoVO;
|
||||||
|
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||||
import xiaozhi.modules.device.service.DeviceService;
|
import xiaozhi.modules.device.service.DeviceService;
|
||||||
import xiaozhi.modules.model.dto.ModelProviderDTO;
|
import xiaozhi.modules.model.dto.ModelProviderDTO;
|
||||||
import xiaozhi.modules.model.dto.VoiceDTO;
|
import xiaozhi.modules.model.dto.VoiceDTO;
|
||||||
@@ -54,6 +63,7 @@ import xiaozhi.modules.timbre.service.TimbreService;
|
|||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> implements AgentService {
|
public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> implements AgentService {
|
||||||
private final AgentDao agentDao;
|
private final AgentDao agentDao;
|
||||||
|
private final AgentTagDao agentTagDao;
|
||||||
private final TimbreService timbreModelService;
|
private final TimbreService timbreModelService;
|
||||||
private final ModelConfigService modelConfigService;
|
private final ModelConfigService modelConfigService;
|
||||||
private final RedisUtils redisUtils;
|
private final RedisUtils redisUtils;
|
||||||
@@ -62,6 +72,8 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
|||||||
private final AgentChatHistoryService agentChatHistoryService;
|
private final AgentChatHistoryService agentChatHistoryService;
|
||||||
private final AgentTemplateService agentTemplateService;
|
private final AgentTemplateService agentTemplateService;
|
||||||
private final ModelProviderService modelProviderService;
|
private final ModelProviderService modelProviderService;
|
||||||
|
private final AgentContextProviderService agentContextProviderService;
|
||||||
|
private final AgentTagService agentTagService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PageData<AgentEntity> adminAgentList(Map<String, Object> params) {
|
public PageData<AgentEntity> adminAgentList(Map<String, Object> params) {
|
||||||
@@ -81,10 +93,17 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
|||||||
|
|
||||||
if (agent.getMemModelId() != null && agent.getMemModelId().equals(Constant.MEMORY_NO_MEM)) {
|
if (agent.getMemModelId() != null && agent.getMemModelId().equals(Constant.MEMORY_NO_MEM)) {
|
||||||
agent.setChatHistoryConf(Constant.ChatHistoryConfEnum.IGNORE.getCode());
|
agent.setChatHistoryConf(Constant.ChatHistoryConfEnum.IGNORE.getCode());
|
||||||
if (agent.getChatHistoryConf() == null) {
|
|
||||||
agent.setChatHistoryConf(Constant.ChatHistoryConfEnum.RECORD_TEXT_AUDIO.getCode());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if (agent.getChatHistoryConf() == null) {
|
||||||
|
agent.setChatHistoryConf(Constant.ChatHistoryConfEnum.RECORD_TEXT_AUDIO.getCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询上下文源配置
|
||||||
|
AgentContextProviderEntity contextProviderEntity = agentContextProviderService.getByAgentId(id);
|
||||||
|
if (contextProviderEntity != null) {
|
||||||
|
agent.setContextProviders(contextProviderEntity.getContextProviders());
|
||||||
|
}
|
||||||
|
|
||||||
// 无需额外查询插件列表,已通过SQL查询出来
|
// 无需额外查询插件列表,已通过SQL查询出来
|
||||||
return agent;
|
return agent;
|
||||||
}
|
}
|
||||||
@@ -117,38 +136,91 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<AgentDTO> getUserAgents(Long userId) {
|
public List<AgentDTO> getUserAgents(Long userId, String keyword, String searchType) {
|
||||||
QueryWrapper<AgentEntity> wrapper = new QueryWrapper<>();
|
QueryWrapper<AgentEntity> queryWrapper = new QueryWrapper<>();
|
||||||
wrapper.eq("user_id", userId);
|
queryWrapper.eq("user_id", userId).orderByDesc("created_at");
|
||||||
List<AgentEntity> agents = agentDao.selectList(wrapper);
|
|
||||||
return agents.stream().map(agent -> {
|
|
||||||
AgentDTO dto = new AgentDTO();
|
|
||||||
dto.setId(agent.getId());
|
|
||||||
dto.setAgentName(agent.getAgentName());
|
|
||||||
dto.setSystemPrompt(agent.getSystemPrompt());
|
|
||||||
|
|
||||||
// 获取 TTS 模型名称
|
// 如果有搜索关键词,根据搜索类型添加相应的查询条件
|
||||||
dto.setTtsModelName(modelConfigService.getModelNameById(agent.getTtsModelId()));
|
if (StringUtils.isNotBlank(keyword)) {
|
||||||
|
if ("mac".equals(searchType)) {
|
||||||
|
// 按MAC地址搜索:先搜索设备,再获取对应的智能体
|
||||||
|
List<DeviceEntity> devices = Optional
|
||||||
|
.ofNullable(deviceService.searchDevicesByMacAddress(keyword, userId)).orElseGet(ArrayList::new);
|
||||||
|
// 获取设备对应的智能体ID列表
|
||||||
|
List<String> agentIds = devices.stream()
|
||||||
|
.map(DeviceEntity::getAgentId)
|
||||||
|
.distinct()
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
if (ToolUtil.isNotEmpty(agentIds)) {
|
||||||
|
queryWrapper.in("id", agentIds);
|
||||||
|
} else {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 按名称搜索(默认):同时搜索智能体名称和标签名
|
||||||
|
List<String> tagAgentIds = agentTagService.getAgentIdsByTagName(keyword);
|
||||||
|
if (ToolUtil.isNotEmpty(tagAgentIds)) {
|
||||||
|
queryWrapper.and(wrapper -> wrapper
|
||||||
|
.like("agent_name", keyword)
|
||||||
|
.or()
|
||||||
|
.in("id", tagAgentIds));
|
||||||
|
} else {
|
||||||
|
queryWrapper.like("agent_name", keyword);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 获取 LLM 模型名称
|
// 执行查询
|
||||||
dto.setLlmModelName(modelConfigService.getModelNameById(agent.getLlmModelId()));
|
List<AgentEntity> agentEntities = baseDao.selectList(queryWrapper);
|
||||||
|
|
||||||
// 获取 VLLM 模型名称
|
// 转换为DTO并设置所有必要字段
|
||||||
dto.setVllmModelName(modelConfigService.getModelNameById(agent.getVllmModelId()));
|
return agentEntities.stream().map(this::buildAgentDTO).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
// 获取记忆模型名称
|
/**
|
||||||
dto.setMemModelId(agent.getMemModelId());
|
* 将AgentEntity转换为AgentDTO
|
||||||
|
*/
|
||||||
|
private AgentDTO buildAgentDTO(AgentEntity agent) {
|
||||||
|
AgentDTO dto = new AgentDTO();
|
||||||
|
dto.setId(agent.getId());
|
||||||
|
dto.setAgentName(agent.getAgentName());
|
||||||
|
dto.setSystemPrompt(agent.getSystemPrompt());
|
||||||
|
|
||||||
// 获取 TTS 音色名称
|
// 获取 TTS 模型名称
|
||||||
dto.setTtsVoiceName(timbreModelService.getTimbreNameById(agent.getTtsVoiceId()));
|
dto.setTtsModelName(modelConfigService.getModelNameById(agent.getTtsModelId()));
|
||||||
|
|
||||||
// 获取智能体最近的最后连接时长
|
// 获取 LLM 模型名称
|
||||||
dto.setLastConnectedAt(deviceService.getLatestLastConnectionTime(agent.getId()));
|
dto.setLlmModelName(modelConfigService.getModelNameById(agent.getLlmModelId()));
|
||||||
|
|
||||||
// 获取设备数量
|
// 获取 VLLM 模型名称
|
||||||
dto.setDeviceCount(getDeviceCountByAgentId(agent.getId()));
|
dto.setVllmModelName(modelConfigService.getModelNameById(agent.getVllmModelId()));
|
||||||
return dto;
|
|
||||||
}).collect(Collectors.toList());
|
// 获取记忆模型名称
|
||||||
|
dto.setMemModelId(agent.getMemModelId());
|
||||||
|
|
||||||
|
// 获取 TTS 音色名称
|
||||||
|
dto.setTtsVoiceName(timbreModelService.getTimbreNameById(agent.getTtsVoiceId()));
|
||||||
|
|
||||||
|
// 获取智能体最近的最后连接时长
|
||||||
|
dto.setLastConnectedAt(deviceService.getLatestLastConnectionTime(agent.getId()));
|
||||||
|
|
||||||
|
// 获取设备数量
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -228,6 +300,9 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
|||||||
if (dto.getLlmModelId() != null) {
|
if (dto.getLlmModelId() != null) {
|
||||||
existingEntity.setLlmModelId(dto.getLlmModelId());
|
existingEntity.setLlmModelId(dto.getLlmModelId());
|
||||||
}
|
}
|
||||||
|
if (dto.getSlmModelId() != null) {
|
||||||
|
existingEntity.setSlmModelId(dto.getSlmModelId());
|
||||||
|
}
|
||||||
if (dto.getVllmModelId() != null) {
|
if (dto.getVllmModelId() != null) {
|
||||||
existingEntity.setVllmModelId(dto.getVllmModelId());
|
existingEntity.setVllmModelId(dto.getVllmModelId());
|
||||||
}
|
}
|
||||||
@@ -237,6 +312,18 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
|||||||
if (dto.getTtsVoiceId() != null) {
|
if (dto.getTtsVoiceId() != null) {
|
||||||
existingEntity.setTtsVoiceId(dto.getTtsVoiceId());
|
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) {
|
if (dto.getMemModelId() != null) {
|
||||||
existingEntity.setMemModelId(dto.getMemModelId());
|
existingEntity.setMemModelId(dto.getMemModelId());
|
||||||
}
|
}
|
||||||
@@ -322,13 +409,22 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
|||||||
existingEntity.setUpdatedAt(new Date());
|
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);
|
agentChatHistoryService.deleteByAgentId(existingEntity.getId(), true, true);
|
||||||
existingEntity.setSummaryMemory("");
|
existingEntity.setSummaryMemory("");
|
||||||
} else if (existingEntity.getChatHistoryConf() != null && existingEntity.getChatHistoryConf() == 1) {
|
// 删除记忆
|
||||||
// 删除音频数据
|
} else if (existingEntity.getMemModelId() != null
|
||||||
agentChatHistoryService.deleteByAgentId(existingEntity.getId(), true, false);
|
&& existingEntity.getMemModelId().equals(Constant.MEMORY_MEM_REPORT_ONLY)) {
|
||||||
|
existingEntity.setSummaryMemory("");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新上下文源配置
|
||||||
|
if (dto.getContextProviders() != null) {
|
||||||
|
AgentContextProviderEntity contextEntity = new AgentContextProviderEntity();
|
||||||
|
contextEntity.setAgentId(agentId);
|
||||||
|
contextEntity.setContextProviders(dto.getContextProviders());
|
||||||
|
agentContextProviderService.saveOrUpdateByAgentId(contextEntity);
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean b = validateLLMIntentParams(dto.getLlmModelId(), dto.getIntentModelId());
|
boolean b = validateLLMIntentParams(dto.getLlmModelId(), dto.getIntentModelId());
|
||||||
@@ -395,11 +491,31 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
|||||||
entity.setIntentModelId(template.getIntentModelId());
|
entity.setIntentModelId(template.getIntentModelId());
|
||||||
entity.setSystemPrompt(template.getSystemPrompt());
|
entity.setSystemPrompt(template.getSystemPrompt());
|
||||||
entity.setSummaryMemory(template.getSummaryMemory());
|
entity.setSummaryMemory(template.getSummaryMemory());
|
||||||
entity.setChatHistoryConf(template.getChatHistoryConf());
|
|
||||||
|
// 根据记忆模型类型设置默认的chatHistoryConf值
|
||||||
|
if (template.getMemModelId() != null) {
|
||||||
|
if (template.getMemModelId().equals("Memory_nomem")) {
|
||||||
|
// 无记忆功能的模型,默认不记录聊天记录
|
||||||
|
entity.setChatHistoryConf(0);
|
||||||
|
} else {
|
||||||
|
// 有记忆功能的模型,默认记录文本和语音
|
||||||
|
entity.setChatHistoryConf(2);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
entity.setChatHistoryConf(template.getChatHistoryConf());
|
||||||
|
}
|
||||||
|
|
||||||
entity.setLangCode(template.getLangCode());
|
entity.setLangCode(template.getLangCode());
|
||||||
entity.setLanguage(template.getLanguage());
|
entity.setLanguage(template.getLanguage());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (entity.getSlmModelId() == null) {
|
||||||
|
String defaultSlmModelId = getDefaultLLMModelId();
|
||||||
|
if (defaultSlmModelId != null) {
|
||||||
|
entity.setSlmModelId(defaultSlmModelId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 设置用户ID和创建者信息
|
// 设置用户ID和创建者信息
|
||||||
UserDetail user = SecurityUser.getUser();
|
UserDetail user = SecurityUser.getUser();
|
||||||
entity.setUserId(user.getId());
|
entity.setUserId(user.getId());
|
||||||
@@ -437,4 +553,24 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
|||||||
agentPluginMappingService.saveBatch(toInsert);
|
agentPluginMappingService.saveBatch(toInsert);
|
||||||
return entity.getId();
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
|||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
|
import xiaozhi.modules.agent.dto.ContextProviderDTO;
|
||||||
import xiaozhi.modules.agent.entity.AgentEntity;
|
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||||
import xiaozhi.modules.agent.entity.AgentPluginMapping;
|
import xiaozhi.modules.agent.entity.AgentPluginMapping;
|
||||||
|
|
||||||
@@ -21,4 +22,7 @@ public class AgentInfoVO extends AgentEntity
|
|||||||
@Schema(description = "插件列表Id")
|
@Schema(description = "插件列表Id")
|
||||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||||
private List<AgentPluginMapping> functions;
|
private List<AgentPluginMapping> functions;
|
||||||
|
|
||||||
|
@Schema(description = "上下文源配置")
|
||||||
|
private List<ContextProviderDTO> contextProviders;
|
||||||
}
|
}
|
||||||
|
|||||||
+68
-7
@@ -20,10 +20,12 @@ import xiaozhi.common.redis.RedisUtils;
|
|||||||
import xiaozhi.common.utils.ConvertUtils;
|
import xiaozhi.common.utils.ConvertUtils;
|
||||||
import xiaozhi.common.utils.JsonUtils;
|
import xiaozhi.common.utils.JsonUtils;
|
||||||
import xiaozhi.modules.agent.dao.AgentVoicePrintDao;
|
import xiaozhi.modules.agent.dao.AgentVoicePrintDao;
|
||||||
|
import xiaozhi.modules.agent.entity.AgentContextProviderEntity;
|
||||||
import xiaozhi.modules.agent.entity.AgentEntity;
|
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||||
import xiaozhi.modules.agent.entity.AgentPluginMapping;
|
import xiaozhi.modules.agent.entity.AgentPluginMapping;
|
||||||
import xiaozhi.modules.agent.entity.AgentTemplateEntity;
|
import xiaozhi.modules.agent.entity.AgentTemplateEntity;
|
||||||
import xiaozhi.modules.agent.entity.AgentVoicePrintEntity;
|
import xiaozhi.modules.agent.entity.AgentVoicePrintEntity;
|
||||||
|
import xiaozhi.modules.agent.service.AgentContextProviderService;
|
||||||
import xiaozhi.modules.agent.service.AgentMcpAccessPointService;
|
import xiaozhi.modules.agent.service.AgentMcpAccessPointService;
|
||||||
import xiaozhi.modules.agent.service.AgentPluginMappingService;
|
import xiaozhi.modules.agent.service.AgentPluginMappingService;
|
||||||
import xiaozhi.modules.agent.service.AgentService;
|
import xiaozhi.modules.agent.service.AgentService;
|
||||||
@@ -53,6 +55,7 @@ public class ConfigServiceImpl implements ConfigService {
|
|||||||
private final TimbreService timbreService;
|
private final TimbreService timbreService;
|
||||||
private final AgentPluginMappingService agentPluginMappingService;
|
private final AgentPluginMappingService agentPluginMappingService;
|
||||||
private final AgentMcpAccessPointService agentMcpAccessPointService;
|
private final AgentMcpAccessPointService agentMcpAccessPointService;
|
||||||
|
private final AgentContextProviderService agentContextProviderService;
|
||||||
private final VoiceCloneService cloneVoiceService;
|
private final VoiceCloneService cloneVoiceService;
|
||||||
private final AgentVoicePrintDao agentVoicePrintDao;
|
private final AgentVoicePrintDao agentVoicePrintDao;
|
||||||
|
|
||||||
@@ -84,6 +87,10 @@ public class ConfigServiceImpl implements ConfigService {
|
|||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
agent.getVadModelId(),
|
agent.getVadModelId(),
|
||||||
agent.getAsrModelId(),
|
agent.getAsrModelId(),
|
||||||
null,
|
null,
|
||||||
@@ -92,6 +99,7 @@ public class ConfigServiceImpl implements ConfigService {
|
|||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
|
null,
|
||||||
result,
|
result,
|
||||||
isCache);
|
isCache);
|
||||||
|
|
||||||
@@ -103,6 +111,15 @@ public class ConfigServiceImpl implements ConfigService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Map<String, Object> getAgentModels(String macAddress, Map<String, String> selectedModule) {
|
public Map<String, Object> getAgentModels(String macAddress, Map<String, String> selectedModule) {
|
||||||
|
// 检查是否为管理控制台请求
|
||||||
|
String redisKey = RedisKeys.getTmpRegisterMacKey(macAddress);
|
||||||
|
Object isAdminRequest = redisUtils.get(redisKey);
|
||||||
|
|
||||||
|
if (isAdminRequest != null && "true".equals(isAdminRequest)) {
|
||||||
|
// 管理控制台请求,返回getConfig的结果
|
||||||
|
redisUtils.delete(redisKey); // 使用后清理
|
||||||
|
return (Map<String, Object>) getConfig(true);
|
||||||
|
}
|
||||||
// 根据MAC地址查找设备
|
// 根据MAC地址查找设备
|
||||||
DeviceEntity device = deviceService.getDeviceByMacAddress(macAddress);
|
DeviceEntity device = deviceService.getDeviceByMacAddress(macAddress);
|
||||||
if (device == null) {
|
if (device == null) {
|
||||||
@@ -123,15 +140,24 @@ public class ConfigServiceImpl implements ConfigService {
|
|||||||
String voice = null;
|
String voice = null;
|
||||||
String referenceAudio = null;
|
String referenceAudio = null;
|
||||||
String referenceText = null;
|
String referenceText = null;
|
||||||
|
String language = null;
|
||||||
TimbreDetailsVO timbre = timbreService.get(agent.getTtsVoiceId());
|
TimbreDetailsVO timbre = timbreService.get(agent.getTtsVoiceId());
|
||||||
if (timbre != null) {
|
if (timbre != null) {
|
||||||
voice = timbre.getTtsVoice();
|
voice = timbre.getTtsVoice();
|
||||||
referenceAudio = timbre.getReferenceAudio();
|
referenceAudio = timbre.getReferenceAudio();
|
||||||
referenceText = timbre.getReferenceText();
|
referenceText = timbre.getReferenceText();
|
||||||
|
// 优先使用用户选择的语言,如果没有则使用音色支持的第一个语言
|
||||||
|
if (StringUtils.isNotBlank(agent.getTtsLanguage())) {
|
||||||
|
language = agent.getTtsLanguage();
|
||||||
|
} else if (StringUtils.isNotBlank(timbre.getLanguages())) {
|
||||||
|
language = timbre.getLanguages().split("、")[0].trim();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
VoiceCloneEntity voice_print = cloneVoiceService.selectById(agent.getTtsVoiceId());
|
VoiceCloneEntity voice_print = cloneVoiceService.selectById(agent.getTtsVoiceId());
|
||||||
if (voice_print != null) {
|
if (voice_print != null) {
|
||||||
voice = voice_print.getVoiceId();
|
voice = voice_print.getVoiceId();
|
||||||
|
// 优先使用用户选择的语言,如果没有则使用默认值
|
||||||
|
language = StringUtils.isNotBlank(agent.getTtsLanguage()) ? agent.getTtsLanguage() : "普通话";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 构建返回数据
|
// 构建返回数据
|
||||||
@@ -151,11 +177,11 @@ public class ConfigServiceImpl implements ConfigService {
|
|||||||
}
|
}
|
||||||
result.put("chat_history_conf", chatHistoryConf);
|
result.put("chat_history_conf", chatHistoryConf);
|
||||||
// 如果客户端已实例化模型,则不返回
|
// 如果客户端已实例化模型,则不返回
|
||||||
String alreadySelectedVadModelId = (String) selectedModule.get("VAD");
|
String alreadySelectedVadModelId = selectedModule.get("VAD");
|
||||||
if (alreadySelectedVadModelId != null && alreadySelectedVadModelId.equals(agent.getVadModelId())) {
|
if (alreadySelectedVadModelId != null && alreadySelectedVadModelId.equals(agent.getVadModelId())) {
|
||||||
agent.setVadModelId(null);
|
agent.setVadModelId(null);
|
||||||
}
|
}
|
||||||
String alreadySelectedAsrModelId = (String) selectedModule.get("ASR");
|
String alreadySelectedAsrModelId = selectedModule.get("ASR");
|
||||||
if (alreadySelectedAsrModelId != null && alreadySelectedAsrModelId.equals(agent.getAsrModelId())) {
|
if (alreadySelectedAsrModelId != null && alreadySelectedAsrModelId.equals(agent.getAsrModelId())) {
|
||||||
agent.setAsrModelId(null);
|
agent.setAsrModelId(null);
|
||||||
}
|
}
|
||||||
@@ -178,6 +204,14 @@ public class ConfigServiceImpl implements ConfigService {
|
|||||||
mcpEndpoint = mcpEndpoint.replace("/mcp/", "/call/");
|
mcpEndpoint = mcpEndpoint.replace("/mcp/", "/call/");
|
||||||
result.put("mcp_endpoint", mcpEndpoint);
|
result.put("mcp_endpoint", mcpEndpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取上下文源配置
|
||||||
|
AgentContextProviderEntity contextProviderEntity = agentContextProviderService.getByAgentId(agent.getId());
|
||||||
|
if (contextProviderEntity != null && contextProviderEntity.getContextProviders() != null
|
||||||
|
&& !contextProviderEntity.getContextProviders().isEmpty()) {
|
||||||
|
result.put("context_providers", contextProviderEntity.getContextProviders());
|
||||||
|
}
|
||||||
|
|
||||||
// 获取声纹信息
|
// 获取声纹信息
|
||||||
buildVoiceprintConfig(agent.getId(), result);
|
buildVoiceprintConfig(agent.getId(), result);
|
||||||
|
|
||||||
@@ -189,10 +223,15 @@ public class ConfigServiceImpl implements ConfigService {
|
|||||||
voice,
|
voice,
|
||||||
referenceAudio,
|
referenceAudio,
|
||||||
referenceText,
|
referenceText,
|
||||||
|
language,
|
||||||
|
agent.getTtsVolume(),
|
||||||
|
agent.getTtsRate(),
|
||||||
|
agent.getTtsPitch(),
|
||||||
agent.getVadModelId(),
|
agent.getVadModelId(),
|
||||||
agent.getAsrModelId(),
|
agent.getAsrModelId(),
|
||||||
agent.getLlmModelId(),
|
agent.getLlmModelId(),
|
||||||
agent.getVllmModelId(),
|
agent.getVllmModelId(),
|
||||||
|
agent.getSlmModelId(),
|
||||||
agent.getTtsModelId(),
|
agent.getTtsModelId(),
|
||||||
agent.getMemModelId(),
|
agent.getMemModelId(),
|
||||||
agent.getIntentModelId(),
|
agent.getIntentModelId(),
|
||||||
@@ -283,7 +322,7 @@ public class ConfigServiceImpl implements ConfigService {
|
|||||||
private void buildVoiceprintConfig(String agentId, Map<String, Object> result) {
|
private void buildVoiceprintConfig(String agentId, Map<String, Object> result) {
|
||||||
try {
|
try {
|
||||||
// 获取声纹接口地址
|
// 获取声纹接口地址
|
||||||
String voiceprintUrl = sysParamsService.getValue("server.voice_print", true);
|
String voiceprintUrl = sysParamsService.getValue(Constant.SERVER_VOICE_PRINT, true);
|
||||||
if (StringUtils.isBlank(voiceprintUrl) || "null".equals(voiceprintUrl)) {
|
if (StringUtils.isBlank(voiceprintUrl) || "null".equals(voiceprintUrl)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -366,10 +405,15 @@ public class ConfigServiceImpl implements ConfigService {
|
|||||||
String voice,
|
String voice,
|
||||||
String referenceAudio,
|
String referenceAudio,
|
||||||
String referenceText,
|
String referenceText,
|
||||||
|
String language,
|
||||||
|
Integer ttsVolume,
|
||||||
|
Integer ttsRate,
|
||||||
|
Integer ttsPitch,
|
||||||
String vadModelId,
|
String vadModelId,
|
||||||
String asrModelId,
|
String asrModelId,
|
||||||
String llmModelId,
|
String llmModelId,
|
||||||
String vllmModelId,
|
String vllmModelId,
|
||||||
|
String slmModelId,
|
||||||
String ttsModelId,
|
String ttsModelId,
|
||||||
String memModelId,
|
String memModelId,
|
||||||
String intentModelId,
|
String intentModelId,
|
||||||
@@ -378,9 +422,9 @@ public class ConfigServiceImpl implements ConfigService {
|
|||||||
boolean isCache) {
|
boolean isCache) {
|
||||||
Map<String, String> selectedModule = new HashMap<>();
|
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,
|
String[] modelIds = { vadModelId, asrModelId, ttsModelId, memModelId, intentModelId, llmModelId, vllmModelId,
|
||||||
ragModelId };
|
slmModelId, ragModelId };
|
||||||
String intentLLMModelId = null;
|
String intentLLMModelId = null;
|
||||||
String memLocalShortLLMModelId = null;
|
String memLocalShortLLMModelId = null;
|
||||||
|
|
||||||
@@ -404,11 +448,19 @@ public class ConfigServiceImpl implements ConfigService {
|
|||||||
((Map<String, Object>) model.getConfigJson()).put("ref_audio", referenceAudio);
|
((Map<String, Object>) model.getConfigJson()).put("ref_audio", referenceAudio);
|
||||||
if (referenceText != null)
|
if (referenceText != null)
|
||||||
((Map<String, Object>) model.getConfigJson()).put("ref_text", referenceText);
|
((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
|
// 火山引擎声音克隆需要替换resource_id
|
||||||
Map<String, Object> map = (Map<String, Object>) model.getConfigJson();
|
Map<String, Object> map = (Map<String, Object>) model.getConfigJson();
|
||||||
if (Constant.VOICE_CLONE_HUOSHAN_DOUBLE_STREAM.equals(map.get("type"))) {
|
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_")) {
|
if (voice != null && voice.startsWith("S_")) {
|
||||||
map.put("resource_id", "seed-icl-1.0");
|
map.put("resource_id", "seed-icl-1.0");
|
||||||
}
|
}
|
||||||
@@ -426,7 +478,7 @@ public class ConfigServiceImpl implements ConfigService {
|
|||||||
if (map.get("functions") != null) {
|
if (map.get("functions") != null) {
|
||||||
String functionStr = (String) map.get("functions");
|
String functionStr = (String) map.get("functions");
|
||||||
if (StringUtils.isNotBlank(functionStr)) {
|
if (StringUtils.isNotBlank(functionStr)) {
|
||||||
String[] functions = functionStr.split("\\;");
|
String[] functions = functionStr.split(";");
|
||||||
map.put("functions", functions);
|
map.put("functions", functions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -459,6 +511,15 @@ public class ConfigServiceImpl implements ConfigService {
|
|||||||
typeConfig.put(memLocalShortLLM.getId(), memLocalShortLLM.getConfigJson());
|
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);
|
result.put(modelTypes[i], typeConfig);
|
||||||
|
|||||||
+38
-87
@@ -1,14 +1,11 @@
|
|||||||
package xiaozhi.modules.device.controller;
|
package xiaozhi.modules.device.controller;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||||
import org.springframework.beans.BeanUtils;
|
import org.springframework.beans.BeanUtils;
|
||||||
import org.springframework.http.HttpEntity;
|
|
||||||
import org.springframework.http.HttpHeaders;
|
|
||||||
import org.springframework.http.HttpMethod;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
@@ -16,9 +13,6 @@ import org.springframework.web.bind.annotation.PutMapping;
|
|||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.client.RestTemplate;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
@@ -30,6 +24,7 @@ import xiaozhi.common.user.UserDetail;
|
|||||||
import xiaozhi.common.utils.Result;
|
import xiaozhi.common.utils.Result;
|
||||||
import xiaozhi.modules.device.dto.DeviceManualAddDTO;
|
import xiaozhi.modules.device.dto.DeviceManualAddDTO;
|
||||||
import xiaozhi.modules.device.dto.DeviceRegisterDTO;
|
import xiaozhi.modules.device.dto.DeviceRegisterDTO;
|
||||||
|
import xiaozhi.modules.device.dto.DeviceToolsCallReqDTO;
|
||||||
import xiaozhi.modules.device.dto.DeviceUnBindDTO;
|
import xiaozhi.modules.device.dto.DeviceUnBindDTO;
|
||||||
import xiaozhi.modules.device.dto.DeviceUpdateDTO;
|
import xiaozhi.modules.device.dto.DeviceUpdateDTO;
|
||||||
import xiaozhi.modules.device.entity.DeviceEntity;
|
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||||
@@ -44,16 +39,11 @@ public class DeviceController {
|
|||||||
private final DeviceService deviceService;
|
private final DeviceService deviceService;
|
||||||
private final RedisUtils redisUtils;
|
private final RedisUtils redisUtils;
|
||||||
private final SysParamsService sysParamsService;
|
private final SysParamsService sysParamsService;
|
||||||
private final RestTemplate restTemplate;
|
|
||||||
private final ObjectMapper objectMapper;
|
|
||||||
|
|
||||||
public DeviceController(DeviceService deviceService, RedisUtils redisUtils, SysParamsService sysParamsService,
|
public DeviceController(DeviceService deviceService, RedisUtils redisUtils, SysParamsService sysParamsService) {
|
||||||
RestTemplate restTemplate, ObjectMapper objectMapper) {
|
|
||||||
this.deviceService = deviceService;
|
this.deviceService = deviceService;
|
||||||
this.redisUtils = redisUtils;
|
this.redisUtils = redisUtils;
|
||||||
this.sysParamsService = sysParamsService;
|
this.sysParamsService = sysParamsService;
|
||||||
this.restTemplate = restTemplate;
|
|
||||||
this.objectMapper = objectMapper;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/bind/{agentId}/{deviceCode}")
|
@PostMapping("/bind/{agentId}/{deviceCode}")
|
||||||
@@ -72,10 +62,12 @@ public class DeviceController {
|
|||||||
return new Result<String>().error(ErrorCode.MCA_NOT_NULL);
|
return new Result<String>().error(ErrorCode.MCA_NOT_NULL);
|
||||||
}
|
}
|
||||||
// 生成六位验证码
|
// 生成六位验证码
|
||||||
String code = String.valueOf(Math.random()).substring(2, 8);
|
String code;
|
||||||
String key = RedisKeys.getDeviceCaptchaKey(code);
|
String key;
|
||||||
String existsMac = null;
|
String existsMac = null;
|
||||||
do {
|
do {
|
||||||
|
code = String.valueOf(Math.random()).substring(2, 8);
|
||||||
|
key = RedisKeys.getDeviceCaptchaKey(code);
|
||||||
existsMac = (String) redisUtils.get(key);
|
existsMac = (String) redisUtils.get(key);
|
||||||
} while (StringUtils.isNotBlank(existsMac));
|
} while (StringUtils.isNotBlank(existsMac));
|
||||||
|
|
||||||
@@ -97,83 +89,12 @@ public class DeviceController {
|
|||||||
@RequiresPermissions("sys:role:normal")
|
@RequiresPermissions("sys:role:normal")
|
||||||
public Result<String> forwardToMqttGateway(@PathVariable String agentId, @RequestBody String requestBody) {
|
public Result<String> forwardToMqttGateway(@PathVariable String agentId, @RequestBody String requestBody) {
|
||||||
try {
|
try {
|
||||||
// 从系统参数中获取MQTT网关地址
|
return new Result<String>().ok(deviceService.getDeviceOnlineData(agentId));
|
||||||
String mqttGatewayUrl = sysParamsService.getValue("server.mqtt_manager_api", true);
|
|
||||||
if (StringUtils.isBlank(mqttGatewayUrl) || "null".equals(mqttGatewayUrl)) {
|
|
||||||
return new Result<>();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取当前用户的设备列表
|
|
||||||
UserDetail user = SecurityUser.getUser();
|
|
||||||
List<DeviceEntity> devices = deviceService.getUserDevices(user.getId(), agentId);
|
|
||||||
|
|
||||||
// 构建deviceIds数组
|
|
||||||
java.util.List<String> deviceIds = new java.util.ArrayList<>();
|
|
||||||
for (DeviceEntity device : devices) {
|
|
||||||
String macAddress = device.getMacAddress() != null ? device.getMacAddress() : "unknown";
|
|
||||||
String groupId = device.getBoard() != null ? device.getBoard() : "GID_default";
|
|
||||||
|
|
||||||
// 替换冒号为下划线
|
|
||||||
groupId = groupId.replace(":", "_");
|
|
||||||
macAddress = macAddress.replace(":", "_");
|
|
||||||
|
|
||||||
// 构建mqtt客户端ID格式:groupId@@@macAddress@@@macAddress
|
|
||||||
String mqttClientId = groupId + "@@@" + macAddress + "@@@" + macAddress;
|
|
||||||
deviceIds.add(mqttClientId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 构建完整的URL
|
|
||||||
String url = "http://" + mqttGatewayUrl + "/api/devices/status";
|
|
||||||
|
|
||||||
// 设置请求头
|
|
||||||
HttpHeaders headers = new HttpHeaders();
|
|
||||||
headers.set("Content-Type", "application/json");
|
|
||||||
|
|
||||||
// 生成Bearer令牌
|
|
||||||
String token = generateBearerToken();
|
|
||||||
if (token == null) {
|
|
||||||
return new Result<String>().error("令牌生成失败");
|
|
||||||
}
|
|
||||||
headers.set("Authorization", "Bearer " + token);
|
|
||||||
|
|
||||||
// 构建请求体JSON
|
|
||||||
String jsonBody = "{\"clientIds\":" + objectMapper.writeValueAsString(deviceIds) + "}";
|
|
||||||
HttpEntity<String> requestEntity = new HttpEntity<>(jsonBody, headers);
|
|
||||||
|
|
||||||
// 发送POST请求
|
|
||||||
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class);
|
|
||||||
|
|
||||||
// 返回响应
|
|
||||||
return new Result<String>().ok(response.getBody());
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return new Result<String>().error("转发请求失败: " + e.getMessage());
|
return new Result<String>().error("转发请求失败: " + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private String generateBearerToken() {
|
|
||||||
try {
|
|
||||||
// 获取当前日期,格式为yyyy-MM-dd
|
|
||||||
String dateStr = java.time.LocalDate.now()
|
|
||||||
.format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd"));
|
|
||||||
|
|
||||||
// 获取MQTT签名密钥
|
|
||||||
String signatureKey = sysParamsService.getValue("server.mqtt_signature_key", false);
|
|
||||||
if (StringUtils.isBlank(signatureKey)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 将日期字符串与MQTT_SIGNATURE_KEY连接
|
|
||||||
String tokenContent = dateStr + signatureKey;
|
|
||||||
|
|
||||||
// 对连接后的字符串进行SHA256哈希计算
|
|
||||||
String token = org.apache.commons.codec.digest.DigestUtils.sha256Hex(tokenContent);
|
|
||||||
|
|
||||||
return token;
|
|
||||||
} catch (Exception e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/unbind")
|
@PostMapping("/unbind")
|
||||||
@Operation(summary = "解绑设备")
|
@Operation(summary = "解绑设备")
|
||||||
@RequiresPermissions("sys:role:normal")
|
@RequiresPermissions("sys:role:normal")
|
||||||
@@ -208,4 +129,34 @@ public class DeviceController {
|
|||||||
deviceService.manualAddDevice(user.getId(), dto);
|
deviceService.manualAddDevice(user.getId(), dto);
|
||||||
return new Result<>();
|
return new Result<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/tools/list/{deviceId}")
|
||||||
|
@Operation(summary = "获取设备工具列表")
|
||||||
|
@RequiresPermissions("sys:role:normal")
|
||||||
|
public Result<Object> getDeviceTools(@PathVariable String deviceId) {
|
||||||
|
Object toolsData = deviceService.getDeviceTools(deviceId);
|
||||||
|
if (toolsData == null) {
|
||||||
|
return new Result<Object>().error(ErrorCode.DEVICE_NOT_EXIST);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Result<Object>().ok(toolsData);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/tools/call/{deviceId}")
|
||||||
|
@Operation(summary = "调用设备工具")
|
||||||
|
@RequiresPermissions("sys:role:normal")
|
||||||
|
public Result<Object> callDeviceTool(@PathVariable String deviceId,
|
||||||
|
@Valid @RequestBody DeviceToolsCallReqDTO request) {
|
||||||
|
String toolName = request.getName();
|
||||||
|
Map<String, Object> arguments = request.getArguments();
|
||||||
|
|
||||||
|
Object result = deviceService.callDeviceTool(deviceId, toolName, arguments);
|
||||||
|
if (result == null) {
|
||||||
|
return new Result<Object>().error(ErrorCode.DEVICE_NOT_EXIST);
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<Object> response = new Result<Object>();
|
||||||
|
response.setMsg("Tools called successfully");
|
||||||
|
return response.ok(result);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+59
-9
@@ -7,7 +7,9 @@ import java.nio.file.Path;
|
|||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
@@ -36,6 +38,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
|||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import xiaozhi.common.constant.Constant;
|
import xiaozhi.common.constant.Constant;
|
||||||
|
import xiaozhi.common.exception.ErrorCode;
|
||||||
import xiaozhi.common.page.PageData;
|
import xiaozhi.common.page.PageData;
|
||||||
import xiaozhi.common.redis.RedisKeys;
|
import xiaozhi.common.redis.RedisKeys;
|
||||||
import xiaozhi.common.redis.RedisUtils;
|
import xiaozhi.common.redis.RedisUtils;
|
||||||
@@ -43,8 +46,11 @@ import xiaozhi.common.utils.Result;
|
|||||||
import xiaozhi.common.validator.ValidatorUtils;
|
import xiaozhi.common.validator.ValidatorUtils;
|
||||||
import xiaozhi.modules.device.entity.OtaEntity;
|
import xiaozhi.modules.device.entity.OtaEntity;
|
||||||
import xiaozhi.modules.device.service.OtaService;
|
import xiaozhi.modules.device.service.OtaService;
|
||||||
|
import xiaozhi.modules.security.user.SecurityUser;
|
||||||
|
import xiaozhi.modules.sys.enums.SuperAdminEnum;
|
||||||
|
import xiaozhi.modules.sys.service.SysParamsService;
|
||||||
|
|
||||||
@Tag(name = "设备管理", description = "OTA 相关接口")
|
@Tag(name = "固件升级管理", description = "OTA 相关接口")
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@RestController
|
@RestController
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@@ -53,6 +59,7 @@ public class OTAMagController {
|
|||||||
private static final Logger logger = LoggerFactory.getLogger(OTAController.class);
|
private static final Logger logger = LoggerFactory.getLogger(OTAController.class);
|
||||||
private final OtaService otaService;
|
private final OtaService otaService;
|
||||||
private final RedisUtils redisUtils;
|
private final RedisUtils redisUtils;
|
||||||
|
private final SysParamsService sysParamsService;
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
@Operation(summary = "分页查询 OTA 固件信息")
|
@Operation(summary = "分页查询 OTA 固件信息")
|
||||||
@@ -145,15 +152,11 @@ public class OTAMagController {
|
|||||||
|
|
||||||
// 检查下载次数
|
// 检查下载次数
|
||||||
String downloadCountKey = RedisKeys.getOtaDownloadCountKey(uuid);
|
String downloadCountKey = RedisKeys.getOtaDownloadCountKey(uuid);
|
||||||
Integer downloadCount = (Integer) redisUtils.get(downloadCountKey);
|
Integer downloadCount = (Integer) Optional.ofNullable(redisUtils.get(downloadCountKey)).orElse(0);
|
||||||
if (downloadCount == null) {
|
|
||||||
downloadCount = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果下载次数超过3次,返回404
|
// 如果下载次数超过3次,返回404
|
||||||
if (downloadCount >= 3) {
|
if (downloadCount >= 3) {
|
||||||
redisUtils.delete(downloadCountKey);
|
redisUtils.delete(List.of(downloadCountKey, RedisKeys.getOtaIdKey(uuid)));
|
||||||
redisUtils.delete(RedisKeys.getOtaIdKey(uuid));
|
|
||||||
logger.warn("Download limit exceeded for UUID: {}", uuid);
|
logger.warn("Download limit exceeded for UUID: {}", uuid);
|
||||||
return ResponseEntity.notFound().build();
|
return ResponseEntity.notFound().build();
|
||||||
}
|
}
|
||||||
@@ -162,7 +165,17 @@ public class OTAMagController {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// 获取固件信息
|
// 获取固件信息
|
||||||
OtaEntity otaEntity = otaService.selectById(id);
|
OtaEntity otaEntity = null;
|
||||||
|
if (id.indexOf("file:") == 0) {
|
||||||
|
id = id.substring(5);
|
||||||
|
otaEntity = new OtaEntity();
|
||||||
|
otaEntity.setFirmwarePath(id);
|
||||||
|
otaEntity.setType("assets");
|
||||||
|
otaEntity.setVersion("1.0.0");
|
||||||
|
} else {
|
||||||
|
otaEntity = otaService.selectById(id);
|
||||||
|
}
|
||||||
|
|
||||||
if (otaEntity == null || StringUtils.isBlank(otaEntity.getFirmwarePath())) {
|
if (otaEntity == null || StringUtils.isBlank(otaEntity.getFirmwarePath())) {
|
||||||
logger.warn("Firmware not found or path is empty for ID: {}", id);
|
logger.warn("Firmware not found or path is empty for ID: {}", id);
|
||||||
return ResponseEntity.notFound().build();
|
return ResponseEntity.notFound().build();
|
||||||
@@ -170,6 +183,7 @@ public class OTAMagController {
|
|||||||
|
|
||||||
// 获取文件路径 - 确保路径是绝对路径或正确的相对路径
|
// 获取文件路径 - 确保路径是绝对路径或正确的相对路径
|
||||||
String firmwarePath = otaEntity.getFirmwarePath();
|
String firmwarePath = otaEntity.getFirmwarePath();
|
||||||
|
String originalFilename = otaEntity.getType() + "_" + otaEntity.getVersion();
|
||||||
Path path;
|
Path path;
|
||||||
|
|
||||||
// 检查是否是绝对路径
|
// 检查是否是绝对路径
|
||||||
@@ -203,7 +217,7 @@ public class OTAMagController {
|
|||||||
byte[] fileContent = Files.readAllBytes(path);
|
byte[] fileContent = Files.readAllBytes(path);
|
||||||
|
|
||||||
// 设置响应头
|
// 设置响应头
|
||||||
String originalFilename = otaEntity.getType() + "_" + otaEntity.getVersion();
|
|
||||||
if (firmwarePath.contains(".")) {
|
if (firmwarePath.contains(".")) {
|
||||||
String extension = firmwarePath.substring(firmwarePath.lastIndexOf("."));
|
String extension = firmwarePath.substring(firmwarePath.lastIndexOf("."));
|
||||||
originalFilename += extension;
|
originalFilename += extension;
|
||||||
@@ -279,6 +293,42 @@ public class OTAMagController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/uploadAssetsBin")
|
||||||
|
@Operation(summary = "上传资源固件文件")
|
||||||
|
@RequiresPermissions("sys:role:normal")
|
||||||
|
public Result<String> uploadAssetsBin(@RequestParam("file") MultipartFile file) {
|
||||||
|
String otaUrl = sysParamsService.getValue(Constant.SERVER_OTA, true);
|
||||||
|
if (StringUtils.isBlank(otaUrl) || otaUrl.equals("null")) {
|
||||||
|
return new Result<String>().error(ErrorCode.OTA_URL_EMPTY);
|
||||||
|
}
|
||||||
|
logger.info("username:{},uploadAssetsBin size: {}", SecurityUser.getUser().getUsername(), file.getSize());
|
||||||
|
// 验证文件大小 (资源固件最大20MB)
|
||||||
|
if (file.getSize() > 20 * 1024 * 1024) {
|
||||||
|
return new Result<String>().error(ErrorCode.VOICE_CLONE_AUDIO_TOO_LARGE);
|
||||||
|
}
|
||||||
|
// 普通用户只能每天上传50次
|
||||||
|
if (SecurityUser.getUser().getSuperAdmin() == SuperAdminEnum.NO.value()) {
|
||||||
|
String uploadCountKey = RedisKeys.getOtaUploadCountKey(SecurityUser.getUser().getId());
|
||||||
|
Integer uploadCount = (Integer) Optional.ofNullable(redisUtils.get(uploadCountKey)).orElse(0);
|
||||||
|
if (uploadCount >= 50) {
|
||||||
|
return new Result<String>().error(ErrorCode.OTA_UPLOAD_COUNT_EXCEED);
|
||||||
|
}
|
||||||
|
// 增加上传次数
|
||||||
|
redisUtils.increment(RedisKeys.getOtaUploadCountKey(SecurityUser.getUser().getId()),
|
||||||
|
RedisUtils.DEFAULT_EXPIRE);
|
||||||
|
}
|
||||||
|
Result<String> result = uploadFirmware(file);
|
||||||
|
|
||||||
|
// 生成资源文件路径
|
||||||
|
if (StringUtils.isNotBlank(result.getData())) {
|
||||||
|
String uuid = UUID.randomUUID().toString();
|
||||||
|
redisUtils.set(RedisKeys.getOtaIdKey(uuid), "file:" + result.getData());
|
||||||
|
String downloadUrl = otaUrl.replace("/ota/", "/otaMag/download/") + uuid;
|
||||||
|
result.setData(downloadUrl);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
private String calculateMD5(MultipartFile file) throws IOException, NoSuchAlgorithmException {
|
private String calculateMD5(MultipartFile file) throws IOException, NoSuchAlgorithmException {
|
||||||
MessageDigest md = MessageDigest.getInstance("MD5");
|
MessageDigest md = MessageDigest.getInstance("MD5");
|
||||||
byte[] digest = md.digest(file.getBytes());
|
byte[] digest = md.digest(file.getBytes());
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package xiaozhi.modules.device.dto;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class DeviceToolsCallReqDTO {
|
||||||
|
|
||||||
|
@NotBlank(message = "工具名称不能为空")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
private Map<String, Object> arguments;
|
||||||
|
}
|
||||||
@@ -2,17 +2,22 @@ package xiaozhi.modules.device.service;
|
|||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import xiaozhi.common.page.PageData;
|
import xiaozhi.common.page.PageData;
|
||||||
import xiaozhi.common.service.BaseService;
|
import xiaozhi.common.service.BaseService;
|
||||||
|
import xiaozhi.modules.device.dto.DeviceManualAddDTO;
|
||||||
import xiaozhi.modules.device.dto.DevicePageUserDTO;
|
import xiaozhi.modules.device.dto.DevicePageUserDTO;
|
||||||
import xiaozhi.modules.device.dto.DeviceReportReqDTO;
|
import xiaozhi.modules.device.dto.DeviceReportReqDTO;
|
||||||
import xiaozhi.modules.device.dto.DeviceReportRespDTO;
|
import xiaozhi.modules.device.dto.DeviceReportRespDTO;
|
||||||
import xiaozhi.modules.device.dto.DeviceManualAddDTO;
|
|
||||||
import xiaozhi.modules.device.entity.DeviceEntity;
|
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||||
import xiaozhi.modules.device.vo.UserShowDeviceListVO;
|
import xiaozhi.modules.device.vo.UserShowDeviceListVO;
|
||||||
|
|
||||||
public interface DeviceService extends BaseService<DeviceEntity> {
|
public interface DeviceService extends BaseService<DeviceEntity> {
|
||||||
|
/**
|
||||||
|
* 获取设备在线数据
|
||||||
|
*/
|
||||||
|
String getDeviceOnlineData(String agentId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查设备是否激活
|
* 检查设备是否激活
|
||||||
@@ -83,6 +88,7 @@ public interface DeviceService extends BaseService<DeviceEntity> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取这个智能体设备理的最近的最后连接时间
|
* 获取这个智能体设备理的最近的最后连接时间
|
||||||
|
*
|
||||||
* @param agentId 智能体id
|
* @param agentId 智能体id
|
||||||
* @return 返回设备最近的最后连接时间
|
* @return 返回设备最近的最后连接时间
|
||||||
*/
|
*/
|
||||||
@@ -98,4 +104,33 @@ public interface DeviceService extends BaseService<DeviceEntity> {
|
|||||||
*/
|
*/
|
||||||
void updateDeviceConnectionInfo(String agentId, String deviceId, String appVersion);
|
void updateDeviceConnectionInfo(String agentId, String deviceId, String appVersion);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成WebSocket认证token
|
||||||
|
*
|
||||||
|
* @param clientId 客户端ID
|
||||||
|
* @param username 用户名(通常为deviceId)
|
||||||
|
* @return 认证token字符串
|
||||||
|
* @throws Exception 生成token时的异常
|
||||||
|
*/
|
||||||
|
String generateWebSocketToken(String clientId, String username) throws Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据MAC地址搜索设备
|
||||||
|
*
|
||||||
|
* @param macAddress MAC地址关键词
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @return 设备列表
|
||||||
|
*/
|
||||||
|
List<DeviceEntity> searchDevicesByMacAddress(String macAddress, Long userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取设备工具列表
|
||||||
|
*/
|
||||||
|
Object getDeviceTools(String deviceId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调用设备工具
|
||||||
|
*/
|
||||||
|
Object callDeviceTool(String deviceId, String toolName, Map<String, Object> arguments);
|
||||||
|
|
||||||
}
|
}
|
||||||
+344
-19
@@ -1,14 +1,20 @@
|
|||||||
package xiaozhi.modules.device.service.impl;
|
package xiaozhi.modules.device.service.impl;
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.TimeZone;
|
import java.util.TimeZone;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import javax.crypto.Mac;
|
import javax.crypto.Mac;
|
||||||
import javax.crypto.spec.SecretKeySpec;
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
@@ -24,7 +30,18 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
|||||||
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
|
|
||||||
|
import cn.hutool.core.date.DatePattern;
|
||||||
|
import cn.hutool.core.date.DateUtil;
|
||||||
|
import cn.hutool.core.map.MapUtil;
|
||||||
import cn.hutool.core.util.RandomUtil;
|
import cn.hutool.core.util.RandomUtil;
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
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 jakarta.servlet.http.HttpServletRequest;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@@ -38,6 +55,7 @@ import xiaozhi.common.service.impl.BaseServiceImpl;
|
|||||||
import xiaozhi.common.user.UserDetail;
|
import xiaozhi.common.user.UserDetail;
|
||||||
import xiaozhi.common.utils.ConvertUtils;
|
import xiaozhi.common.utils.ConvertUtils;
|
||||||
import xiaozhi.common.utils.DateUtils;
|
import xiaozhi.common.utils.DateUtils;
|
||||||
|
import xiaozhi.common.utils.ToolUtil;
|
||||||
import xiaozhi.modules.device.dao.DeviceDao;
|
import xiaozhi.modules.device.dao.DeviceDao;
|
||||||
import xiaozhi.modules.device.dto.DeviceManualAddDTO;
|
import xiaozhi.modules.device.dto.DeviceManualAddDTO;
|
||||||
import xiaozhi.modules.device.dto.DevicePageUserDTO;
|
import xiaozhi.modules.device.dto.DevicePageUserDTO;
|
||||||
@@ -86,16 +104,16 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
if (StringUtils.isBlank(activationCode)) {
|
if (StringUtils.isBlank(activationCode)) {
|
||||||
throw new RenException(ErrorCode.ACTIVATION_CODE_EMPTY);
|
throw new RenException(ErrorCode.ACTIVATION_CODE_EMPTY);
|
||||||
}
|
}
|
||||||
String deviceKey = "ota:activation:code:" + activationCode;
|
String deviceKey = RedisKeys.getOtaActivationCode(activationCode);
|
||||||
Object cacheDeviceId = redisUtils.get(deviceKey);
|
Object cacheDeviceId = redisUtils.get(deviceKey);
|
||||||
if (cacheDeviceId == null) {
|
if (ToolUtil.isEmpty(cacheDeviceId)) {
|
||||||
throw new RenException(ErrorCode.ACTIVATION_CODE_ERROR);
|
throw new RenException(ErrorCode.ACTIVATION_CODE_ERROR);
|
||||||
}
|
}
|
||||||
String deviceId = (String) cacheDeviceId;
|
String deviceId = (String) cacheDeviceId;
|
||||||
String safeDeviceId = deviceId.replace(":", "_").toLowerCase();
|
String safeDeviceId = deviceId.replace(":", "_").toLowerCase();
|
||||||
String cacheDeviceKey = String.format("ota:activation:data:%s", safeDeviceId);
|
String cacheDeviceKey = RedisKeys.getOtaDeviceActivationInfo(safeDeviceId);
|
||||||
Map<String, Object> cacheMap = (Map<String, Object>) redisUtils.get(cacheDeviceKey);
|
Map<String, Object> cacheMap = (Map<String, Object>) redisUtils.get(cacheDeviceKey);
|
||||||
if (cacheMap == null) {
|
if (ToolUtil.isEmpty(cacheMap)) {
|
||||||
throw new RenException(ErrorCode.ACTIVATION_CODE_ERROR);
|
throw new RenException(ErrorCode.ACTIVATION_CODE_ERROR);
|
||||||
}
|
}
|
||||||
String cachedCode = (String) cacheMap.get("activation_code");
|
String cachedCode = (String) cacheMap.get("activation_code");
|
||||||
@@ -131,19 +149,56 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
deviceEntity.setLastConnectedAt(currentTime);
|
deviceEntity.setLastConnectedAt(currentTime);
|
||||||
deviceDao.insert(deviceEntity);
|
deviceDao.insert(deviceEntity);
|
||||||
|
|
||||||
// 清理redis缓存
|
// 清理redis缓存、清除智能体设备数量缓存
|
||||||
redisUtils.delete(cacheDeviceKey);
|
redisUtils.delete(List.of(cacheDeviceKey, deviceKey, RedisKeys.getAgentDeviceCountById(agentId)));
|
||||||
redisUtils.delete(deviceKey);
|
|
||||||
|
|
||||||
// 添加:清除智能体设备数量缓存
|
|
||||||
redisUtils.delete(RedisKeys.getAgentDeviceCountById(agentId));
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取设备在线数据
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public DeviceReportRespDTO checkDeviceActive(String macAddress, String clientId,
|
public String getDeviceOnlineData(String agentId) {
|
||||||
DeviceReportReqDTO deviceReport) {
|
// 从系统参数中获取MQTT网关地址
|
||||||
|
String mqttGatewayUrl = sysParamsService.getValue("server.mqtt_manager_api", true);
|
||||||
|
if (StringUtils.isBlank(mqttGatewayUrl) || "null".equals(mqttGatewayUrl)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
// 构建完整的URL
|
||||||
|
String url = StrUtil.format("http://{}/api/devices/status", mqttGatewayUrl);
|
||||||
|
|
||||||
|
// 获取当前用户的设备列表
|
||||||
|
UserDetail user = SecurityUser.getUser();
|
||||||
|
List<DeviceEntity> devices = getUserDevices(user.getId(), agentId);
|
||||||
|
|
||||||
|
// 构建deviceIds数组
|
||||||
|
Set<String> deviceIds = devices.stream().map(o -> {
|
||||||
|
String macAddress = Optional.ofNullable(o.getMacAddress()).orElse("unknown").replace(":", "_");
|
||||||
|
String groupId = Optional.ofNullable(o.getBoard()).orElse("GID_default").replace(":", "_");
|
||||||
|
return StrUtil.format("{}@@@{}@@@{}", groupId, macAddress, macAddress);
|
||||||
|
}).collect(Collectors.toSet());
|
||||||
|
|
||||||
|
// 构建请求入参
|
||||||
|
Map<String, Set<String>> params = MapUtil
|
||||||
|
.builder(new HashMap<String, Set<String>>())
|
||||||
|
.put("clientIds", deviceIds).build();
|
||||||
|
|
||||||
|
if (ToolUtil.isNotEmpty(deviceIds)) {
|
||||||
|
// 发送请求
|
||||||
|
String resultMessage = HttpRequest.post(url)
|
||||||
|
.header(Header.CONTENT_TYPE, ContentType.JSON.getValue())
|
||||||
|
.header(Header.AUTHORIZATION, "Bearer " + generateBearerToken())
|
||||||
|
.body(JSONUtil.toJsonStr(params))
|
||||||
|
.timeout(10000) // 超时,毫秒
|
||||||
|
.execute().body();
|
||||||
|
return resultMessage;
|
||||||
|
}
|
||||||
|
// 返回响应
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DeviceReportRespDTO checkDeviceActive(String macAddress, String clientId, DeviceReportReqDTO deviceReport) {
|
||||||
DeviceReportRespDTO response = new DeviceReportRespDTO();
|
DeviceReportRespDTO response = new DeviceReportRespDTO();
|
||||||
response.setServer_time(buildServerTime());
|
response.setServer_time(buildServerTime());
|
||||||
|
|
||||||
@@ -169,7 +224,22 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
DeviceReportRespDTO.Websocket websocket = new DeviceReportRespDTO.Websocket();
|
DeviceReportRespDTO.Websocket websocket = new DeviceReportRespDTO.Websocket();
|
||||||
// 从系统参数获取WebSocket URL,如果未配置则使用默认值
|
// 从系统参数获取WebSocket URL,如果未配置则使用默认值
|
||||||
String wsUrl = sysParamsService.getValue(Constant.SERVER_WEBSOCKET, true);
|
String wsUrl = sysParamsService.getValue(Constant.SERVER_WEBSOCKET, true);
|
||||||
websocket.setToken("");
|
|
||||||
|
// 检查是否启用认证并生成token
|
||||||
|
String authEnabled = sysParamsService.getValue(Constant.SERVER_AUTH_ENABLED, true);
|
||||||
|
if ("true".equalsIgnoreCase(authEnabled)) {
|
||||||
|
try {
|
||||||
|
// 生成token
|
||||||
|
String token = generateWebSocketToken(clientId, macAddress);
|
||||||
|
websocket.setToken(token);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("生成WebSocket token失败: {}", e.getMessage());
|
||||||
|
websocket.setToken("");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
websocket.setToken("");
|
||||||
|
}
|
||||||
|
|
||||||
if (StringUtils.isBlank(wsUrl) || wsUrl.equals("null")) {
|
if (StringUtils.isBlank(wsUrl) || wsUrl.equals("null")) {
|
||||||
log.error("WebSocket地址未配置,请登录智控台,在参数管理找到【server.websocket】配置");
|
log.error("WebSocket地址未配置,请登录智控台,在参数管理找到【server.websocket】配置");
|
||||||
wsUrl = "ws://xiaozhi.server.com:8000/xiaozhi/v1/";
|
wsUrl = "ws://xiaozhi.server.com:8000/xiaozhi/v1/";
|
||||||
@@ -189,7 +259,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
|
|
||||||
// 添加MQTT UDP配置
|
// 添加MQTT UDP配置
|
||||||
// 从系统参数获取MQTT Gateway地址,仅在配置有效时使用
|
// 从系统参数获取MQTT Gateway地址,仅在配置有效时使用
|
||||||
String mqttUdpConfig = sysParamsService.getValue(Constant.SERVER_MQTT_GATEWAY, false);
|
String mqttUdpConfig = sysParamsService.getValue(Constant.SERVER_MQTT_GATEWAY, true);
|
||||||
if (mqttUdpConfig != null && !mqttUdpConfig.equals("null") && !mqttUdpConfig.isEmpty()) {
|
if (mqttUdpConfig != null && !mqttUdpConfig.equals("null") && !mqttUdpConfig.isEmpty()) {
|
||||||
try {
|
try {
|
||||||
String groupId = deviceById != null && deviceById.getBoard() != null ? deviceById.getBoard()
|
String groupId = deviceById != null && deviceById.getBoard() != null ? deviceById.getBoard()
|
||||||
@@ -339,8 +409,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
|
|
||||||
private String getDeviceCacheKey(String deviceId) {
|
private String getDeviceCacheKey(String deviceId) {
|
||||||
String safeDeviceId = deviceId.replace(":", "_").toLowerCase();
|
String safeDeviceId = deviceId.replace(":", "_").toLowerCase();
|
||||||
String dataKey = String.format("ota:activation:data:%s", safeDeviceId);
|
return RedisKeys.getOtaDeviceActivationInfo(safeDeviceId);
|
||||||
return dataKey;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public DeviceReportRespDTO.Activation buildActivation(String deviceId, DeviceReportReqDTO deviceReport) {
|
public DeviceReportRespDTO.Activation buildActivation(String deviceId, DeviceReportReqDTO deviceReport) {
|
||||||
@@ -379,7 +448,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
redisUtils.set(dataKey, dataMap);
|
redisUtils.set(dataKey, dataMap);
|
||||||
|
|
||||||
// 写入反查激活码 key
|
// 写入反查激活码 key
|
||||||
String codeKey = "ota:activation:code:" + newCode;
|
String codeKey = RedisKeys.getOtaActivationCode(newCode);
|
||||||
redisUtils.set(codeKey, deviceId);
|
redisUtils.set(codeKey, deviceId);
|
||||||
}
|
}
|
||||||
return code;
|
return code;
|
||||||
@@ -479,6 +548,14 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
redisUtils.delete(RedisKeys.getAgentDeviceCountById(dto.getAgentId()));
|
redisUtils.delete(RedisKeys.getAgentDeviceCountById(dto.getAgentId()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<DeviceEntity> searchDevicesByMacAddress(String macAddress, Long userId) {
|
||||||
|
QueryWrapper<DeviceEntity> wrapper = new QueryWrapper<>();
|
||||||
|
wrapper.like("mac_address", macAddress);
|
||||||
|
wrapper.eq("user_id", userId);
|
||||||
|
return deviceDao.selectList(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成MQTT密码签名
|
* 生成MQTT密码签名
|
||||||
*
|
*
|
||||||
@@ -494,6 +571,40 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
return Base64.getEncoder().encodeToString(signature);
|
return Base64.getEncoder().encodeToString(signature);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成WebSocket认证token 遵循Python端AuthManager的实现逻辑:token = signature.timestamp
|
||||||
|
*
|
||||||
|
* @param clientId 客户端ID
|
||||||
|
* @param username 用户名 (通常为deviceId/macAddress)
|
||||||
|
* @return 认证token字符串
|
||||||
|
*/
|
||||||
|
public String generateWebSocketToken(String clientId, String username)
|
||||||
|
throws NoSuchAlgorithmException, InvalidKeyException {
|
||||||
|
// 从系统参数获取密钥
|
||||||
|
String secretKey = sysParamsService.getValue(Constant.SERVER_SECRET, false);
|
||||||
|
if (StringUtils.isBlank(secretKey)) {
|
||||||
|
throw new IllegalStateException("WebSocket认证密钥未配置(server.secret)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前时间戳(秒)
|
||||||
|
long timestamp = System.currentTimeMillis() / 1000;
|
||||||
|
|
||||||
|
// 构建签名内容: clientId|username|timestamp
|
||||||
|
String content = String.format("%s|%s|%d", clientId, username, timestamp);
|
||||||
|
|
||||||
|
// 生成HMAC-SHA256签名
|
||||||
|
Mac hmac = Mac.getInstance("HmacSHA256");
|
||||||
|
SecretKeySpec keySpec = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
|
||||||
|
hmac.init(keySpec);
|
||||||
|
byte[] signature = hmac.doFinal(content.getBytes(StandardCharsets.UTF_8));
|
||||||
|
|
||||||
|
// Base64 URL-safe编码签名(去除填充符=)
|
||||||
|
String signatureBase64 = Base64.getUrlEncoder().withoutPadding().encodeToString(signature);
|
||||||
|
|
||||||
|
// 返回格式: signature.timestamp
|
||||||
|
return String.format("%s.%d", signatureBase64, timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 构建MQTT配置信息
|
* 构建MQTT配置信息
|
||||||
*
|
*
|
||||||
@@ -504,7 +615,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
private DeviceReportRespDTO.MQTT buildMqttConfig(String macAddress, String groupId)
|
private DeviceReportRespDTO.MQTT buildMqttConfig(String macAddress, String groupId)
|
||||||
throws Exception {
|
throws Exception {
|
||||||
// 从环境变量或系统参数获取签名密钥
|
// 从环境变量或系统参数获取签名密钥
|
||||||
String signatureKey = sysParamsService.getValue("server.mqtt_signature_key", false);
|
String signatureKey = sysParamsService.getValue("server.mqtt_signature_key", true);
|
||||||
if (StringUtils.isBlank(signatureKey)) {
|
if (StringUtils.isBlank(signatureKey)) {
|
||||||
log.warn("缺少MQTT_SIGNATURE_KEY,跳过MQTT配置生成");
|
log.warn("缺少MQTT_SIGNATURE_KEY,跳过MQTT配置生成");
|
||||||
return null;
|
return null;
|
||||||
@@ -547,4 +658,218 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
|
|
||||||
return mqtt;
|
return mqtt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成BearerToken
|
||||||
|
*/
|
||||||
|
private String generateBearerToken() {
|
||||||
|
try {
|
||||||
|
String dateStr = DateUtil.format(new Date(), DatePattern.NORM_DATE_PATTERN);
|
||||||
|
String signatureKey = sysParamsService.getValue(Constant.SERVER_MQTT_SECRET, false);
|
||||||
|
if (ToolUtil.isEmpty(signatureKey)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return DigestUtil.sha256Hex(dateStr + signatureKey);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getDeviceTools(String deviceId) {
|
||||||
|
// 从系统参数中获取MQTT网关地址
|
||||||
|
String mqttGatewayUrl = sysParamsService.getValue("server.mqtt_manager_api", true);
|
||||||
|
if (StringUtils.isBlank(mqttGatewayUrl) || "null".equals(mqttGatewayUrl)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取设备信息
|
||||||
|
DeviceEntity device = baseDao.selectById(deviceId);
|
||||||
|
if (device == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查设备是否属于当前用户
|
||||||
|
UserDetail user = SecurityUser.getUser();
|
||||||
|
if (!device.getUserId().equals(user.getId())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建clientId
|
||||||
|
String macAddress = Optional.ofNullable(device.getMacAddress()).orElse("unknown").replace(":", "_");
|
||||||
|
String groupId = Optional.ofNullable(device.getBoard()).orElse("GID_default").replace(":", "_");
|
||||||
|
String clientId = StrUtil.format("{}@@@{}@@@{}", groupId, macAddress, macAddress);
|
||||||
|
|
||||||
|
// 构建完整的URL
|
||||||
|
String url = StrUtil.format("http://{}/api/commands/{}", mqttGatewayUrl, clientId);
|
||||||
|
|
||||||
|
// 存储所有工具列表
|
||||||
|
List<Object> allTools = new ArrayList<>();
|
||||||
|
String cursor = null;
|
||||||
|
|
||||||
|
// 循环获取分页数据
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建返回结果
|
||||||
|
if (allTools.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> resultData = new HashMap<>();
|
||||||
|
resultData.put("tools", allTools);
|
||||||
|
return resultData;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object callDeviceTool(String deviceId, String toolName, Map<String, Object> arguments) {
|
||||||
|
// 从系统参数中获取MQTT网关地址
|
||||||
|
String mqttGatewayUrl = sysParamsService.getValue("server.mqtt_manager_api", true);
|
||||||
|
if (StringUtils.isBlank(mqttGatewayUrl) || "null".equals(mqttGatewayUrl)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取设备信息
|
||||||
|
DeviceEntity device = baseDao.selectById(deviceId);
|
||||||
|
if (device == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查设备是否属于当前用户
|
||||||
|
UserDetail user = SecurityUser.getUser();
|
||||||
|
if (!device.getUserId().equals(user.getId())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建clientId
|
||||||
|
String macAddress = Optional.ofNullable(device.getMacAddress()).orElse("unknown").replace(":", "_");
|
||||||
|
String groupId = Optional.ofNullable(device.getBoard()).orElse("GID_default").replace(":", "_");
|
||||||
|
String clientId = StrUtil.format("{}@@@{}@@@{}", groupId, macAddress, macAddress);
|
||||||
|
|
||||||
|
// 构建完整的URL
|
||||||
|
String url = StrUtil.format("http://{}/api/commands/{}", mqttGatewayUrl, clientId);
|
||||||
|
|
||||||
|
// 构建请求体
|
||||||
|
Map<String, Object> params = MapUtil
|
||||||
|
.builder(new HashMap<String, Object>())
|
||||||
|
.put("name", toolName)
|
||||||
|
.put("arguments", arguments)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Map<String, Object> payload = MapUtil
|
||||||
|
.builder(new HashMap<String, Object>())
|
||||||
|
.put("jsonrpc", "2.0")
|
||||||
|
.put("id", 2)
|
||||||
|
.put("method", "tools/call")
|
||||||
|
.put("params", params)
|
||||||
|
.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.isNotBlank(resultMessage)) {
|
||||||
|
cn.hutool.json.JSONObject jsonObject = JSONUtil.parseObj(resultMessage);
|
||||||
|
if (jsonObject.getBool("success", false)) {
|
||||||
|
cn.hutool.json.JSONObject data = jsonObject.getJSONObject("data");
|
||||||
|
if (data != null) {
|
||||||
|
cn.hutool.json.JSONArray content = data.getJSONArray("content");
|
||||||
|
if (content != null && content.size() > 0) {
|
||||||
|
cn.hutool.json.JSONObject firstContent = content.getJSONObject(0);
|
||||||
|
if (firstContent != null && "text".equals(firstContent.getStr("type"))) {
|
||||||
|
String text = firstContent.getStr("text");
|
||||||
|
if (StringUtils.isNotBlank(text)) {
|
||||||
|
String trimmedText = text.trim();
|
||||||
|
if (trimmedText.startsWith("{") || trimmedText.startsWith("[")) {
|
||||||
|
try {
|
||||||
|
return JSONUtil.parseObj(trimmedText);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return trimmedText;
|
||||||
|
}
|
||||||
|
} else if ("true".equals(trimmedText)) {
|
||||||
|
return true;
|
||||||
|
} else if ("false".equals(trimmedText)) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return trimmedText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -66,7 +66,7 @@ public class OtaServiceImpl extends BaseServiceImpl<OtaDao, OtaEntity> implement
|
|||||||
// 同类固件只保留最新的一条
|
// 同类固件只保留最新的一条
|
||||||
List<OtaEntity> otaList = baseDao.selectList(queryWrapper);
|
List<OtaEntity> otaList = baseDao.selectList(queryWrapper);
|
||||||
if (otaList != null && otaList.size() > 0) {
|
if (otaList != null && otaList.size() > 0) {
|
||||||
OtaEntity otaBefore = otaList.getFirst();
|
OtaEntity otaBefore = otaList.get(0);
|
||||||
entity.setId(otaBefore.getId());
|
entity.setId(otaBefore.getId());
|
||||||
baseDao.updateById(entity);
|
baseDao.updateById(entity);
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
package xiaozhi.modules.knowledge.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import xiaozhi.modules.knowledge.rag.KnowledgeBaseAdapterFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 知识库配置类
|
||||||
|
* 配置知识库相关的Bean
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class KnowledgeBaseConfig {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提供KnowledgeBaseAdapterFactory的Bean实例
|
||||||
|
* @return KnowledgeBaseAdapterFactory实例
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
public KnowledgeBaseAdapterFactory knowledgeBaseAdapterFactory() {
|
||||||
|
return new KnowledgeBaseAdapterFactory();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
}
|
||||||
+21
-17
@@ -1,7 +1,6 @@
|
|||||||
package xiaozhi.modules.knowledge.controller;
|
package xiaozhi.modules.knowledge.controller;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.*;
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||||
@@ -24,8 +23,11 @@ import xiaozhi.common.exception.ErrorCode;
|
|||||||
import xiaozhi.common.exception.RenException;
|
import xiaozhi.common.exception.RenException;
|
||||||
import xiaozhi.common.page.PageData;
|
import xiaozhi.common.page.PageData;
|
||||||
import xiaozhi.common.utils.Result;
|
import xiaozhi.common.utils.Result;
|
||||||
|
import xiaozhi.common.utils.ToolUtil;
|
||||||
import xiaozhi.modules.knowledge.dto.KnowledgeBaseDTO;
|
import xiaozhi.modules.knowledge.dto.KnowledgeBaseDTO;
|
||||||
import xiaozhi.modules.knowledge.service.KnowledgeBaseService;
|
import xiaozhi.modules.knowledge.service.KnowledgeBaseService;
|
||||||
|
import xiaozhi.modules.knowledge.service.KnowledgeManagerService;
|
||||||
|
import xiaozhi.modules.model.entity.ModelConfigEntity;
|
||||||
import xiaozhi.modules.security.user.SecurityUser;
|
import xiaozhi.modules.security.user.SecurityUser;
|
||||||
|
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@@ -35,6 +37,7 @@ import xiaozhi.modules.security.user.SecurityUser;
|
|||||||
public class KnowledgeBaseController {
|
public class KnowledgeBaseController {
|
||||||
|
|
||||||
private final KnowledgeBaseService knowledgeBaseService;
|
private final KnowledgeBaseService knowledgeBaseService;
|
||||||
|
private final KnowledgeManagerService knowledgeManagerService;
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
@Operation(summary = "分页查询知识库列表")
|
@Operation(summary = "分页查询知识库列表")
|
||||||
@@ -95,6 +98,8 @@ public class KnowledgeBaseController {
|
|||||||
throw new RenException(ErrorCode.NO_PERMISSION);
|
throw new RenException(ErrorCode.NO_PERMISSION);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// [FIX] 注入 ID,防止 Service 层找不到记录
|
||||||
|
knowledgeBaseDTO.setId(existingKnowledgeBase.getId());
|
||||||
knowledgeBaseDTO.setDatasetId(datasetId);
|
knowledgeBaseDTO.setDatasetId(datasetId);
|
||||||
KnowledgeBaseDTO resp = knowledgeBaseService.update(knowledgeBaseDTO);
|
KnowledgeBaseDTO resp = knowledgeBaseService.update(knowledgeBaseDTO);
|
||||||
return new Result<KnowledgeBaseDTO>().ok(resp);
|
return new Result<KnowledgeBaseDTO>().ok(resp);
|
||||||
@@ -116,7 +121,8 @@ public class KnowledgeBaseController {
|
|||||||
throw new RenException(ErrorCode.NO_PERMISSION);
|
throw new RenException(ErrorCode.NO_PERMISSION);
|
||||||
}
|
}
|
||||||
|
|
||||||
knowledgeBaseService.deleteByDatasetId(datasetId);
|
// [Architecture Fix] 通过编排层级联删除,防止孤儿数据并解决循环依赖
|
||||||
|
knowledgeManagerService.deleteDatasetWithFiles(datasetId);
|
||||||
return new Result<>();
|
return new Result<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,20 +137,18 @@ public class KnowledgeBaseController {
|
|||||||
|
|
||||||
// 获取当前登录用户ID
|
// 获取当前登录用户ID
|
||||||
Long currentUserId = SecurityUser.getUserId();
|
Long currentUserId = SecurityUser.getUserId();
|
||||||
String[] idArray = ids.split(",");
|
List<String> idList = Arrays.asList(ids.split(","));
|
||||||
for (String datasetId : idArray) {
|
List<KnowledgeBaseDTO> knowledgeBaseDTOs = Optional.ofNullable(knowledgeBaseService.getByDatasetIdList(idList))
|
||||||
if (StringUtils.isNotBlank(datasetId)) {
|
.orElseGet(ArrayList::new);
|
||||||
// 先获取现有知识库信息以检查权限
|
if (ToolUtil.isNotEmpty(knowledgeBaseDTOs)) {
|
||||||
KnowledgeBaseDTO existingKnowledgeBase = knowledgeBaseService.getByDatasetId(datasetId.trim());
|
knowledgeBaseDTOs.forEach(item -> {
|
||||||
|
|
||||||
// 检查权限:用户只能删除自己创建的知识库
|
// 检查权限:用户只能删除自己创建的知识库
|
||||||
if (existingKnowledgeBase.getCreator() == null
|
if (item.getCreator() == null || !item.getCreator().equals(currentUserId)) {
|
||||||
|| !existingKnowledgeBase.getCreator().equals(currentUserId)) {
|
|
||||||
throw new RenException(ErrorCode.NO_PERMISSION);
|
throw new RenException(ErrorCode.NO_PERMISSION);
|
||||||
}
|
}
|
||||||
|
// [Architecture Fix] 通过编排层级联删除
|
||||||
knowledgeBaseService.deleteByDatasetId(datasetId.trim());
|
knowledgeManagerService.deleteDatasetWithFiles(item.getDatasetId());
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
return new Result<>();
|
return new Result<>();
|
||||||
}
|
}
|
||||||
@@ -152,8 +156,8 @@ public class KnowledgeBaseController {
|
|||||||
@GetMapping("/rag-models")
|
@GetMapping("/rag-models")
|
||||||
@Operation(summary = "获取RAG模型列表")
|
@Operation(summary = "获取RAG模型列表")
|
||||||
@RequiresPermissions("sys:role:normal")
|
@RequiresPermissions("sys:role:normal")
|
||||||
public Result<List<Map<String, Object>>> getRAGModels() {
|
public Result<List<ModelConfigEntity>> getRAGModels() {
|
||||||
List<Map<String, Object>> result = knowledgeBaseService.getRAGModels();
|
List<ModelConfigEntity> result = knowledgeBaseService.getRAGModels();
|
||||||
return new Result<List<Map<String, Object>>>().ok(result);
|
return new Result<List<ModelConfigEntity>>().ok(result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+64
-62
@@ -4,6 +4,7 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||||
|
import org.springdoc.core.annotations.ParameterObject;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
@@ -11,7 +12,6 @@ import com.fasterxml.jackson.core.type.TypeReference;
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import xiaozhi.common.exception.ErrorCode;
|
import xiaozhi.common.exception.ErrorCode;
|
||||||
@@ -20,6 +20,9 @@ import xiaozhi.common.page.PageData;
|
|||||||
import xiaozhi.common.utils.Result;
|
import xiaozhi.common.utils.Result;
|
||||||
import xiaozhi.modules.knowledge.dto.KnowledgeBaseDTO;
|
import xiaozhi.modules.knowledge.dto.KnowledgeBaseDTO;
|
||||||
import xiaozhi.modules.knowledge.dto.KnowledgeFilesDTO;
|
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.KnowledgeBaseService;
|
||||||
import xiaozhi.modules.knowledge.service.KnowledgeFilesService;
|
import xiaozhi.modules.knowledge.service.KnowledgeFilesService;
|
||||||
import xiaozhi.modules.security.user.SecurityUser;
|
import xiaozhi.modules.security.user.SecurityUser;
|
||||||
@@ -57,22 +60,17 @@ public class KnowledgeFilesController {
|
|||||||
public Result<PageData<KnowledgeFilesDTO>> getPageList(
|
public Result<PageData<KnowledgeFilesDTO>> getPageList(
|
||||||
@PathVariable("dataset_id") String datasetId,
|
@PathVariable("dataset_id") String datasetId,
|
||||||
@RequestParam(required = false) String name,
|
@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 = "1") Integer page,
|
||||||
@RequestParam(required = false, defaultValue = "10") Integer page_size) {
|
@RequestParam(required = false, defaultValue = "10") Integer page_size) {
|
||||||
// 验证知识库权限
|
// 验证知识库权限
|
||||||
validateKnowledgeBasePermission(datasetId);
|
validateKnowledgeBasePermission(datasetId);
|
||||||
|
|
||||||
// 如果指定了状态参数,使用状态查询接口
|
// 组装参数
|
||||||
if (status != null) {
|
|
||||||
PageData<KnowledgeFilesDTO> pageData = knowledgeFilesService.getPageListByStatus(datasetId, status, page, page_size);
|
|
||||||
return new Result<PageData<KnowledgeFilesDTO>>().ok(pageData);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 否则使用通用查询接口
|
|
||||||
KnowledgeFilesDTO knowledgeFilesDTO = new KnowledgeFilesDTO();
|
KnowledgeFilesDTO knowledgeFilesDTO = new KnowledgeFilesDTO();
|
||||||
knowledgeFilesDTO.setDatasetId(datasetId);
|
knowledgeFilesDTO.setDatasetId(datasetId);
|
||||||
knowledgeFilesDTO.setName(name);
|
knowledgeFilesDTO.setName(name);
|
||||||
|
knowledgeFilesDTO.setStatus(status);
|
||||||
PageData<KnowledgeFilesDTO> pageData = knowledgeFilesService.getPageList(knowledgeFilesDTO, page, page_size);
|
PageData<KnowledgeFilesDTO> pageData = knowledgeFilesService.getPageList(knowledgeFilesDTO, page, page_size);
|
||||||
return new Result<PageData<KnowledgeFilesDTO>>().ok(pageData);
|
return new Result<PageData<KnowledgeFilesDTO>>().ok(pageData);
|
||||||
}
|
}
|
||||||
@@ -82,13 +80,16 @@ public class KnowledgeFilesController {
|
|||||||
@RequiresPermissions("sys:role:normal")
|
@RequiresPermissions("sys:role:normal")
|
||||||
public Result<PageData<KnowledgeFilesDTO>> getPageListByStatus(
|
public Result<PageData<KnowledgeFilesDTO>> getPageListByStatus(
|
||||||
@PathVariable("dataset_id") String datasetId,
|
@PathVariable("dataset_id") String datasetId,
|
||||||
@PathVariable("status") Integer status,
|
@PathVariable("status") String status,
|
||||||
@RequestParam(required = false, defaultValue = "1") Integer page,
|
@RequestParam(required = false, defaultValue = "1") Integer page,
|
||||||
@RequestParam(required = false, defaultValue = "10") Integer page_size) {
|
@RequestParam(required = false, defaultValue = "10") Integer page_size) {
|
||||||
// 验证知识库权限
|
// 验证知识库权限
|
||||||
validateKnowledgeBasePermission(datasetId);
|
validateKnowledgeBasePermission(datasetId);
|
||||||
|
// 组装参数
|
||||||
PageData<KnowledgeFilesDTO> pageData = knowledgeFilesService.getPageListByStatus(datasetId, status, page, page_size);
|
KnowledgeFilesDTO knowledgeFilesDTO = new KnowledgeFilesDTO();
|
||||||
|
knowledgeFilesDTO.setDatasetId(datasetId);
|
||||||
|
knowledgeFilesDTO.setStatus(status);
|
||||||
|
PageData<KnowledgeFilesDTO> pageData = knowledgeFilesService.getPageList(knowledgeFilesDTO, page, page_size);
|
||||||
return new Result<PageData<KnowledgeFilesDTO>>().ok(pageData);
|
return new Result<PageData<KnowledgeFilesDTO>>().ok(pageData);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,16 +114,29 @@ public class KnowledgeFilesController {
|
|||||||
return new Result<KnowledgeFilesDTO>().ok(resp);
|
return new Result<KnowledgeFilesDTO>().ok(resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping("/documents/{document_id}")
|
@DeleteMapping("/documents")
|
||||||
@Operation(summary = "删除单个文档")
|
@Operation(summary = "批量删除文档")
|
||||||
@Parameter(name = "document_id", description = "文档ID", required = true)
|
|
||||||
@RequiresPermissions("sys:role:normal")
|
@RequiresPermissions("sys:role:normal")
|
||||||
public Result<Void> delete(@PathVariable("dataset_id") String datasetId,
|
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) {
|
@PathVariable("document_id") String documentId) {
|
||||||
// 验证知识库权限
|
// 验证知识库权限
|
||||||
validateKnowledgeBasePermission(datasetId);
|
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<>();
|
return new Result<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,65 +164,53 @@ public class KnowledgeFilesController {
|
|||||||
@GetMapping("/documents/{document_id}/chunks")
|
@GetMapping("/documents/{document_id}/chunks")
|
||||||
@Operation(summary = "列出指定文档的切片")
|
@Operation(summary = "列出指定文档的切片")
|
||||||
@RequiresPermissions("sys:role:normal")
|
@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,
|
@PathVariable("document_id") String documentId,
|
||||||
@RequestParam(required = false) String keywords,
|
@ParameterObject ChunkDTO.ListReq req) {
|
||||||
@RequestParam(required = false, defaultValue = "1") Integer page,
|
|
||||||
@RequestParam(required = false, defaultValue = "1024") Integer page_size,
|
// 验证权限 (内部已包含知识库存在性校验与归属权校验)
|
||||||
@RequestParam(required = false) String id) {
|
|
||||||
// 验证知识库权限
|
|
||||||
validateKnowledgeBasePermission(datasetId);
|
validateKnowledgeBasePermission(datasetId);
|
||||||
|
|
||||||
Map<String, Object> result = knowledgeFilesService.listChunks(datasetId, documentId, keywords, page, page_size,
|
// 设置默认值
|
||||||
id);
|
if (req.getPage() == null)
|
||||||
return new Result<Map<String, Object>>().ok(result);
|
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")
|
@PostMapping("/retrieval-test")
|
||||||
@Operation(summary = "召回测试")
|
@Operation(summary = "召回测试")
|
||||||
@RequiresPermissions("sys:role:normal")
|
@RequiresPermissions("sys:role:normal")
|
||||||
public Result<Map<String, Object>> retrievalTest(@PathVariable("dataset_id") String datasetId,
|
public Result<RetrievalDTO.ResultVO> retrievalTest(
|
||||||
@RequestBody Map<String, Object> params) {
|
@PathVariable("dataset_id") String datasetId,
|
||||||
|
@RequestBody RetrievalDTO.TestReq req) {
|
||||||
|
|
||||||
// 验证知识库权限
|
// 验证知识库权限
|
||||||
validateKnowledgeBasePermission(datasetId);
|
validateKnowledgeBasePermission(datasetId);
|
||||||
|
|
||||||
try {
|
// 业务下沉逻辑:如果未指定知识库ID,则设为当前路径中的 datasetId
|
||||||
// 提取参数
|
if (req.getDatasetIds() == null || req.getDatasetIds().isEmpty()) {
|
||||||
String question = (String) params.get("question");
|
req.setDatasetIds(java.util.Arrays.asList(datasetId));
|
||||||
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());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// [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对象
|
* 解析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);
|
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 = "知识库名称")
|
@Schema(description = "知识库名称")
|
||||||
private String name;
|
private String name;
|
||||||
|
|
||||||
|
@Schema(description = "知识库头像(Base64)")
|
||||||
|
private String avatar;
|
||||||
|
|
||||||
@Schema(description = "知识库描述")
|
@Schema(description = "知识库描述")
|
||||||
private String 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:启用)")
|
@Schema(description = "状态(0:禁用 1:启用)")
|
||||||
private Integer status;
|
private Integer status;
|
||||||
|
|
||||||
|
|||||||
+28
-5
@@ -6,10 +6,12 @@ import java.util.Date;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@Schema(description = "知识库文档")
|
@Schema(description = "知识库文档")
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
public class KnowledgeFilesDTO implements Serializable {
|
public class KnowledgeFilesDTO implements Serializable {
|
||||||
|
|
||||||
@Serial
|
@Serial
|
||||||
@@ -35,7 +37,19 @@ public class KnowledgeFilesDTO implements Serializable {
|
|||||||
@Schema(description = "文件路径")
|
@Schema(description = "文件路径")
|
||||||
private String filePath;
|
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;
|
private Map<String, Object> metaFields;
|
||||||
|
|
||||||
@Schema(description = "分块方法")
|
@Schema(description = "分块方法")
|
||||||
@@ -44,10 +58,10 @@ public class KnowledgeFilesDTO implements Serializable {
|
|||||||
@Schema(description = "解析器配置")
|
@Schema(description = "解析器配置")
|
||||||
private Map<String, Object> parserConfig;
|
private Map<String, Object> parserConfig;
|
||||||
|
|
||||||
@Schema(description = "状态")
|
@Schema(description = "可用状态 (1: 启用/正常, 0: 禁用/失效)")
|
||||||
private Integer status;
|
private String status;
|
||||||
|
|
||||||
@Schema(description = "文档解析状态")
|
@Schema(description = "运行状态 (UNSTART/RUNNING/CANCEL/DONE/FAIL)")
|
||||||
private String run;
|
private String run;
|
||||||
|
|
||||||
@Schema(description = "创建者")
|
@Schema(description = "创建者")
|
||||||
@@ -62,6 +76,15 @@ public class KnowledgeFilesDTO implements Serializable {
|
|||||||
@Schema(description = "更新时间")
|
@Schema(description = "更新时间")
|
||||||
private Date updatedAt;
|
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_UNSTART = 0;
|
||||||
private static final Integer STATUS_RUNNING = 1;
|
private static final Integer STATUS_RUNNING = 1;
|
||||||
@@ -77,7 +100,7 @@ public class KnowledgeFilesDTO implements Serializable {
|
|||||||
return STATUS_UNSTART;
|
return STATUS_UNSTART;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据run字段的值直接映射到对应的状态码
|
// RAGFlow根据run字段的值直接映射到对应的状态码
|
||||||
switch (run.toUpperCase()) {
|
switch (run.toUpperCase()) {
|
||||||
case "RUNNING":
|
case "RUNNING":
|
||||||
return STATUS_RUNNING;
|
return STATUS_RUNNING;
|
||||||
|
|||||||
@@ -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")
|
@Schema(description = "知识库ID")
|
||||||
private String datasetId;
|
private String datasetId;
|
||||||
|
|
||||||
@Schema(description = "RAG模型配置ID")
|
// @Deprecated
|
||||||
|
@Schema(description = "RAG模型配置ID (连接RAGFlow的凭证指针)")
|
||||||
private String ragModelId;
|
private String ragModelId;
|
||||||
|
|
||||||
|
@Schema(description = "租户ID")
|
||||||
|
private String tenantId;
|
||||||
|
|
||||||
@Schema(description = "知识库名称")
|
@Schema(description = "知识库名称")
|
||||||
private String name;
|
private String name;
|
||||||
|
|
||||||
|
@Schema(description = "知识库头像(Base64)")
|
||||||
|
private String avatar;
|
||||||
|
|
||||||
@Schema(description = "知识库描述")
|
@Schema(description = "知识库描述")
|
||||||
private String 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:启用)")
|
@Schema(description = "状态(0:禁用 1:启用)")
|
||||||
private Integer status;
|
private Integer status;
|
||||||
|
|
||||||
|
|||||||
+224
@@ -0,0 +1,224 @@
|
|||||||
|
package xiaozhi.modules.knowledge.rag;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
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适配器抽象基类
|
||||||
|
* 定义通用的知识库操作接口,支持多种后端API实现
|
||||||
|
*/
|
||||||
|
public abstract class KnowledgeBaseAdapter {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取适配器类型标识
|
||||||
|
*
|
||||||
|
* @return 适配器类型(如:ragflow, milvus, pinecone等)
|
||||||
|
*/
|
||||||
|
public abstract String getAdapterType();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化适配器配置
|
||||||
|
*
|
||||||
|
* @param config 配置参数
|
||||||
|
*/
|
||||||
|
public abstract void initialize(Map<String, Object> config);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证配置是否有效
|
||||||
|
*
|
||||||
|
* @param config 配置参数
|
||||||
|
* @return 验证结果
|
||||||
|
*/
|
||||||
|
public abstract boolean validateConfig(Map<String, Object> config);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询文档列表
|
||||||
|
*
|
||||||
|
* @param datasetId 知识库ID
|
||||||
|
* @param queryParams 查询参数
|
||||||
|
* @param page 页码
|
||||||
|
* @param limit 每页数量
|
||||||
|
* @return 分页数据
|
||||||
|
*/
|
||||||
|
public abstract PageData<KnowledgeFilesDTO> getDocumentList(String datasetId,
|
||||||
|
DocumentDTO.ListReq req);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据文档ID获取文档详情
|
||||||
|
*
|
||||||
|
* @param datasetId 知识库ID
|
||||||
|
* @param documentId 文档ID
|
||||||
|
* @return 文档详情 (强类型 InfoVO)
|
||||||
|
*/
|
||||||
|
public abstract DocumentDTO.InfoVO getDocumentById(String datasetId, String documentId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传文档到知识库
|
||||||
|
*
|
||||||
|
* @param req 上传请求参数
|
||||||
|
* @return 上传的文档信息
|
||||||
|
*/
|
||||||
|
public abstract KnowledgeFilesDTO uploadDocument(DocumentDTO.UploadReq req);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据状态分页查询文档列表
|
||||||
|
*
|
||||||
|
* @param datasetId 知识库ID
|
||||||
|
* @param status 文档解析状态
|
||||||
|
* @param page 页码
|
||||||
|
* @param limit 每页数量
|
||||||
|
* @return 分页数据
|
||||||
|
*/
|
||||||
|
public abstract PageData<KnowledgeFilesDTO> getDocumentListByStatus(String datasetId,
|
||||||
|
Integer status,
|
||||||
|
Integer page,
|
||||||
|
Integer limit);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除文档 (支持批量删除)
|
||||||
|
*
|
||||||
|
* @param datasetId 知识库ID
|
||||||
|
* @param req 包含文档ID列表的请求对象
|
||||||
|
*/
|
||||||
|
public abstract void deleteDocument(String datasetId, DocumentDTO.BatchIdReq req);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析文档(切块)
|
||||||
|
*
|
||||||
|
* @param datasetId 知识库ID
|
||||||
|
* @param documentIds 文档ID列表
|
||||||
|
* @return 解析结果
|
||||||
|
*/
|
||||||
|
public abstract boolean parseDocuments(String datasetId, List<String> documentIds);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 列出指定文档的切片
|
||||||
|
*
|
||||||
|
* @param datasetId 知识库ID
|
||||||
|
* @param documentId 文档ID
|
||||||
|
* @param req 列表请求参数 (分页、关键词等)
|
||||||
|
* @return 切片列表VO
|
||||||
|
*/
|
||||||
|
public abstract ChunkDTO.ListVO listChunks(String datasetId,
|
||||||
|
String documentId,
|
||||||
|
ChunkDTO.ListReq req);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 召回测试 - 从知识库中检索相关切片
|
||||||
|
*
|
||||||
|
* @param req 检索测试请求参数
|
||||||
|
* @return 召回测试结果
|
||||||
|
*/
|
||||||
|
public abstract RetrievalDTO.ResultVO retrievalTest(
|
||||||
|
RetrievalDTO.TestReq req);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试连接
|
||||||
|
*
|
||||||
|
* @return 连接测试结果
|
||||||
|
*/
|
||||||
|
public abstract boolean testConnection();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取适配器状态信息
|
||||||
|
*
|
||||||
|
* @return 状态信息
|
||||||
|
*/
|
||||||
|
public abstract Map<String, Object> getStatus();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取支持的配置参数
|
||||||
|
*
|
||||||
|
* @return 配置参数说明
|
||||||
|
*/
|
||||||
|
public abstract Map<String, Object> getSupportedConfig();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取默认配置
|
||||||
|
*
|
||||||
|
* @return 默认配置
|
||||||
|
*/
|
||||||
|
public abstract Map<String, Object> getDefaultConfig();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建数据集
|
||||||
|
*
|
||||||
|
* @param req 创建参数
|
||||||
|
* @return 数据集详情
|
||||||
|
*/
|
||||||
|
public abstract DatasetDTO.InfoVO createDataset(DatasetDTO.CreateReq req);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新数据集
|
||||||
|
*
|
||||||
|
* @param datasetId 数据集ID
|
||||||
|
* @param req 更新参数
|
||||||
|
* @return 数据集详情
|
||||||
|
*/
|
||||||
|
public abstract DatasetDTO.InfoVO updateDataset(String datasetId, DatasetDTO.UpdateReq req);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除数据集
|
||||||
|
*
|
||||||
|
* @param req 删除请求参数(包含ID列表)
|
||||||
|
* @return 批量操作结果
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
+206
@@ -0,0 +1,206 @@
|
|||||||
|
package xiaozhi.modules.knowledge.rag;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import xiaozhi.common.exception.ErrorCode;
|
||||||
|
import xiaozhi.common.exception.RenException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 知识库适配器工厂类
|
||||||
|
* 负责创建和管理不同类型的知识库API适配器
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class KnowledgeBaseAdapterFactory {
|
||||||
|
|
||||||
|
// 注册的适配器类型映射
|
||||||
|
private static final Map<String, Class<? extends KnowledgeBaseAdapter>> adapterRegistry = new HashMap<>();
|
||||||
|
|
||||||
|
// 适配器实例缓存
|
||||||
|
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);
|
||||||
|
// 可以在这里注册更多适配器类型
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册新的适配器类型
|
||||||
|
*
|
||||||
|
* @param adapterType 适配器类型标识
|
||||||
|
* @param adapterClass 适配器类
|
||||||
|
*/
|
||||||
|
public static void registerAdapter(String adapterType, Class<? extends KnowledgeBaseAdapter> adapterClass) {
|
||||||
|
if (adapterRegistry.containsKey(adapterType)) {
|
||||||
|
log.warn("适配器类型 '{}' 已存在,将被覆盖", adapterType);
|
||||||
|
}
|
||||||
|
adapterRegistry.put(adapterType, adapterClass);
|
||||||
|
log.info("注册适配器类型: {} -> {}", adapterType, adapterClass.getSimpleName());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取适配器实例
|
||||||
|
*
|
||||||
|
* @param adapterType 适配器类型
|
||||||
|
* @param config 配置参数
|
||||||
|
* @return 适配器实例
|
||||||
|
*/
|
||||||
|
public static KnowledgeBaseAdapter getAdapter(String adapterType, Map<String, Object> config) {
|
||||||
|
String cacheKey = buildCacheKey(adapterType, config);
|
||||||
|
|
||||||
|
// 检查缓存中是否已存在实例
|
||||||
|
if (adapterCache.containsKey(cacheKey)) {
|
||||||
|
log.debug("从缓存获取适配器实例: {}", cacheKey);
|
||||||
|
return adapterCache.get(cacheKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建新的适配器实例
|
||||||
|
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);
|
||||||
|
|
||||||
|
return adapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取适配器实例(无配置)
|
||||||
|
*
|
||||||
|
* @param adapterType 适配器类型
|
||||||
|
* @return 适配器实例
|
||||||
|
*/
|
||||||
|
public static KnowledgeBaseAdapter getAdapter(String adapterType) {
|
||||||
|
return getAdapter(adapterType, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有已注册的适配器类型
|
||||||
|
*
|
||||||
|
* @return 适配器类型集合
|
||||||
|
*/
|
||||||
|
public static Set<String> getRegisteredAdapterTypes() {
|
||||||
|
return adapterRegistry.keySet();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查适配器类型是否已注册
|
||||||
|
*
|
||||||
|
* @param adapterType 适配器类型
|
||||||
|
* @return 是否已注册
|
||||||
|
*/
|
||||||
|
public static boolean isAdapterTypeRegistered(String adapterType) {
|
||||||
|
return adapterRegistry.containsKey(adapterType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除适配器缓存
|
||||||
|
*/
|
||||||
|
public static void clearCache() {
|
||||||
|
int cacheSize = adapterCache.size();
|
||||||
|
adapterCache.clear();
|
||||||
|
log.info("清除适配器缓存,共清除 {} 个实例", cacheSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除特定适配器类型的缓存
|
||||||
|
*
|
||||||
|
* @param adapterType 适配器类型
|
||||||
|
*/
|
||||||
|
public static void removeCacheByType(String adapterType) {
|
||||||
|
int removedCount = 0;
|
||||||
|
for (String cacheKey : adapterCache.keySet()) {
|
||||||
|
if (cacheKey.startsWith(adapterType + "@")) {
|
||||||
|
adapterCache.remove(cacheKey);
|
||||||
|
removedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.info("移除适配器类型 '{}' 的缓存,共移除 {} 个实例", adapterType, removedCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取适配器工厂状态信息
|
||||||
|
*
|
||||||
|
* @return 状态信息
|
||||||
|
*/
|
||||||
|
public static Map<String, Object> getFactoryStatus() {
|
||||||
|
Map<String, Object> status = new HashMap<>();
|
||||||
|
status.put("registeredAdapterTypes", adapterRegistry.keySet());
|
||||||
|
status.put("cachedAdapterCount", adapterCache.size());
|
||||||
|
status.put("cacheKeys", adapterCache.keySet());
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建适配器实例
|
||||||
|
*
|
||||||
|
* @param adapterType 适配器类型
|
||||||
|
* @param config 配置参数
|
||||||
|
* @return 适配器实例
|
||||||
|
*/
|
||||||
|
private static KnowledgeBaseAdapter createAdapter(String adapterType, Map<String, Object> config) {
|
||||||
|
if (!adapterRegistry.containsKey(adapterType)) {
|
||||||
|
throw new RenException(ErrorCode.RAG_ADAPTER_TYPE_NOT_SUPPORTED,
|
||||||
|
"不支持的适配器类型: " + adapterType);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Class<? extends KnowledgeBaseAdapter> adapterClass = adapterRegistry.get(adapterType);
|
||||||
|
KnowledgeBaseAdapter adapter = adapterClass.getDeclaredConstructor().newInstance();
|
||||||
|
|
||||||
|
// 初始化适配器
|
||||||
|
if (config != null) {
|
||||||
|
adapter.initialize(config);
|
||||||
|
|
||||||
|
// 验证配置
|
||||||
|
if (!adapter.validateConfig(config)) {
|
||||||
|
throw new RenException(ErrorCode.RAG_CONFIG_VALIDATION_FAILED,
|
||||||
|
"适配器配置验证失败: " + adapterType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("成功创建适配器实例: {}", adapterType);
|
||||||
|
return adapter;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("创建适配器实例失败: {}", adapterType, e);
|
||||||
|
throw new RenException(ErrorCode.RAG_ADAPTER_CREATION_FAILED,
|
||||||
|
"创建适配器失败: " + adapterType + ", 错误: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建缓存键
|
||||||
|
*
|
||||||
|
* @param adapterType 适配器类型
|
||||||
|
* @param config 配置参数
|
||||||
|
* @return 缓存键
|
||||||
|
*/
|
||||||
|
private static String buildCacheKey(String adapterType, Map<String, Object> config) {
|
||||||
|
if (config == null || config.isEmpty()) {
|
||||||
|
return adapterType + "@default";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 基于配置参数生成缓存键
|
||||||
|
StringBuilder keyBuilder = new StringBuilder(adapterType + "@");
|
||||||
|
|
||||||
|
// 使用配置的哈希值作为缓存键的一部分
|
||||||
|
int configHash = config.hashCode();
|
||||||
|
keyBuilder.append(configHash);
|
||||||
|
|
||||||
|
return keyBuilder.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+725
@@ -0,0 +1,725 @@
|
|||||||
|
package xiaozhi.modules.knowledge.rag.impl;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springframework.core.io.AbstractResource;
|
||||||
|
import org.springframework.util.LinkedMultiValueMap;
|
||||||
|
import org.springframework.util.MultiValueMap;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import xiaozhi.common.exception.ErrorCode;
|
||||||
|
import xiaozhi.common.exception.RenException;
|
||||||
|
import xiaozhi.common.page.PageData;
|
||||||
|
import xiaozhi.modules.knowledge.dto.KnowledgeFilesDTO;
|
||||||
|
import xiaozhi.modules.knowledge.dto.dataset.DatasetDTO;
|
||||||
|
import xiaozhi.modules.knowledge.dto.document.ChunkDTO;
|
||||||
|
import xiaozhi.modules.knowledge.dto.document.RetrievalDTO;
|
||||||
|
import xiaozhi.modules.knowledge.dto.document.DocumentDTO;
|
||||||
|
import xiaozhi.modules.knowledge.rag.KnowledgeBaseAdapter;
|
||||||
|
import xiaozhi.modules.knowledge.rag.RAGFlowClient;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RAGFlow知识库适配器实现
|
||||||
|
* <p>
|
||||||
|
* 重构说明 (Refactoring Note):
|
||||||
|
* 本类已升级为使用 {@link RAGFlowClient} 统一处理 HTTP 通信。
|
||||||
|
* 解决了旧代码中 Timeout 缺失、Error Handling 分散的问题。
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class RAGFlowAdapter extends KnowledgeBaseAdapter {
|
||||||
|
|
||||||
|
private static final String ADAPTER_TYPE = "ragflow";
|
||||||
|
|
||||||
|
private Map<String, Object> config;
|
||||||
|
private ObjectMapper objectMapper;
|
||||||
|
// Client 实例,初始化时创建
|
||||||
|
private RAGFlowClient client;
|
||||||
|
|
||||||
|
public RAGFlowAdapter() {
|
||||||
|
this.objectMapper = new ObjectMapper();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getAdapterType() {
|
||||||
|
return ADAPTER_TYPE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initialize(Map<String, Object> config) {
|
||||||
|
this.config = config;
|
||||||
|
validateConfig(config);
|
||||||
|
|
||||||
|
String baseUrl = getConfigValue(config, "base_url", "baseUrl");
|
||||||
|
String apiKey = getConfigValue(config, "api_key", "apiKey");
|
||||||
|
|
||||||
|
// 初始化 Client,默认超时 30s,可通过 config 扩展
|
||||||
|
int timeout = 30;
|
||||||
|
Object timeoutObj = getConfigValue(config, "timeout", "timeout");
|
||||||
|
if (timeoutObj != null) {
|
||||||
|
try {
|
||||||
|
timeout = Integer.parseInt(timeoutObj.toString());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("解析超时配置失败,使用默认值 30s");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.client = new RAGFlowClient(baseUrl, apiKey, timeout);
|
||||||
|
log.info("RAGFlow适配器初始化完成,Client已就绪");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean validateConfig(Map<String, Object> config) {
|
||||||
|
if (config == null) {
|
||||||
|
throw new RenException(ErrorCode.RAG_CONFIG_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
String baseUrl = getConfigValue(config, "base_url", "baseUrl");
|
||||||
|
String apiKey = getConfigValue(config, "api_key", "apiKey");
|
||||||
|
|
||||||
|
if (StringUtils.isBlank(baseUrl)) {
|
||||||
|
throw new RenException(ErrorCode.RAG_API_ERROR_URL_NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (StringUtils.isBlank(apiKey)) {
|
||||||
|
throw new RenException(ErrorCode.RAG_API_ERROR_API_KEY_NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (apiKey.contains("你")) {
|
||||||
|
throw new RenException(ErrorCode.RAG_API_ERROR_API_KEY_INVALID);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!baseUrl.startsWith("http://") && !baseUrl.startsWith("https://")) {
|
||||||
|
throw new RenException(ErrorCode.RAG_API_ERROR_URL_INVALID);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 辅助方法:支持多种键名获取配置(兼容 camelCase 和 snake_case)
|
||||||
|
*/
|
||||||
|
private String getConfigValue(Map<String, Object> config, String snakeKey, String camelKey) {
|
||||||
|
if (config.containsKey(snakeKey)) {
|
||||||
|
return (String) config.get(snakeKey);
|
||||||
|
}
|
||||||
|
if (config.containsKey(camelKey)) {
|
||||||
|
return (String) config.get(camelKey);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 辅助方法:确保 Client 已初始化
|
||||||
|
*/
|
||||||
|
private RAGFlowClient getClient() {
|
||||||
|
if (this.client == null) {
|
||||||
|
// 尝试重新初始化
|
||||||
|
if (this.config != null) {
|
||||||
|
initialize(this.config);
|
||||||
|
} else {
|
||||||
|
throw new RenException(ErrorCode.RAG_CONFIG_NOT_FOUND, "适配器未初始化"); // 应该抛出 RuntimeException
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.client;
|
||||||
|
}
|
||||||
|
|
||||||
|
private RenException convertToRenException(Exception e) {
|
||||||
|
if (e instanceof RenException) {
|
||||||
|
return (RenException) e;
|
||||||
|
}
|
||||||
|
return new RenException(ErrorCode.RAG_API_ERROR, e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PageData<KnowledgeFilesDTO> getDocumentList(String datasetId, DocumentDTO.ListReq req) {
|
||||||
|
try {
|
||||||
|
log.info("=== [RAGFlow] 获取文档列表: datasetId={} ===", datasetId);
|
||||||
|
|
||||||
|
// 使用 Jackson 将 DTO 转为 Map 作为查询参数
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
Map<String, Object> params = objectMapper.convertValue(req, Map.class);
|
||||||
|
|
||||||
|
Map<String, Object> response = getClient().get("/api/v1/datasets/" + datasetId + "/documents", params);
|
||||||
|
|
||||||
|
Object dataObj = response.get("data");
|
||||||
|
return parseDocumentListResponse(dataObj, req.getPage() != null ? req.getPage() : 1,
|
||||||
|
req.getPageSize() != null ? req.getPageSize() : 10);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("获取文档列表失败", e);
|
||||||
|
throw convertToRenException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DocumentDTO.InfoVO getDocumentById(String datasetId, String documentId) {
|
||||||
|
try {
|
||||||
|
log.info("=== [RAGFlow] 获取文档详情: datasetId={}, documentId={} ===", datasetId, documentId);
|
||||||
|
DocumentDTO.ListReq req = DocumentDTO.ListReq.builder()
|
||||||
|
.id(documentId)
|
||||||
|
.page(1)
|
||||||
|
.pageSize(1)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
Map<String, Object> params = objectMapper.convertValue(req, Map.class);
|
||||||
|
Map<String, Object> response = getClient().get("/api/v1/datasets/" + datasetId + "/documents", params);
|
||||||
|
|
||||||
|
Object dataObj = response.get("data");
|
||||||
|
if (dataObj instanceof Map) {
|
||||||
|
Map<String, Object> dataMap = (Map<String, Object>) dataObj;
|
||||||
|
List<?> documents = (List<?>) dataMap.get("docs");
|
||||||
|
if (documents != null && !documents.isEmpty()) {
|
||||||
|
return objectMapper.convertValue(documents.get(0), DocumentDTO.InfoVO.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("获取文档详情失败: documentId={}", documentId, e);
|
||||||
|
throw convertToRenException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public KnowledgeFilesDTO uploadDocument(DocumentDTO.UploadReq req) {
|
||||||
|
String datasetId = req.getDatasetId();
|
||||||
|
MultipartFile file = req.getFile();
|
||||||
|
try {
|
||||||
|
log.info("=== [RAGFlow] 上传文档: datasetId={} ===", datasetId);
|
||||||
|
|
||||||
|
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
|
||||||
|
body.add("file", new MultipartFileResource(file));
|
||||||
|
|
||||||
|
if (StringUtils.isNotBlank(req.getName())) {
|
||||||
|
body.add("name", req.getName());
|
||||||
|
}
|
||||||
|
if (req.getMetaFields() != null && !req.getMetaFields().isEmpty()) {
|
||||||
|
body.add("meta", objectMapper.writeValueAsString(req.getMetaFields()));
|
||||||
|
}
|
||||||
|
if (req.getChunkMethod() != null) {
|
||||||
|
// 将枚举值转为 RAGFlow 期待的字符串(如 NAIVE -> naive)
|
||||||
|
body.add("chunk_method", req.getChunkMethod().name().toLowerCase());
|
||||||
|
}
|
||||||
|
if (req.getParserConfig() != null) {
|
||||||
|
body.add("parser_config", objectMapper.writeValueAsString(req.getParserConfig()));
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotBlank(req.getParentPath())) {
|
||||||
|
body.add("parent_path", req.getParentPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> response = getClient().postMultipart("/api/v1/datasets/" + datasetId + "/documents",
|
||||||
|
body);
|
||||||
|
|
||||||
|
Object dataObj = response.get("data");
|
||||||
|
return parseUploadResponse(dataObj, datasetId, file);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("文档上传失败", e);
|
||||||
|
throw convertToRenException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PageData<KnowledgeFilesDTO> getDocumentListByStatus(String datasetId, Integer status, Integer page,
|
||||||
|
Integer limit) {
|
||||||
|
List<DocumentDTO.InfoVO.RunStatus> runStatusList = null;
|
||||||
|
if (status != null) {
|
||||||
|
runStatusList = new ArrayList<>();
|
||||||
|
switch (status) {
|
||||||
|
case 0:
|
||||||
|
runStatusList.add(DocumentDTO.InfoVO.RunStatus.UNSTART);
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
runStatusList.add(DocumentDTO.InfoVO.RunStatus.RUNNING);
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
runStatusList.add(DocumentDTO.InfoVO.RunStatus.CANCEL);
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
runStatusList.add(DocumentDTO.InfoVO.RunStatus.DONE);
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
runStatusList.add(DocumentDTO.InfoVO.RunStatus.FAIL);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DocumentDTO.ListReq req = DocumentDTO.ListReq.builder()
|
||||||
|
.run(runStatusList)
|
||||||
|
.page(page)
|
||||||
|
.pageSize(limit)
|
||||||
|
.build();
|
||||||
|
return getDocumentList(datasetId, req);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deleteDocument(String datasetId, DocumentDTO.BatchIdReq req) {
|
||||||
|
try {
|
||||||
|
log.info("=== [RAGFlow] 批量删除文档: datasetId={}, count={} ===", datasetId,
|
||||||
|
req.getIds() != null ? req.getIds().size() : 0);
|
||||||
|
getClient().delete("/api/v1/datasets/" + datasetId + "/documents", req);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("批量删除文档失败: datasetId={}", datasetId, e);
|
||||||
|
throw convertToRenException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean parseDocuments(String datasetId, List<String> documentIds) {
|
||||||
|
try {
|
||||||
|
log.info("=== [RAGFlow] 解析文档 ===");
|
||||||
|
Map<String, Object> body = new HashMap<>();
|
||||||
|
body.put("document_ids", documentIds);
|
||||||
|
|
||||||
|
getClient().post("/api/v1/datasets/" + datasetId + "/chunks", body);
|
||||||
|
return true;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("解析文档失败", e);
|
||||||
|
throw convertToRenException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ChunkDTO.ListVO listChunks(String datasetId, String documentId, ChunkDTO.ListReq req) {
|
||||||
|
try {
|
||||||
|
// [提灯重构] 使用 objectMapper 动态转换查询参数,消除硬编码
|
||||||
|
Map<String, Object> params = objectMapper.convertValue(req, new TypeReference<Map<String, Object>>() {
|
||||||
|
});
|
||||||
|
|
||||||
|
Map<String, Object> response = getClient()
|
||||||
|
.get("/api/v1/datasets/" + datasetId + "/documents/" + documentId + "/chunks", params);
|
||||||
|
|
||||||
|
Object dataObj = response.get("data");
|
||||||
|
if (dataObj == null) {
|
||||||
|
log.warn("[RAGFlow] listChunks 响应 data 为空, docId={}", documentId);
|
||||||
|
return ChunkDTO.ListVO.builder()
|
||||||
|
.chunks(new ArrayList<>())
|
||||||
|
.total(0L)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
ChunkDTO.ListVO result = objectMapper.convertValue(dataObj, ChunkDTO.ListVO.class);
|
||||||
|
if (result.getTotal() == null) {
|
||||||
|
result.setTotal(0L);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("获取切片失败: docId={}", documentId, e);
|
||||||
|
throw convertToRenException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RetrievalDTO.ResultVO retrievalTest(RetrievalDTO.TestReq req) {
|
||||||
|
try {
|
||||||
|
// [Production Reinforce] 参数防御性对齐:RAGFlow Python 端对 0 或负数分页敏感
|
||||||
|
// 解决 ValueError('Search does not support negative slicing.')
|
||||||
|
if (req.getPage() != null && req.getPage() < 1) {
|
||||||
|
req.setPage(1);
|
||||||
|
}
|
||||||
|
if (req.getPageSize() != null && req.getPageSize() < 1) {
|
||||||
|
req.setPageSize(10); // 默认 10 条
|
||||||
|
}
|
||||||
|
if (req.getTopK() != null && req.getTopK() < 1) {
|
||||||
|
req.setTopK(1024); // RAGFlow 内部默认 TopK
|
||||||
|
}
|
||||||
|
// 相似度阈值归一化 (0.0 ~ 1.0)
|
||||||
|
if (req.getSimilarityThreshold() != null) {
|
||||||
|
if (req.getSimilarityThreshold() < 0f)
|
||||||
|
req.setSimilarityThreshold(0.2f);
|
||||||
|
if (req.getSimilarityThreshold() > 1f)
|
||||||
|
req.setSimilarityThreshold(1.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// [提灯重构] 直接透传强类型 DTO,由 getClient 处理序列化
|
||||||
|
Map<String, Object> response = getClient().post("/api/v1/retrieval", req);
|
||||||
|
|
||||||
|
Object dataObj = response.get("data");
|
||||||
|
if (dataObj == null) {
|
||||||
|
log.warn("[RAGFlow] retrievalTest 响应 data 为空");
|
||||||
|
return RetrievalDTO.ResultVO.builder()
|
||||||
|
.chunks(new ArrayList<>())
|
||||||
|
.docAggs(new ArrayList<>())
|
||||||
|
.total(0L)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
RetrievalDTO.ResultVO result = objectMapper.convertValue(dataObj, RetrievalDTO.ResultVO.class);
|
||||||
|
if (result.getTotal() == null) {
|
||||||
|
result.setTotal(0L);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("召回测试失败", e);
|
||||||
|
throw convertToRenException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean testConnection() {
|
||||||
|
try {
|
||||||
|
getClient().get("/api/v1/health", null);
|
||||||
|
return true;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("连接测试失败: {}", e.getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> getStatus() {
|
||||||
|
Map<String, Object> status = new HashMap<>();
|
||||||
|
status.put("adapterType", getAdapterType());
|
||||||
|
status.put("configKeys", config != null ? config.keySet() : "未配置");
|
||||||
|
status.put("connectionTest", testConnection());
|
||||||
|
status.put("lastChecked", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> getSupportedConfig() {
|
||||||
|
Map<String, Object> supportedConfig = new HashMap<>();
|
||||||
|
supportedConfig.put("base_url", "RAGFlow API基础URL");
|
||||||
|
supportedConfig.put("api_key", "RAGFlow API密钥");
|
||||||
|
supportedConfig.put("timeout", "请求超时时间(毫秒)");
|
||||||
|
return supportedConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> getDefaultConfig() {
|
||||||
|
Map<String, Object> defaultConfig = new HashMap<>();
|
||||||
|
defaultConfig.put("timeout", 30000);
|
||||||
|
return defaultConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DatasetDTO.InfoVO createDataset(DatasetDTO.CreateReq req) {
|
||||||
|
try {
|
||||||
|
// [Production Fix] 强化默认值处理,防止 RAGFlow API 因空字符串或缺失字段报错 (Code 101)
|
||||||
|
// 解决 "Field: <avatar> - Message: <Missing MIME prefix>" 等校验失败
|
||||||
|
if (StringUtils.isBlank(req.getPermission())) {
|
||||||
|
req.setPermission("me");
|
||||||
|
}
|
||||||
|
if (StringUtils.isBlank(req.getChunkMethod())) {
|
||||||
|
req.setChunkMethod("naive");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🤖 自动补全嵌入模型:优先使用请求传参,其次使用配置中的默认模型
|
||||||
|
if (StringUtils.isBlank(req.getEmbeddingModel())) {
|
||||||
|
String defaultModel = (String) getConfigValue(config, "embedding_model", "embeddingModel");
|
||||||
|
if (StringUtils.isNotBlank(defaultModel)) {
|
||||||
|
log.info("RAGFlow: 使用配置中的默认嵌入模型: {}", defaultModel);
|
||||||
|
req.setEmbeddingModel(defaultModel);
|
||||||
|
}
|
||||||
|
// 若配置中也无默认值,则留空由 RAGFlow 服务端自行兜底(或抛出业务异常)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🖼️ 自动补全头像:若为空则提供一个 1x1 透明像素,防止 RAGFlow 校验 MIME Prefix 失败
|
||||||
|
if (StringUtils.isBlank(req.getAvatar())) {
|
||||||
|
req.setAvatar(
|
||||||
|
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 直接将强类型请求对象传给 Client,Jackson 会处理 JsonProperty 映射
|
||||||
|
Map<String, Object> response = getClient().post("/api/v1/datasets", req);
|
||||||
|
|
||||||
|
// 安全地获取 data 并通过 DatasetDTO.InfoVO 进行全量映射
|
||||||
|
Object dataObj = response.get("data");
|
||||||
|
if (dataObj != null) {
|
||||||
|
return objectMapper.convertValue(dataObj, DatasetDTO.InfoVO.class);
|
||||||
|
}
|
||||||
|
throw new RenException(ErrorCode.RAG_API_ERROR, "Invalid response from createDataset: missing data object");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("创建数据集失败", e);
|
||||||
|
throw convertToRenException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DatasetDTO.InfoVO updateDataset(String datasetId, DatasetDTO.UpdateReq req) {
|
||||||
|
try {
|
||||||
|
// RAGFlow API 更新建议路径带 ID
|
||||||
|
Map<String, Object> response = getClient().put("/api/v1/datasets/" + datasetId, req);
|
||||||
|
|
||||||
|
Object dataObj = response.get("data");
|
||||||
|
if (dataObj != null) {
|
||||||
|
return objectMapper.convertValue(dataObj, DatasetDTO.InfoVO.class);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("更新数据集失败", e);
|
||||||
|
throw convertToRenException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DatasetDTO.BatchOperationVO deleteDataset(DatasetDTO.BatchIdReq req) {
|
||||||
|
try {
|
||||||
|
// RAGFlow 批量删除接口使用 DELETE /api/v1/datasets
|
||||||
|
Map<String, Object> response = getClient().delete("/api/v1/datasets", req);
|
||||||
|
|
||||||
|
Object dataObj = response.get("data");
|
||||||
|
if (dataObj != null) {
|
||||||
|
return objectMapper.convertValue(dataObj, DatasetDTO.BatchOperationVO.class);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("批量删除数据集失败", e);
|
||||||
|
throw convertToRenException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Integer getDocumentCount(String datasetId) {
|
||||||
|
try {
|
||||||
|
DatasetDTO.InfoVO info = getDatasetInfo(datasetId);
|
||||||
|
if (info != null && info.getDocumentCount() != null) {
|
||||||
|
return info.getDocumentCount().intValue();
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("获取文档数量失败: {}", e.getMessage());
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DatasetDTO.InfoVO getDatasetInfo(String datasetId) {
|
||||||
|
try {
|
||||||
|
Map<String, Object> params = new HashMap<>();
|
||||||
|
params.put("id", datasetId);
|
||||||
|
params.put("page", 1);
|
||||||
|
params.put("page_size", 1);
|
||||||
|
|
||||||
|
Map<String, Object> response = getClient().get("/api/v1/datasets", params);
|
||||||
|
Object dataObj = response.get("data");
|
||||||
|
|
||||||
|
if (dataObj instanceof List) {
|
||||||
|
List<?> list = (List<?>) dataObj;
|
||||||
|
if (!list.isEmpty()) {
|
||||||
|
return objectMapper.convertValue(list.get(0), DatasetDTO.InfoVO.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// RAGFlow 端不存在该数据集
|
||||||
|
return null;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("获取数据集信息失败: datasetId={}, error={}", datasetId, e.getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void postStream(String endpoint, Object body, Consumer<String> onData) {
|
||||||
|
try {
|
||||||
|
getClient().postStream(endpoint, body, onData);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("流式请求失败", e);
|
||||||
|
throw convertToRenException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object postSearchBotAsk(Map<String, Object> config, Object body,
|
||||||
|
Consumer<String> onData) {
|
||||||
|
// SearchBot 实际上是 Dataset 检索的一种封装,或者是未公开的 API?
|
||||||
|
// 假设 RAGFlow 没有显式的 /searchbots 接口供 SDK 调用,而是 Dataset Retrieval 或者 Chat。
|
||||||
|
// 但根据 BotDTO,它是 /api/v1/searchbots/ask (假设)
|
||||||
|
// 这里的 config 可能是覆盖用的,或者我们只是用 adapter 实例已有的 client。
|
||||||
|
// 但 Bot 可能使用不同的 API Key?通常 Adapter 实例绑定了一个 Key。
|
||||||
|
// 如果 Bot 使用系统 Key,则直接用 getClient()。
|
||||||
|
|
||||||
|
// 暂时假设 endpoint /api/v1/searchbots/ask 存在(或者类似的)
|
||||||
|
// 如果是流式:
|
||||||
|
try {
|
||||||
|
getClient().postStream("/api/v1/searchbots/ask", body, onData);
|
||||||
|
return null;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("SearchBot Ask 失败", e);
|
||||||
|
throw convertToRenException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void postAgentBotCompletion(Map<String, Object> config, String agentId, Object body,
|
||||||
|
Consumer<String> onData) {
|
||||||
|
// AgentBot 对应 /api/v1/agentbots/{id}/completions
|
||||||
|
try {
|
||||||
|
getClient().postStream("/api/v1/agentbots/" + agentId + "/completions", body, onData);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("AgentBot Completion 失败", e);
|
||||||
|
throw convertToRenException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 复用原有的辅助解析方法,保持兼容
|
||||||
|
// [Bug Fix] 不再吞掉反序列化异常,避免上层误判"文档已删除"
|
||||||
|
private PageData<KnowledgeFilesDTO> parseDocumentListResponse(Object dataObj, long curPage, long pageSize) {
|
||||||
|
if (dataObj == null) {
|
||||||
|
return new PageData<>(new ArrayList<>(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> dataMap = (Map<String, Object>) dataObj;
|
||||||
|
List<Map<String, Object>> documents = (List<Map<String, Object>>) dataMap.get("docs");
|
||||||
|
if (documents == null || documents.isEmpty()) {
|
||||||
|
// RAGFlow 明确返回了空文档列表,这是合法的"真空"
|
||||||
|
return new PageData<>(new ArrayList<>(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<KnowledgeFilesDTO> list = new ArrayList<>();
|
||||||
|
for (Object docObj : documents) {
|
||||||
|
try {
|
||||||
|
// 单文档转换容错:一个文档反序列化失败不影响其他文档
|
||||||
|
DocumentDTO.InfoVO info = objectMapper.convertValue(docObj, DocumentDTO.InfoVO.class);
|
||||||
|
list.add(mapToKnowledgeFilesDTO(info, null));
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[RAGFlow] 单文档 DTO 转换失败,跳过该文档: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
long total = 0;
|
||||||
|
if (dataMap.containsKey("total")) {
|
||||||
|
total = ((Number) dataMap.get("total")).longValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new PageData<>(list, total);
|
||||||
|
}
|
||||||
|
|
||||||
|
private KnowledgeFilesDTO parseUploadResponse(Object dataObj, String datasetId, MultipartFile file) {
|
||||||
|
KnowledgeFilesDTO result = null;
|
||||||
|
|
||||||
|
// 尝试从响应数据中提取文档ID (documentId)
|
||||||
|
if (dataObj != null) {
|
||||||
|
try {
|
||||||
|
DocumentDTO.InfoVO info = null;
|
||||||
|
if (dataObj instanceof Map) {
|
||||||
|
info = objectMapper.convertValue(dataObj, DocumentDTO.InfoVO.class);
|
||||||
|
} else if (dataObj instanceof List) {
|
||||||
|
List<?> list = (List<?>) dataObj;
|
||||||
|
if (!list.isEmpty()) {
|
||||||
|
info = objectMapper.convertValue(list.get(0), DocumentDTO.InfoVO.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (info != null) {
|
||||||
|
result = mapToKnowledgeFilesDTO(info, datasetId);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("解析上传响应数据失败: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result == null) {
|
||||||
|
log.error("未能从RAGFlow响应中提取到documentId,响应内容: {}", dataObj);
|
||||||
|
// 这里应该返回一个最小化的包含基础信息的 DTO 而不是 null,防止上游 NPE
|
||||||
|
result = new KnowledgeFilesDTO();
|
||||||
|
result.setDatasetId(datasetId);
|
||||||
|
result.setName(file.getOriginalFilename());
|
||||||
|
result.setFileSize(file.getSize());
|
||||||
|
result.setStatus("1");
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 RAGFlow 的强类型 InfoVO 映射到内部使用的 KnowledgeFilesDTO
|
||||||
|
* 确保所有可用字段(名称、大小、状态、配置等)都得到全量同步
|
||||||
|
*/
|
||||||
|
private KnowledgeFilesDTO mapToKnowledgeFilesDTO(DocumentDTO.InfoVO info, String datasetId) {
|
||||||
|
KnowledgeFilesDTO dto = new KnowledgeFilesDTO();
|
||||||
|
if (info == null)
|
||||||
|
return dto;
|
||||||
|
|
||||||
|
dto.setId(info.getId());
|
||||||
|
dto.setDocumentId(info.getId());
|
||||||
|
dto.setDatasetId(StringUtils.isNotBlank(info.getDatasetId()) ? info.getDatasetId() : datasetId);
|
||||||
|
dto.setName(info.getName());
|
||||||
|
dto.setFileSize(info.getSize());
|
||||||
|
|
||||||
|
// 状态映射
|
||||||
|
if (info.getRun() != null) {
|
||||||
|
dto.setRun(info.getRun().name());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (StringUtils.isNotBlank(info.getStatus())) {
|
||||||
|
dto.setStatus(info.getStatus());
|
||||||
|
} else {
|
||||||
|
dto.setStatus("1"); // 默认启用
|
||||||
|
}
|
||||||
|
|
||||||
|
// 时间同步
|
||||||
|
if (info.getCreateTime() != null) {
|
||||||
|
dto.setCreatedAt(new Date(info.getCreateTime()));
|
||||||
|
}
|
||||||
|
if (info.getUpdateTime() != null) {
|
||||||
|
dto.setUpdatedAt(new Date(info.getUpdateTime()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 核心元数据补齐 (Issue 1)
|
||||||
|
dto.setProgress(info.getProgress());
|
||||||
|
dto.setThumbnail(info.getThumbnail());
|
||||||
|
dto.setProcessDuration(info.getProcessDuration());
|
||||||
|
dto.setSourceType(info.getSourceType());
|
||||||
|
dto.setChunkCount(info.getChunkCount() != null ? info.getChunkCount().intValue() : 0);
|
||||||
|
dto.setTokenCount(info.getTokenCount());
|
||||||
|
dto.setError(info.getProgressMsg()); // 将进度描述映射为错误信息提示
|
||||||
|
|
||||||
|
// 扩展字段同步
|
||||||
|
dto.setMetaFields(info.getMetaFields());
|
||||||
|
if (info.getChunkMethod() != null) {
|
||||||
|
dto.setChunkMethod(info.getChunkMethod().name().toLowerCase());
|
||||||
|
}
|
||||||
|
if (info.getParserConfig() != null) {
|
||||||
|
dto.setParserConfig(objectMapper.convertValue(info.getParserConfig(), Map.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
return dto;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class MultipartFileResource extends AbstractResource {
|
||||||
|
private final MultipartFile multipartFile;
|
||||||
|
|
||||||
|
public MultipartFileResource(MultipartFile multipartFile) {
|
||||||
|
this.multipartFile = multipartFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDescription() {
|
||||||
|
return "MultipartFile resource [" + multipartFile.getOriginalFilename() + "]";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getFilename() {
|
||||||
|
return multipartFile.getOriginalFilename();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InputStream getInputStream() throws IOException {
|
||||||
|
return multipartFile.getInputStream();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long contentLength() throws IOException {
|
||||||
|
return multipartFile.getSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean exists() {
|
||||||
|
return !multipartFile.isEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+20
-1
@@ -7,6 +7,7 @@ import xiaozhi.common.page.PageData;
|
|||||||
import xiaozhi.common.service.BaseService;
|
import xiaozhi.common.service.BaseService;
|
||||||
import xiaozhi.modules.knowledge.dto.KnowledgeBaseDTO;
|
import xiaozhi.modules.knowledge.dto.KnowledgeBaseDTO;
|
||||||
import xiaozhi.modules.knowledge.entity.KnowledgeBaseEntity;
|
import xiaozhi.modules.knowledge.entity.KnowledgeBaseEntity;
|
||||||
|
import xiaozhi.modules.model.entity.ModelConfigEntity;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 知识库知识库服务接口
|
* 知识库知识库服务接口
|
||||||
@@ -55,6 +56,14 @@ public interface KnowledgeBaseService extends BaseService<KnowledgeBaseEntity> {
|
|||||||
*/
|
*/
|
||||||
KnowledgeBaseDTO getByDatasetId(String datasetId);
|
KnowledgeBaseDTO getByDatasetId(String datasetId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据知识库ID集合查询知识库
|
||||||
|
*
|
||||||
|
* @param datasetIdList 知识库ID集合
|
||||||
|
* @return 知识库详情
|
||||||
|
*/
|
||||||
|
List<KnowledgeBaseDTO> getByDatasetIdList(List<String> datasetIdList);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据知识库ID删除知识库
|
* 根据知识库ID删除知识库
|
||||||
*
|
*
|
||||||
@@ -83,5 +92,15 @@ public interface KnowledgeBaseService extends BaseService<KnowledgeBaseEntity> {
|
|||||||
*
|
*
|
||||||
* @return RAG模型列表
|
* @return RAG模型列表
|
||||||
*/
|
*/
|
||||||
List<Map<String, Object>> getRAGModels();
|
List<ModelConfigEntity> getRAGModels();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新知识库统计信息 (用于被文件服务回调)
|
||||||
|
*
|
||||||
|
* @param datasetId 知识库ID
|
||||||
|
* @param docDelta 文档数增量
|
||||||
|
* @param chunkDelta 分块数增量
|
||||||
|
* @param tokenDelta Token数增量
|
||||||
|
*/
|
||||||
|
void updateStatistics(String datasetId, Integer docDelta, Long chunkDelta, Long tokenDelta);
|
||||||
}
|
}
|
||||||
+50
-41
@@ -7,6 +7,9 @@ import org.springframework.web.multipart.MultipartFile;
|
|||||||
|
|
||||||
import xiaozhi.common.page.PageData;
|
import xiaozhi.common.page.PageData;
|
||||||
import xiaozhi.modules.knowledge.dto.KnowledgeFilesDTO;
|
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 documentId 文档ID
|
||||||
* @param datasetId 知识库ID
|
* @param datasetId 知识库ID
|
||||||
* @return 文档详情
|
* @return 文档详情 (强类型 InfoVO)
|
||||||
*/
|
*/
|
||||||
KnowledgeFilesDTO getByDocumentId(String documentId, String datasetId);
|
DocumentDTO.InfoVO getByDocumentId(String documentId, String datasetId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 上传文档到知识库
|
* 上传文档到知识库
|
||||||
@@ -48,23 +51,12 @@ public interface KnowledgeFilesService {
|
|||||||
Map<String, Object> parserConfig);
|
Map<String, Object> parserConfig);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据状态分页查询文档列表
|
* 批量删除文档
|
||||||
*
|
*
|
||||||
* @param datasetId 知识库ID
|
* @param datasetId 知识库ID
|
||||||
* @param status 文档解析状态(0-未开始,1-进行中,2-已取消,3-已完成,4-失败)
|
* @param req 删除请求参数 (含文档ID列表)
|
||||||
* @param page 页码
|
|
||||||
* @param limit 每页数量
|
|
||||||
* @return 分页数据
|
|
||||||
*/
|
*/
|
||||||
PageData<KnowledgeFilesDTO> getPageListByStatus(String datasetId, Integer status, Integer page, Integer limit);
|
void deleteDocuments(String datasetId, DocumentDTO.BatchIdReq req);
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据文档ID和知识库ID删除文档
|
|
||||||
*
|
|
||||||
* @param documentId 文档ID
|
|
||||||
* @param datasetId 知识库ID
|
|
||||||
*/
|
|
||||||
void deleteByDocumentId(String documentId, String datasetId);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取RAG配置信息
|
* 获取RAG配置信息
|
||||||
@@ -88,36 +80,53 @@ public interface KnowledgeFilesService {
|
|||||||
*
|
*
|
||||||
* @param datasetId 知识库ID
|
* @param datasetId 知识库ID
|
||||||
* @param documentId 文档ID
|
* @param documentId 文档ID
|
||||||
* @param keywords 关键词过滤
|
* @param req 切片列表请求参数
|
||||||
* @param page 页码
|
|
||||||
* @param pageSize 每页数量
|
|
||||||
* @param chunkId 切片ID
|
|
||||||
* @return 切片列表信息
|
* @return 切片列表信息
|
||||||
*/
|
*/
|
||||||
Map<String, Object> listChunks(String datasetId, String documentId, String keywords,
|
ChunkDTO.ListVO listChunks(String datasetId, String documentId, ChunkDTO.ListReq req);
|
||||||
Integer page, Integer pageSize, String chunkId);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 召回测试 - 从指定数据集或文档中检索相关切片
|
* 召回测试
|
||||||
*
|
*
|
||||||
* @param question 用户查询或查询关键词
|
* @param req 检索测试请求参数
|
||||||
* @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 元数据过滤条件
|
|
||||||
* @return 召回测试结果
|
* @return 召回测试结果
|
||||||
*/
|
*/
|
||||||
Map<String, Object> retrievalTest(String question, List<String> datasetIds, List<String> documentIds,
|
RetrievalDTO.ResultVO retrievalTest(RetrievalDTO.TestReq req);
|
||||||
Integer page, Integer pageSize, Float similarityThreshold,
|
|
||||||
Float vectorSimilarityWeight, Integer topK, String rerankId,
|
/**
|
||||||
Boolean keyword, Boolean highlight, List<String> crossLanguages,
|
* 保存文档影子记录
|
||||||
Map<String, Object> metadataCondition);
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
+372
-720
File diff suppressed because it is too large
Load Diff
+724
-1439
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package xiaozhi.modules.llm.service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LLM服务接口
|
||||||
|
* 支持多种大模型调用
|
||||||
|
*/
|
||||||
|
public interface LLMService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成聊天记录总结
|
||||||
|
*
|
||||||
|
* @param conversation 对话内容
|
||||||
|
* @param promptTemplate 提示词模板
|
||||||
|
* @return 总结结果
|
||||||
|
*/
|
||||||
|
String generateSummary(String conversation, String promptTemplate);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成聊天记录总结(使用默认提示词)
|
||||||
|
*
|
||||||
|
* @param conversation 对话内容
|
||||||
|
* @return 总结结果
|
||||||
|
*/
|
||||||
|
String generateSummary(String conversation);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成聊天记录总结(指定模型ID)
|
||||||
|
*
|
||||||
|
* @param conversation 对话内容
|
||||||
|
* @param modelId 模型ID
|
||||||
|
* @return 总结结果
|
||||||
|
*/
|
||||||
|
String generateSummaryWithModel(String conversation, String modelId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成聊天记录总结(指定模型ID和提示词模板)
|
||||||
|
*
|
||||||
|
* @param conversation 对话内容
|
||||||
|
* @param promptTemplate 提示词模板
|
||||||
|
* @param modelId 模型ID
|
||||||
|
* @return 总结结果
|
||||||
|
*/
|
||||||
|
String generateSummary(String conversation, String promptTemplate, String modelId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成聊天记录总结(包含历史记忆合并)
|
||||||
|
*
|
||||||
|
* @param conversation 对话内容
|
||||||
|
* @param historyMemory 历史记忆
|
||||||
|
* @param promptTemplate 提示词模板
|
||||||
|
* @param modelId 模型ID
|
||||||
|
* @return 总结结果
|
||||||
|
*/
|
||||||
|
String generateSummaryWithHistory(String conversation, String historyMemory, String promptTemplate, String modelId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查服务是否可用
|
||||||
|
*
|
||||||
|
* @return 是否可用
|
||||||
|
*/
|
||||||
|
boolean isAvailable();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查指定模型的服务是否可用
|
||||||
|
*
|
||||||
|
* @param modelId 模型ID
|
||||||
|
* @return 是否可用
|
||||||
|
*/
|
||||||
|
boolean isAvailable(String modelId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成会话标题
|
||||||
|
*
|
||||||
|
* @param conversation 对话内容
|
||||||
|
* @param modelId 模型ID
|
||||||
|
* @return 标题(约15字)
|
||||||
|
*/
|
||||||
|
String generateTitle(String conversation, String modelId);
|
||||||
|
}
|
||||||
+394
@@ -0,0 +1,394 @@
|
|||||||
|
package xiaozhi.modules.llm.service.impl;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
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.stereotype.Service;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
import cn.hutool.json.JSONArray;
|
||||||
|
import cn.hutool.json.JSONObject;
|
||||||
|
import cn.hutool.json.JSONUtil;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import xiaozhi.modules.llm.service.LLMService;
|
||||||
|
import xiaozhi.modules.model.entity.ModelConfigEntity;
|
||||||
|
import xiaozhi.modules.model.service.ModelConfigService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OpenAI风格API的LLM服务实现
|
||||||
|
* 支持阿里云、DeepSeek、ChatGLM等兼容OpenAI API的模型
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class OpenAIStyleLLMServiceImpl implements LLMService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ModelConfigService modelConfigService;
|
||||||
|
|
||||||
|
private final RestTemplate restTemplate = new RestTemplate();
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String generateSummaryWithModel(String conversation, String modelId) {
|
||||||
|
return generateSummary(conversation, null, modelId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String generateSummary(String conversation, String promptTemplate, String modelId) {
|
||||||
|
if (!isAvailable()) {
|
||||||
|
log.warn("LLM服务不可用,无法生成总结");
|
||||||
|
return "LLM服务不可用,无法生成总结";
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 从智控台获取LLM模型配置
|
||||||
|
ModelConfigEntity llmConfig;
|
||||||
|
if (modelId != null && !modelId.trim().isEmpty()) {
|
||||||
|
// 通过具体模型ID获取配置
|
||||||
|
llmConfig = modelConfigService.getModelByIdFromCache(modelId);
|
||||||
|
} else {
|
||||||
|
// 保持向后兼容,使用默认配置
|
||||||
|
llmConfig = getDefaultLLMConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (llmConfig == null || llmConfig.getConfigJson() == null) {
|
||||||
|
log.error("未找到可用的LLM模型配置,modelId: {}", modelId);
|
||||||
|
return "未找到可用的LLM模型配置";
|
||||||
|
}
|
||||||
|
|
||||||
|
JSONObject configJson = llmConfig.getConfigJson();
|
||||||
|
String baseUrl = configJson.getStr("base_url");
|
||||||
|
String model = configJson.getStr("model_name");
|
||||||
|
String apiKey = configJson.getStr("api_key");
|
||||||
|
Double temperature = configJson.getDouble("temperature");
|
||||||
|
Integer maxTokens = configJson.getInt("max_tokens");
|
||||||
|
|
||||||
|
if (StringUtils.isBlank(baseUrl) || StringUtils.isBlank(apiKey)) {
|
||||||
|
log.error("LLM配置不完整,baseUrl或apiKey为空");
|
||||||
|
return "LLM配置不完整,无法生成总结";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建提示词
|
||||||
|
String prompt = (promptTemplate != null ? promptTemplate : DEFAULT_SUMMARY_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", temperature != null ? temperature : 0.7);
|
||||||
|
requestBody.put("max_tokens", maxTokens != null ? maxTokens : 2000);
|
||||||
|
|
||||||
|
// 发送HTTP请求
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||||
|
headers.set("Authorization", "Bearer " + apiKey);
|
||||||
|
|
||||||
|
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(requestBody, headers);
|
||||||
|
|
||||||
|
// 构建完整的API URL
|
||||||
|
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");
|
||||||
|
return messageObj.getStr("content");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.error("LLM API调用失败,状态码:{},响应:{}", response.getStatusCode(), response.getBody());
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("调用LLM服务生成总结时发生异常,modelId: {}", modelId, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return "生成总结失败,请稍后重试";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String generateSummary(String conversation, String promptTemplate) {
|
||||||
|
return generateSummary(conversation, promptTemplate, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String generateSummaryWithHistory(String conversation, String historyMemory, String promptTemplate,
|
||||||
|
String modelId) {
|
||||||
|
if (!isAvailable()) {
|
||||||
|
log.warn("LLM服务不可用,无法生成总结");
|
||||||
|
return "LLM服务不可用,无法生成总结";
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 从智控台获取LLM模型配置
|
||||||
|
ModelConfigEntity llmConfig;
|
||||||
|
if (modelId != null && !modelId.trim().isEmpty()) {
|
||||||
|
// 通过具体模型ID获取配置
|
||||||
|
llmConfig = modelConfigService.getModelByIdFromCache(modelId);
|
||||||
|
} else {
|
||||||
|
// 保持向后兼容,使用默认配置
|
||||||
|
llmConfig = getDefaultLLMConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (llmConfig == null || llmConfig.getConfigJson() == null) {
|
||||||
|
log.error("未找到可用的LLM模型配置,modelId: {}", modelId);
|
||||||
|
return "未找到可用的LLM模型配置";
|
||||||
|
}
|
||||||
|
|
||||||
|
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 "LLM配置不完整,无法生成总结";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建提示词,包含历史记忆
|
||||||
|
String prompt = (promptTemplate != null ? promptTemplate : DEFAULT_SUMMARY_PROMPT)
|
||||||
|
.replace("{history_memory}", historyMemory != null ? historyMemory : "无历史记忆")
|
||||||
|
.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.2);
|
||||||
|
requestBody.put("max_tokens", 2000);
|
||||||
|
|
||||||
|
// 发送HTTP请求
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||||
|
headers.set("Authorization", "Bearer " + apiKey);
|
||||||
|
|
||||||
|
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(requestBody, headers);
|
||||||
|
|
||||||
|
// 构建完整的API URL
|
||||||
|
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");
|
||||||
|
return messageObj.getStr("content");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.error("LLM API调用失败,状态码:{},响应:{}", response.getStatusCode(), response.getBody());
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("调用LLM服务生成总结时发生异常,modelId: {}", modelId, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return "生成总结失败,请稍后重试";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isAvailable() {
|
||||||
|
try {
|
||||||
|
ModelConfigEntity defaultLLMConfig = getDefaultLLMConfig();
|
||||||
|
if (defaultLLMConfig == null || defaultLLMConfig.getConfigJson() == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
JSONObject configJson = defaultLLMConfig.getConfigJson();
|
||||||
|
String baseUrl = configJson.getStr("base_url");
|
||||||
|
String apiKey = configJson.getStr("api_key");
|
||||||
|
|
||||||
|
return baseUrl != null && !baseUrl.trim().isEmpty() &&
|
||||||
|
apiKey != null && !apiKey.trim().isEmpty();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("检查LLM服务可用性时发生异常:", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isAvailable(String modelId) {
|
||||||
|
try {
|
||||||
|
if (modelId == null || modelId.trim().isEmpty()) {
|
||||||
|
return isAvailable();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通过具体模型ID获取配置
|
||||||
|
ModelConfigEntity modelConfig = modelConfigService.getModelByIdFromCache(modelId);
|
||||||
|
if (modelConfig == null || modelConfig.getConfigJson() == null) {
|
||||||
|
log.warn("未找到指定的LLM模型配置,modelId: {}", modelId);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
JSONObject configJson = modelConfig.getConfigJson();
|
||||||
|
String baseUrl = configJson.getStr("base_url");
|
||||||
|
String apiKey = configJson.getStr("api_key");
|
||||||
|
|
||||||
|
return baseUrl != null && !baseUrl.trim().isEmpty() &&
|
||||||
|
apiKey != null && !apiKey.trim().isEmpty();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("检查LLM服务可用性时发生异常,modelId: {}", modelId, e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从智控台获取默认的LLM模型配置
|
||||||
|
*/
|
||||||
|
private ModelConfigEntity getDefaultLLMConfig() {
|
||||||
|
try {
|
||||||
|
// 获取所有启用的LLM模型配置
|
||||||
|
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);
|
||||||
|
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);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user