mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-02 20:14:36 +00:00
Compare commits
660 Commits
v4.3.7b1
...
fix/plugin
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
779cf9899f | ||
|
|
b251fc4b89 | ||
|
|
075c85e2bc | ||
|
|
62b63ca2ca | ||
|
|
3680a80248 | ||
|
|
6713b57d01 | ||
|
|
ea13ef87f2 | ||
|
|
59bd581e88 | ||
|
|
cba83a62e8 | ||
|
|
f412127fb0 | ||
|
|
5273bbb23f | ||
|
|
0ceab3f6a5 | ||
|
|
aedc097188 | ||
|
|
18b27dd9ef | ||
|
|
3f50a56623 | ||
|
|
1fcdbd472f | ||
|
|
547006cb4a | ||
|
|
92bf9a7ea5 | ||
|
|
832efb4069 | ||
|
|
8f1847d480 | ||
|
|
fe619e415f | ||
|
|
0154ea6cd3 | ||
|
|
8db55267d8 | ||
|
|
b9662250a6 | ||
|
|
d9378c3a88 | ||
|
|
86a4d1bf0b | ||
|
|
ce6e79db8e | ||
|
|
d53e2cb9a0 | ||
|
|
c1168745b7 | ||
|
|
69b87a0d8a | ||
|
|
6637b153f1 | ||
|
|
e768fc6116 | ||
|
|
2442d3bf52 | ||
|
|
42d78817f4 | ||
|
|
4b9f25a05d | ||
|
|
d1f0e07cc0 | ||
|
|
78e55509ae | ||
|
|
2c28635a39 | ||
|
|
5f3cecfbe2 | ||
|
|
12df9d6ee9 | ||
|
|
195f6efeff | ||
|
|
564d829e25 | ||
|
|
58c1916712 | ||
|
|
a8fba46040 | ||
|
|
3115d6f6dd | ||
|
|
323481d69b | ||
|
|
5a5c4295b1 | ||
|
|
88111d87ac | ||
|
|
4e5a6ee79a | ||
|
|
05c684d757 | ||
|
|
2838020580 | ||
|
|
9b34ae2db4 | ||
|
|
f8010a20eb | ||
|
|
917edb3413 | ||
|
|
10425ede34 | ||
|
|
e4b40a8fa0 | ||
|
|
0b8ab4b54b | ||
|
|
49239e0e08 | ||
|
|
aec2a30445 | ||
|
|
c8915ca964 | ||
|
|
a715eddd06 | ||
|
|
2f9c235b41 | ||
|
|
cc4d8838eb | ||
|
|
fa0a77f09f | ||
|
|
fd6a7b73d4 | ||
|
|
bf0848d60b | ||
|
|
e06fac2bb7 | ||
|
|
bec61427a0 | ||
|
|
5fae7b2eb0 | ||
|
|
2eebdfe16a | ||
|
|
9cd3544d59 | ||
|
|
de4d14fee3 | ||
|
|
f29c568381 | ||
|
|
af3f557055 | ||
|
|
b894842736 | ||
|
|
e190029e1f | ||
|
|
e4940a8050 | ||
|
|
617c95ebc4 | ||
|
|
1cdd428bcc | ||
|
|
71ac719aee | ||
|
|
4621e6cc9f | ||
|
|
66087f83e1 | ||
|
|
25f9330491 | ||
|
|
14b1e0d33b | ||
|
|
83ccb33fd3 | ||
|
|
05bcf543ba | ||
|
|
7cd063bb5d | ||
|
|
8f1317b39e | ||
|
|
77a0de5ef0 | ||
|
|
875227a2fe | ||
|
|
2317392ee5 | ||
|
|
c7efa4dd7f | ||
|
|
e701daa8e0 | ||
|
|
1ae99199b2 | ||
|
|
7c067a1cb3 | ||
|
|
478bc62576 | ||
|
|
a740eb8ee9 | ||
|
|
f8aedd02b3 | ||
|
|
ea638cab80 | ||
|
|
7129dd536e | ||
|
|
1b1cc7769b | ||
|
|
44b8354dfd | ||
|
|
55ec9d11ae | ||
|
|
5b3d3801b5 | ||
|
|
9f1ea75d09 | ||
|
|
6e37aae636 | ||
|
|
921d12f596 | ||
|
|
6bf6deaefd | ||
|
|
1201949f2c | ||
|
|
1c419e3591 | ||
|
|
b0a9be77b0 | ||
|
|
e02ade5a30 | ||
|
|
1a51ba8e7e | ||
|
|
e7b22d6ebf | ||
|
|
dddfa8ac79 | ||
|
|
99e2976826 | ||
|
|
71e44f0e54 | ||
|
|
4c904c2375 | ||
|
|
498d030da9 | ||
|
|
c111bf1714 | ||
|
|
6570f276d2 | ||
|
|
42e1e038bd | ||
|
|
d0e54a45c7 | ||
|
|
23fa47b07e | ||
|
|
4902c1d3b2 | ||
|
|
a6f96e5209 | ||
|
|
37c41bcfe4 | ||
|
|
9e223949a7 | ||
|
|
267bd72c63 | ||
|
|
af0d00e5e9 | ||
|
|
244e16c491 | ||
|
|
cad259fe39 | ||
|
|
bc3199bf29 | ||
|
|
127dc455c3 | ||
|
|
e8dc6fde53 | ||
|
|
4a97895dea | ||
|
|
3c0495fc51 | ||
|
|
dfd25deb68 | ||
|
|
f4db53b759 | ||
|
|
9f90341dcb | ||
|
|
67b726afb2 | ||
|
|
01852b81d4 | ||
|
|
4d6f109788 | ||
|
|
e1e5e7aedf | ||
|
|
cd53abc440 | ||
|
|
16a15a122a | ||
|
|
6fa653f232 | ||
|
|
c13971d7d6 | ||
|
|
9c659ce8fa | ||
|
|
c9fc64360f | ||
|
|
88a04fdbe8 | ||
|
|
bbe019f0c6 | ||
|
|
865f6ee81b | ||
|
|
bd5ec59b7c | ||
|
|
9c0cc1003d | ||
|
|
ea07d8ad00 | ||
|
|
3ac3fad4bc | ||
|
|
254a13bba3 | ||
|
|
4355f0fa78 | ||
|
|
031737f05d | ||
|
|
9e366fc536 | ||
|
|
8bd6442965 | ||
|
|
1a1eadb282 | ||
|
|
eed72b1c12 | ||
|
|
351350ea03 | ||
|
|
bc3d6ba92f | ||
|
|
345e4baf2a | ||
|
|
6c64dc057f | ||
|
|
eec0a9c9d9 | ||
|
|
6896a55485 | ||
|
|
4b0fad233e | ||
|
|
52eb991a70 | ||
|
|
10c716be0c | ||
|
|
6e77351eda | ||
|
|
20f5ebd9b8 | ||
|
|
d2c75329cf | ||
|
|
7e2fe082f0 | ||
|
|
d451b059fd | ||
|
|
93c52fcd4c | ||
|
|
f1608682e6 | ||
|
|
077e631c13 | ||
|
|
d7df1f05d1 | ||
|
|
8b8cfb76de | ||
|
|
79311ccde3 | ||
|
|
def798bf1f | ||
|
|
5290834b8b | ||
|
|
89064a9d5b | ||
|
|
8c2aef3734 | ||
|
|
3fb9e542b6 | ||
|
|
01844d8687 | ||
|
|
2655425fbe | ||
|
|
bd15b630b0 | ||
|
|
fe5ce68436 | ||
|
|
0541b05966 | ||
|
|
13cb0aa9be | ||
|
|
a048369b38 | ||
|
|
9ae0c263dc | ||
|
|
a4e66f6459 | ||
|
|
2a74a8d6ae | ||
|
|
d31f25c8df | ||
|
|
11c05ea8db | ||
|
|
2b8bd1cc71 | ||
|
|
9148e02679 | ||
|
|
fd15284d91 | ||
|
|
8c7a0ec027 | ||
|
|
a1cef5c9bf | ||
|
|
90438cec36 | ||
|
|
95dd19f4d7 | ||
|
|
c64eb58cf8 | ||
|
|
fbd3d7ae3a | ||
|
|
40c7b0f731 | ||
|
|
cadcf10047 | ||
|
|
3e8f47fd97 | ||
|
|
b11ae55c6e | ||
|
|
2d63d528c6 | ||
|
|
10f253015d | ||
|
|
b34ebf85a6 | ||
|
|
06d3298cde | ||
|
|
614621ab7b | ||
|
|
8600d0a8e7 | ||
|
|
b83e6a53be | ||
|
|
88132dff8a | ||
|
|
2dc5999583 | ||
|
|
73461814c9 | ||
|
|
210e5e50d3 | ||
|
|
4fd488b97a | ||
|
|
422a34ead4 | ||
|
|
02a1036d63 | ||
|
|
2d837c9cb4 | ||
|
|
2ded774747 | ||
|
|
d9a630b8c1 | ||
|
|
b8df0dbd7f | ||
|
|
298437f352 | ||
|
|
94d72c378c | ||
|
|
f09ba6a0e3 | ||
|
|
1eda076b93 | ||
|
|
d6c10763a8 | ||
|
|
9df50d2cab | ||
|
|
6c6b510a0a | ||
|
|
063dc6fe97 | ||
|
|
42caae1bcf | ||
|
|
aa09a27a63 | ||
|
|
96e32a10e2 | ||
|
|
9a9f0eaa7d | ||
|
|
f5dea3c64c | ||
|
|
e213046302 | ||
|
|
41d31d77d8 | ||
|
|
6fb7fc80cc | ||
|
|
7bee5ff2f8 | ||
|
|
afe82ebdfd | ||
|
|
65c10ea54b | ||
|
|
ff0023c6c2 | ||
|
|
0e17d869ab | ||
|
|
7ec41bb91a | ||
|
|
da164c214e | ||
|
|
32a5de9bbb | ||
|
|
1b12b1fc35 | ||
|
|
caa1ed9d6a | ||
|
|
05f40e72ff | ||
|
|
27fb22d7be | ||
|
|
ca504384d2 | ||
|
|
b7e1e43fbd | ||
|
|
deabb19389 | ||
|
|
809035daac | ||
|
|
1eac87b89f | ||
|
|
70a2d137f0 | ||
|
|
c72b785c1f | ||
|
|
8588199640 | ||
|
|
2e42cd2faf | ||
|
|
7b3555af45 | ||
|
|
e12a77ca05 | ||
|
|
9ce3ad8300 | ||
|
|
1f60d9c3d6 | ||
|
|
d855d29c15 | ||
|
|
18083e9160 | ||
|
|
7f9e8ecac1 | ||
|
|
995c852f0a | ||
|
|
682962cc47 | ||
|
|
24e90a7f9b | ||
|
|
6a5a7182db | ||
|
|
c581c8e809 | ||
|
|
ffd2423920 | ||
|
|
c388339bd5 | ||
|
|
28492a62bb | ||
|
|
6a687ebeeb | ||
|
|
29dfae1518 | ||
|
|
791877d391 | ||
|
|
8fd0c3cc18 | ||
|
|
10dd8c86d0 | ||
|
|
c2574bdd3a | ||
|
|
d2d7892325 | ||
|
|
6d858475d7 | ||
|
|
59d55b382d | ||
|
|
8c17e55913 | ||
|
|
af509fe61f | ||
|
|
87e2a2099a | ||
|
|
3f22f62332 | ||
|
|
d1ee5f931a | ||
|
|
35506dd2bb | ||
|
|
2f06321ebf | ||
|
|
023281ae56 | ||
|
|
50dff55217 | ||
|
|
3204292360 | ||
|
|
e0d72969e3 | ||
|
|
a65b7ad413 | ||
|
|
45df44e01b | ||
|
|
d8addb105a | ||
|
|
f17ccad665 | ||
|
|
120ceb0b55 | ||
|
|
8a6f80a181 | ||
|
|
b19e468668 | ||
|
|
aeac79e1b3 | ||
|
|
b89a240250 | ||
|
|
13f42857f5 | ||
|
|
61f3f31edc | ||
|
|
3663d9dc10 | ||
|
|
89ec86c530 | ||
|
|
d9ba2a17ff | ||
|
|
c4ea6188f9 | ||
|
|
5d9f6ec763 | ||
|
|
b73847f1a6 | ||
|
|
d6e1e79f07 | ||
|
|
525008b8b2 | ||
|
|
bbf77bac4c | ||
|
|
f4ae829f59 | ||
|
|
3af8c13fab | ||
|
|
a8f7924867 | ||
|
|
77047e87d6 | ||
|
|
24d865bcd3 | ||
|
|
81ec7c201c | ||
|
|
fc6e414be4 | ||
|
|
e60cb6ad0e | ||
|
|
c90f2d6a12 | ||
|
|
fe8a738cd7 | ||
|
|
604cc53973 | ||
|
|
195b694ecc | ||
|
|
ee2d4e3ab9 | ||
|
|
d21f23beee | ||
|
|
558587883b | ||
|
|
2e6a1daf4f | ||
|
|
1fc5e75f93 | ||
|
|
a332206ba3 | ||
|
|
8e620dc635 | ||
|
|
c9a21ebace | ||
|
|
a05cdcac50 | ||
|
|
ecfb2bfb34 | ||
|
|
e17dba0a98 | ||
|
|
6b138943ce | ||
|
|
eb0e6aff68 | ||
|
|
4d0095626a | ||
|
|
aa0a501ade | ||
|
|
68ef7bd2c4 | ||
|
|
61dc5de085 | ||
|
|
63bdd71e22 | ||
|
|
9ea5b50802 | ||
|
|
1cd586634d | ||
|
|
45bedbe70e | ||
|
|
f7f1dde7b5 | ||
|
|
ba06555078 | ||
|
|
840fa39979 | ||
|
|
b295416e6c | ||
|
|
914f77ff37 | ||
|
|
b0b7b914d8 | ||
|
|
12713aad45 | ||
|
|
02e12cc1e4 | ||
|
|
61f08f3218 | ||
|
|
75c2a063cc | ||
|
|
b4773c4e48 | ||
|
|
fb73da8735 | ||
|
|
679e549b1d | ||
|
|
898144e9f4 | ||
|
|
b99c5561fc | ||
|
|
b2f4b91979 | ||
|
|
4528000fc4 | ||
|
|
96e40eaf25 | ||
|
|
197258ae91 | ||
|
|
19f417174c | ||
|
|
9c82eeddeb | ||
|
|
f11e01b549 | ||
|
|
863b26c3fa | ||
|
|
b788858f9e | ||
|
|
de8a7df6c2 | ||
|
|
ba5b481617 | ||
|
|
07ad846e96 | ||
|
|
30945aafdd | ||
|
|
24c15b4479 | ||
|
|
1d4c5bbdf1 | ||
|
|
57fcec011d | ||
|
|
455e3db28d | ||
|
|
8caab43b00 | ||
|
|
7479545339 | ||
|
|
10ee30695a | ||
|
|
a9a262eaae | ||
|
|
a8594b76cd | ||
|
|
11ee0fef5d | ||
|
|
9a9ba34717 | ||
|
|
312e47bf46 | ||
|
|
628865fd06 | ||
|
|
806a03cd53 | ||
|
|
24bd90fcf6 | ||
|
|
d2765577c8 | ||
|
|
60ca688bcb | ||
|
|
76d8eea41d | ||
|
|
635c3a04d8 | ||
|
|
dde97abe38 | ||
|
|
90a22d894d | ||
|
|
88ef9cd6ae | ||
|
|
e3595b5c57 | ||
|
|
ce82f87e43 | ||
|
|
854b291c5a | ||
|
|
9780fd059c | ||
|
|
adc65f66eb | ||
|
|
ae772074a1 | ||
|
|
16c1e9edd1 | ||
|
|
3ab9ffb7b7 | ||
|
|
82e2123fe7 | ||
|
|
7a65f3d2f4 | ||
|
|
b5b5d499e5 | ||
|
|
173f9e9c30 | ||
|
|
a610c72067 | ||
|
|
d210a49fae | ||
|
|
b015c248ea | ||
|
|
4a559ea770 | ||
|
|
e306751863 | ||
|
|
2f51f5f33e | ||
|
|
74a2a61fc1 | ||
|
|
b6c0345b3e | ||
|
|
6421a6f5cb | ||
|
|
daf56e5dc2 | ||
|
|
cb7c9af25c | ||
|
|
45e61befac | ||
|
|
ea50ba10e6 | ||
|
|
5c4a727e74 | ||
|
|
867f05c4ad | ||
|
|
b06b32306f | ||
|
|
dbfcb70f8d | ||
|
|
e64d56c4ac | ||
|
|
8f0da7943c | ||
|
|
e62ff7e520 | ||
|
|
86e951916e | ||
|
|
6bf08466de | ||
|
|
5e36dd480d | ||
|
|
0e2cd8c018 | ||
|
|
b4f92eba38 | ||
|
|
905e48c8ed | ||
|
|
10ec79312e | ||
|
|
24f779ff95 | ||
|
|
08c0677de9 | ||
|
|
cc5d32cf8a | ||
|
|
01a5133396 | ||
|
|
0aa5188b29 | ||
|
|
e49a161d0a | ||
|
|
0ddc3d60e7 | ||
|
|
51794176af | ||
|
|
b634aa48dc | ||
|
|
16ae8ac546 | ||
|
|
1ecb0735cb | ||
|
|
c368d828c9 | ||
|
|
019ae9c216 | ||
|
|
580d9441a4 | ||
|
|
b5d192425e | ||
|
|
58312deb8c | ||
|
|
cf646752c5 | ||
|
|
b53750fde4 | ||
|
|
52e6135ae8 | ||
|
|
f4eb59e2ad | ||
|
|
34d84590e2 | ||
|
|
d09b823c49 | ||
|
|
348620ac0a | ||
|
|
a8481e43f0 | ||
|
|
3c04eeaff9 | ||
|
|
87131cf03b | ||
|
|
7d51293594 | ||
|
|
b78b0e50bb | ||
|
|
6b4c1a7dee | ||
|
|
2e1f16d7b4 | ||
|
|
50c33c5213 | ||
|
|
ace6d62d76 | ||
|
|
b7c4c21796 | ||
|
|
66602da9cb | ||
|
|
31b483509c | ||
|
|
ba7cf69c9d | ||
|
|
37296be67e | ||
|
|
6c03a1dd31 | ||
|
|
b75ec9e989 | ||
|
|
5c8523e4ef | ||
|
|
9802a42a9e | ||
|
|
99e3abec72 | ||
|
|
fc2efdf994 | ||
|
|
6ed672d996 | ||
|
|
2bf593fa6b | ||
|
|
3182214663 | ||
|
|
20614b20b7 | ||
|
|
da323817f7 | ||
|
|
763c1a885c | ||
|
|
dbc09f46f4 | ||
|
|
cf43f09aff | ||
|
|
c3c51b0fbf | ||
|
|
8a42daa63f | ||
|
|
d91d98c9d4 | ||
|
|
2e82f2b2d1 | ||
|
|
f459c7017a | ||
|
|
c27ccb8475 | ||
|
|
abb2f7ae05 | ||
|
|
80606ed32c | ||
|
|
bc7c5fa864 | ||
|
|
ed0ea68037 | ||
|
|
6ac4dbc011 | ||
|
|
e642ffa5b3 | ||
|
|
6a24c951e0 | ||
|
|
58369480e2 | ||
|
|
43553e2c7d | ||
|
|
268ac8855a | ||
|
|
0f10cc62ec | ||
|
|
99f649c6b7 | ||
|
|
f25ac78538 | ||
|
|
cef24d8c4b | ||
|
|
7a10dfdac1 | ||
|
|
02892e57bb | ||
|
|
524c56a12b | ||
|
|
0e0d7cc7b8 | ||
|
|
1f877e2b8e | ||
|
|
8cd50fbdb4 | ||
|
|
42421d171e | ||
|
|
32215e9a3f | ||
|
|
dd1c7ffc39 | ||
|
|
b59bf62da5 | ||
|
|
f4c32f7b30 | ||
|
|
8844a5304d | ||
|
|
922ddd47f4 | ||
|
|
8c8702c6c9 | ||
|
|
70147fcf5e | ||
|
|
b3ee16e876 | ||
|
|
8d7976190d | ||
|
|
3edae3e678 | ||
|
|
dd2254203c | ||
|
|
f8658e2d77 | ||
|
|
021c3bbb94 | ||
|
|
0a64a96f65 | ||
|
|
48576dc46d | ||
|
|
12de0343b4 | ||
|
|
fcd34a9ff3 | ||
|
|
0dcf904d81 | ||
|
|
4fe92d8ece | ||
|
|
c893ffc177 | ||
|
|
a076ce5756 | ||
|
|
af82227dff | ||
|
|
8f2b177145 | ||
|
|
9a997fbcb0 | ||
|
|
17070471f7 | ||
|
|
cb48221ed3 | ||
|
|
68eb0290e0 | ||
|
|
61bc6a1dc2 | ||
|
|
4a84bf2355 | ||
|
|
2c2a89d9db | ||
|
|
c91e2f0efe | ||
|
|
411d082d2a | ||
|
|
d4e08a1765 | ||
|
|
b529d07479 | ||
|
|
d44df75e5c | ||
|
|
b74e07b608 | ||
|
|
4a868afecd | ||
|
|
1cb9560663 | ||
|
|
8f878673ae | ||
|
|
74a5e37892 | ||
|
|
76a69ecc7e | ||
|
|
f06e3d3efa | ||
|
|
973e7bae42 | ||
|
|
94aa175c1a | ||
|
|
777b766fff | ||
|
|
1adaa93034 | ||
|
|
9853eccd89 | ||
|
|
7699ba3cae | ||
|
|
9ac8b1a6fd | ||
|
|
f476c4724d | ||
|
|
3d12632c9f | ||
|
|
350e59fa6b | ||
|
|
b3d5b3fc8f | ||
|
|
4a02c531b2 | ||
|
|
2dd2abedde | ||
|
|
0d59c04151 | ||
|
|
08e0ede655 | ||
|
|
bcf89ca434 | ||
|
|
5e2f677d0b | ||
|
|
4df372052d | ||
|
|
2c5a0a00ba | ||
|
|
f3295b0fdd | ||
|
|
431d515c26 | ||
|
|
d9e6198992 | ||
|
|
3951cbf266 | ||
|
|
c47c4994ae | ||
|
|
a6072c2abb | ||
|
|
360422f25e | ||
|
|
f135c946bd | ||
|
|
750cc24900 | ||
|
|
46062bf4b9 | ||
|
|
869b2176a7 | ||
|
|
7138c101e3 | ||
|
|
04e26225cd | ||
|
|
f9f2de570f | ||
|
|
1dd598c7be | ||
|
|
c0f04e4f20 | ||
|
|
d3279b9823 | ||
|
|
2ad1f97e12 | ||
|
|
1046f3c2aa | ||
|
|
1afecf01e4 | ||
|
|
3ee7736361 | ||
|
|
0666778fea | ||
|
|
8df90558ab | ||
|
|
c1c03f11b4 | ||
|
|
da9afcd0ad | ||
|
|
bc1fbfa190 | ||
|
|
f3199dda20 | ||
|
|
4d0a28a1a7 | ||
|
|
76831579ad | ||
|
|
c2d752f9e9 | ||
|
|
4c0917556f | ||
|
|
e17b0cf5c5 | ||
|
|
f2647316a5 | ||
|
|
78cc157657 | ||
|
|
f576f990de | ||
|
|
254feb6a3a | ||
|
|
4c5139e9ff | ||
|
|
a055e37d3a | ||
|
|
bef5d6627b | ||
|
|
69767ebdb4 | ||
|
|
53ecd0933e | ||
|
|
d32f783392 | ||
|
|
4d3610cdf7 | ||
|
|
166eebabff | ||
|
|
9f2f1cd577 | ||
|
|
d86b884cab | ||
|
|
8345edd9f7 | ||
|
|
e3821b3f09 | ||
|
|
72ca62eae4 | ||
|
|
075091ed06 | ||
|
|
d0a3dee083 | ||
|
|
6ba9b6973d | ||
|
|
345eccf04c | ||
|
|
127a38b15c | ||
|
|
760db38c11 | ||
|
|
e4729337c8 | ||
|
|
7be226d3fa | ||
|
|
68372a4b7a | ||
|
|
d65f862c36 | ||
|
|
5fa75330cf | ||
|
|
547e3d098e | ||
|
|
0f39a31648 | ||
|
|
f1ddddfe00 | ||
|
|
4e61302156 | ||
|
|
9e3cf418ba | ||
|
|
3e29ec7892 | ||
|
|
f452742cd2 | ||
|
|
b560432b0b | ||
|
|
99e5478ced | ||
|
|
a3552893aa | ||
|
|
0b527868bc | ||
|
|
0f35458cf7 | ||
|
|
70ad92ca16 | ||
|
|
c0d56aa905 |
8
.dockerignore
Normal file
8
.dockerignore
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
.github
|
||||||
|
.venv
|
||||||
|
.vscode
|
||||||
|
.data
|
||||||
|
.temp
|
||||||
|
web/.next
|
||||||
|
web/node_modules
|
||||||
|
web/.env
|
||||||
4
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
4
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@@ -1,5 +1,5 @@
|
|||||||
name: 漏洞反馈
|
name: 漏洞反馈
|
||||||
description: 【供中文用户】报错或漏洞请使用这个模板创建,不使用此模板创建的异常、漏洞相关issue将被直接关闭。由于自己操作不当/不甚了解所用技术栈引起的网络连接问题恕无法解决,请勿提 issue。容器间网络连接问题,参考文档 https://docs.langbot.app/zh/workshop/network-details.html
|
description: 【供中文用户】报错或漏洞请使用这个模板创建,不使用此模板创建的异常、漏洞相关issue将被直接关闭。由于自己操作不当/不甚了解所用技术栈引起的网络连接问题恕无法解决,请勿提 issue。容器间网络连接问题,参考文档 https://link.langbot.app/zh/docs/network
|
||||||
title: "[Bug]: "
|
title: "[Bug]: "
|
||||||
labels: ["bug?"]
|
labels: ["bug?"]
|
||||||
body:
|
body:
|
||||||
@@ -19,7 +19,7 @@ body:
|
|||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: 复现步骤
|
label: 复现步骤
|
||||||
description: 提供越多信息,我们会越快解决问题,建议多提供配置截图;**如果你不认真填写(只一两句话概括),我们会很生气并且立即关闭 issue 或两年后才回复你**
|
description: 提供越多信息,我们会越快解决问题,建议多提供配置截图;**如果涉及 Dify、n8n、Langflow 等外部平台,请提供应用的导出文件(如 Dify 应用的 DSL),我们将更快回复您。**
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
- type: textarea
|
- type: textarea
|
||||||
|
|||||||
4
.github/ISSUE_TEMPLATE/bug-report_en.yml
vendored
4
.github/ISSUE_TEMPLATE/bug-report_en.yml
vendored
@@ -1,5 +1,5 @@
|
|||||||
name: Bug report
|
name: Bug report
|
||||||
description: Report bugs or vulnerabilities using this template. For container network connection issues, refer to the documentation https://docs.langbot.app/en/workshop/network-details.html
|
description: Report bugs or vulnerabilities using this template. For container network connection issues, refer to the documentation https://link.langbot.app/en/docs/network
|
||||||
title: "[Bug]: "
|
title: "[Bug]: "
|
||||||
labels: ["bug?"]
|
labels: ["bug?"]
|
||||||
body:
|
body:
|
||||||
@@ -19,7 +19,7 @@ body:
|
|||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: Reproduction steps
|
label: Reproduction steps
|
||||||
description: How to reproduce this problem, the more detailed the better; the more information you provide, the faster we will solve the problem. 【注意】请务必认真填写此部分,若不提供完整信息(如只有一两句话的概括),我们将不会回复!
|
description: How to reproduce this problem, the more detailed the better; the more information you provide, the faster we will solve the problem.
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
- type: textarea
|
- type: textarea
|
||||||
|
|||||||
11
.github/pull_request_template.md
vendored
11
.github/pull_request_template.md
vendored
@@ -2,6 +2,17 @@
|
|||||||
|
|
||||||
> 请在此部分填写你实现/解决/优化的内容:
|
> 请在此部分填写你实现/解决/优化的内容:
|
||||||
> Summary of what you implemented/solved/optimized:
|
> Summary of what you implemented/solved/optimized:
|
||||||
|
>
|
||||||
|
|
||||||
|
### 更改前后对比截图 / Screenshots
|
||||||
|
|
||||||
|
> 请在此部分粘贴更改前后对比截图(可以是界面截图、控制台输出、对话截图等):
|
||||||
|
> Please paste the screenshots of changes before and after here (can be interface screenshots, console output, conversation screenshots, etc.):
|
||||||
|
>
|
||||||
|
> 修改前 / Before:
|
||||||
|
>
|
||||||
|
> 修改后 / After:
|
||||||
|
>
|
||||||
|
|
||||||
## 检查清单 / Checklist
|
## 检查清单 / Checklist
|
||||||
|
|
||||||
|
|||||||
8
.github/workflows/build-docker-image.yml
vendored
8
.github/workflows/build-docker-image.yml
vendored
@@ -1,7 +1,5 @@
|
|||||||
name: Build Docker Image
|
name: Build Docker Image
|
||||||
on:
|
on:
|
||||||
#防止fork乱用action设置只能手动触发构建
|
|
||||||
workflow_dispatch:
|
|
||||||
## 发布release的时候会自动构建
|
## 发布release的时候会自动构建
|
||||||
release:
|
release:
|
||||||
types: [published]
|
types: [published]
|
||||||
@@ -41,5 +39,9 @@ jobs:
|
|||||||
run: docker login --username=${{ secrets.DOCKER_USERNAME }} --password ${{ secrets.DOCKER_PASSWORD }}
|
run: docker login --username=${{ secrets.DOCKER_USERNAME }} --password ${{ secrets.DOCKER_PASSWORD }}
|
||||||
- name: Create Buildx
|
- name: Create Buildx
|
||||||
run: docker buildx create --name mybuilder --use
|
run: docker buildx create --name mybuilder --use
|
||||||
- name: Build # image name: rockchin/langbot:<VERSION>
|
- name: Build for Release # only relase, exlude pre-release
|
||||||
|
if: ${{ github.event.release.prerelease == false }}
|
||||||
run: docker buildx build --platform linux/arm64,linux/amd64 -t rockchin/langbot:${{ steps.check_version.outputs.version }} -t rockchin/langbot:latest . --push
|
run: docker buildx build --platform linux/arm64,linux/amd64 -t rockchin/langbot:${{ steps.check_version.outputs.version }} -t rockchin/langbot:latest . --push
|
||||||
|
- name: Build for Pre-release # no update for latest tag
|
||||||
|
if: ${{ github.event.release.prerelease == true }}
|
||||||
|
run: docker buildx build --platform linux/arm64,linux/amd64 -t rockchin/langbot:${{ steps.check_version.outputs.version }} . --push
|
||||||
@@ -43,10 +43,10 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
cd /tmp/langbot_build_web/web
|
cd /tmp/langbot_build_web/web
|
||||||
npm install
|
npm install
|
||||||
npm run build
|
npx vite build
|
||||||
- name: Package Output
|
- name: Package Output
|
||||||
run: |
|
run: |
|
||||||
cp -r /tmp/langbot_build_web/web/out ./web
|
cp -r /tmp/langbot_build_web/web/dist ./web
|
||||||
- name: Upload Artifact
|
- name: Upload Artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
|
|||||||
25
.github/workflows/check-i18n.yml
vendored
Normal file
25
.github/workflows/check-i18n.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
name: Check i18n Keys
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-i18n:
|
||||||
|
name: Check i18n Key Consistency
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
|
||||||
|
- name: Check i18n keys against en-US reference
|
||||||
|
run: node web/scripts/check-i18n.mjs
|
||||||
60
.github/workflows/lint.yml
vendored
Normal file
60
.github/workflows/lint.yml
vendored
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
name: Lint
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- master
|
||||||
|
- dev
|
||||||
|
pull_request:
|
||||||
|
types: [opened, synchronize, reopened, ready_for_review]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
ruff:
|
||||||
|
name: Ruff Lint & Format
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.12'
|
||||||
|
|
||||||
|
- name: Install uv
|
||||||
|
uses: astral-sh/setup-uv@v4
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: uv sync --dev
|
||||||
|
|
||||||
|
- name: Run ruff check
|
||||||
|
run: uv run ruff check src
|
||||||
|
|
||||||
|
- name: Run ruff format
|
||||||
|
run: uv run ruff format src --check
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
name: Frontend Lint
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '25'
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: 9
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
working-directory: web
|
||||||
|
run: pnpm install
|
||||||
|
|
||||||
|
- name: Run lint
|
||||||
|
working-directory: web
|
||||||
|
run: pnpm lint
|
||||||
46
.github/workflows/publish-to-pypi.yml
vendored
Normal file
46
.github/workflows/publish-to-pypi.yml
vendored
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
name: Build and Publish to PyPI
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-publish:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
id-token: write # Required for trusted publishing to PyPI
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '22'
|
||||||
|
|
||||||
|
- name: Build frontend
|
||||||
|
run: |
|
||||||
|
cd web
|
||||||
|
npm install -g pnpm
|
||||||
|
pnpm install
|
||||||
|
pnpm build
|
||||||
|
mkdir -p ../src/langbot/web/dist
|
||||||
|
cp -r dist ../src/langbot/web/
|
||||||
|
|
||||||
|
- name: Install the latest version of uv
|
||||||
|
uses: astral-sh/setup-uv@v6
|
||||||
|
with:
|
||||||
|
version: "latest"
|
||||||
|
|
||||||
|
- name: Build package
|
||||||
|
run: |
|
||||||
|
uv build
|
||||||
|
|
||||||
|
- name: Publish to PyPI
|
||||||
|
run: |
|
||||||
|
uv publish --token ${{ secrets.PYPI_TOKEN }}
|
||||||
2
.github/workflows/run-tests.yml
vendored
2
.github/workflows/run-tests.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ['3.10', '3.11', '3.12']
|
python-version: ['3.11', '3.12', '3.13']
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
108
.github/workflows/test-dev-image.yaml
vendored
Normal file
108
.github/workflows/test-dev-image.yaml
vendored
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
name: Test Dev Image
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows: ["Build Dev Image"]
|
||||||
|
types:
|
||||||
|
- completed
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test-dev-image:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
# Only run if the build workflow succeeded
|
||||||
|
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Update Docker Compose to use master tag
|
||||||
|
working-directory: ./docker
|
||||||
|
run: |
|
||||||
|
# Replace 'latest' with 'master' tag for testing the dev image
|
||||||
|
sed -i 's/rockchin\/langbot:latest/rockchin\/langbot:master/g' docker-compose.yaml
|
||||||
|
echo "Updated docker-compose.yaml to use master tag:"
|
||||||
|
cat docker-compose.yaml
|
||||||
|
|
||||||
|
- name: Start Docker Compose
|
||||||
|
working-directory: ./docker
|
||||||
|
run: docker compose up -d
|
||||||
|
|
||||||
|
- name: Wait and Test API
|
||||||
|
run: |
|
||||||
|
# Function to test API endpoint
|
||||||
|
test_api() {
|
||||||
|
echo "Testing API endpoint..."
|
||||||
|
response=$(curl -s --connect-timeout 10 --max-time 30 -w "\n%{http_code}" http://localhost:5300/api/v1/system/info 2>&1)
|
||||||
|
curl_exit_code=$?
|
||||||
|
|
||||||
|
if [ $curl_exit_code -ne 0 ]; then
|
||||||
|
echo "Curl failed with exit code: $curl_exit_code"
|
||||||
|
echo "Error: $response"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
http_code=$(echo "$response" | tail -n 1)
|
||||||
|
response_body=$(echo "$response" | head -n -1)
|
||||||
|
|
||||||
|
if [ "$http_code" = "200" ]; then
|
||||||
|
echo "API is healthy! Response code: $http_code"
|
||||||
|
echo "Response: $response_body"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
echo "API returned non-200 response: $http_code"
|
||||||
|
echo "Response body: $response_body"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Wait 30 seconds before first attempt
|
||||||
|
echo "Waiting 30 seconds for services to start..."
|
||||||
|
sleep 30
|
||||||
|
|
||||||
|
# Try up to 3 times with 30-second intervals
|
||||||
|
max_attempts=3
|
||||||
|
attempt=1
|
||||||
|
|
||||||
|
while [ $attempt -le $max_attempts ]; do
|
||||||
|
echo "Attempt $attempt of $max_attempts"
|
||||||
|
|
||||||
|
if test_api; then
|
||||||
|
echo "Success! API is responding correctly."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $attempt -lt $max_attempts ]; then
|
||||||
|
echo "Retrying in 30 seconds..."
|
||||||
|
sleep 30
|
||||||
|
fi
|
||||||
|
|
||||||
|
attempt=$((attempt + 1))
|
||||||
|
done
|
||||||
|
|
||||||
|
# All attempts failed
|
||||||
|
echo "Failed to get healthy response after $max_attempts attempts"
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
- name: Show Container Logs on Failure
|
||||||
|
if: failure()
|
||||||
|
working-directory: ./docker
|
||||||
|
run: |
|
||||||
|
echo "=== Docker Compose Status ==="
|
||||||
|
docker compose ps
|
||||||
|
echo ""
|
||||||
|
echo "=== LangBot Logs ==="
|
||||||
|
docker compose logs langbot
|
||||||
|
echo ""
|
||||||
|
echo "=== Plugin Runtime Logs ==="
|
||||||
|
docker compose logs langbot_plugin_runtime
|
||||||
|
|
||||||
|
- name: Cleanup
|
||||||
|
if: always()
|
||||||
|
working-directory: ./docker
|
||||||
|
run: docker compose down
|
||||||
171
.github/workflows/test-migrations.yml
vendored
Normal file
171
.github/workflows/test-migrations.yml
vendored
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
name: Test Migrations
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- master
|
||||||
|
- dev
|
||||||
|
paths:
|
||||||
|
- 'src/langbot/pkg/persistence/**'
|
||||||
|
- 'src/langbot/pkg/entity/persistence/**'
|
||||||
|
pull_request:
|
||||||
|
types: [opened, synchronize, reopened, ready_for_review]
|
||||||
|
paths:
|
||||||
|
- 'src/langbot/pkg/persistence/**'
|
||||||
|
- 'src/langbot/pkg/entity/persistence/**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test-migrations-sqlite:
|
||||||
|
name: Migrations (SQLite)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.12'
|
||||||
|
|
||||||
|
- name: Install uv
|
||||||
|
uses: astral-sh/setup-uv@v4
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: uv sync --dev
|
||||||
|
|
||||||
|
- name: Test Alembic upgrade (SQLite)
|
||||||
|
run: |
|
||||||
|
uv run python -c "
|
||||||
|
import asyncio
|
||||||
|
from sqlalchemy.ext.asyncio import create_async_engine
|
||||||
|
from langbot.pkg.entity.persistence.base import Base
|
||||||
|
from langbot.pkg.persistence.alembic_runner import run_alembic_upgrade, run_alembic_stamp, get_alembic_current
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
engine = create_async_engine('sqlite+aiosqlite:///test_migrations.db')
|
||||||
|
|
||||||
|
# Create all tables (simulates existing DB)
|
||||||
|
async with engine.begin() as conn:
|
||||||
|
await conn.run_sync(Base.metadata.create_all)
|
||||||
|
|
||||||
|
# Stamp baseline
|
||||||
|
await run_alembic_stamp(engine, '0001_baseline')
|
||||||
|
rev = await get_alembic_current(engine)
|
||||||
|
assert rev == '0001_baseline', f'Expected 0001_baseline, got {rev}'
|
||||||
|
print(f'Stamped: {rev}')
|
||||||
|
|
||||||
|
# Upgrade to head
|
||||||
|
await run_alembic_upgrade(engine, 'head')
|
||||||
|
rev = await get_alembic_current(engine)
|
||||||
|
print(f'After upgrade: {rev}')
|
||||||
|
assert rev is not None, 'Expected a revision after upgrade'
|
||||||
|
|
||||||
|
# Verify idempotent
|
||||||
|
await run_alembic_upgrade(engine, 'head')
|
||||||
|
rev2 = await get_alembic_current(engine)
|
||||||
|
assert rev2 == rev, f'Expected {rev}, got {rev2}'
|
||||||
|
print(f'Idempotent check passed: {rev2}')
|
||||||
|
|
||||||
|
# Fresh DB: upgrade from scratch
|
||||||
|
engine2 = create_async_engine('sqlite+aiosqlite:///test_migrations_fresh.db')
|
||||||
|
async with engine2.begin() as conn:
|
||||||
|
await conn.run_sync(Base.metadata.create_all)
|
||||||
|
await run_alembic_upgrade(engine2, 'head')
|
||||||
|
rev3 = await get_alembic_current(engine2)
|
||||||
|
print(f'Fresh DB upgrade: {rev3}')
|
||||||
|
assert rev3 is not None
|
||||||
|
|
||||||
|
print('All SQLite migration tests passed!')
|
||||||
|
|
||||||
|
asyncio.run(main())
|
||||||
|
"
|
||||||
|
|
||||||
|
test-migrations-postgres:
|
||||||
|
name: Migrations (PostgreSQL)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:16
|
||||||
|
env:
|
||||||
|
POSTGRES_USER: langbot
|
||||||
|
POSTGRES_PASSWORD: langbot
|
||||||
|
POSTGRES_DB: langbot_test
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
options: >-
|
||||||
|
--health-cmd="pg_isready -U langbot"
|
||||||
|
--health-interval=5s
|
||||||
|
--health-timeout=5s
|
||||||
|
--health-retries=5
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.12'
|
||||||
|
|
||||||
|
- name: Install uv
|
||||||
|
uses: astral-sh/setup-uv@v4
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: uv sync --dev
|
||||||
|
|
||||||
|
- name: Test Alembic upgrade (PostgreSQL)
|
||||||
|
run: |
|
||||||
|
uv run python -c "
|
||||||
|
import asyncio
|
||||||
|
from sqlalchemy.ext.asyncio import create_async_engine
|
||||||
|
from langbot.pkg.entity.persistence.base import Base
|
||||||
|
from langbot.pkg.persistence.alembic_runner import run_alembic_upgrade, run_alembic_stamp, get_alembic_current
|
||||||
|
|
||||||
|
DB_URL = 'postgresql+asyncpg://langbot:langbot@localhost:5432/langbot_test'
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
engine = create_async_engine(DB_URL)
|
||||||
|
|
||||||
|
# Create all tables
|
||||||
|
async with engine.begin() as conn:
|
||||||
|
await conn.run_sync(Base.metadata.create_all)
|
||||||
|
|
||||||
|
# Stamp baseline
|
||||||
|
await run_alembic_stamp(engine, '0001_baseline')
|
||||||
|
rev = await get_alembic_current(engine)
|
||||||
|
assert rev == '0001_baseline', f'Expected 0001_baseline, got {rev}'
|
||||||
|
print(f'Stamped: {rev}')
|
||||||
|
|
||||||
|
# Upgrade to head
|
||||||
|
await run_alembic_upgrade(engine, 'head')
|
||||||
|
rev = await get_alembic_current(engine)
|
||||||
|
print(f'After upgrade: {rev}')
|
||||||
|
assert rev is not None
|
||||||
|
|
||||||
|
# Verify idempotent
|
||||||
|
await run_alembic_upgrade(engine, 'head')
|
||||||
|
rev2 = await get_alembic_current(engine)
|
||||||
|
assert rev2 == rev, f'Expected {rev}, got {rev2}'
|
||||||
|
print(f'Idempotent check passed: {rev2}')
|
||||||
|
|
||||||
|
# Fresh DB: drop all and upgrade from scratch
|
||||||
|
engine2 = create_async_engine(DB_URL.replace('langbot_test', 'langbot_fresh'))
|
||||||
|
|
||||||
|
# Create fresh database
|
||||||
|
from sqlalchemy import text
|
||||||
|
async with engine.connect() as conn:
|
||||||
|
await conn.execute(text('COMMIT'))
|
||||||
|
await conn.execute(text('CREATE DATABASE langbot_fresh'))
|
||||||
|
|
||||||
|
async with engine2.begin() as conn:
|
||||||
|
await conn.run_sync(Base.metadata.create_all)
|
||||||
|
await run_alembic_upgrade(engine2, 'head')
|
||||||
|
rev3 = await get_alembic_current(engine2)
|
||||||
|
print(f'Fresh DB upgrade: {rev3}')
|
||||||
|
assert rev3 is not None
|
||||||
|
|
||||||
|
print('All PostgreSQL migration tests passed!')
|
||||||
|
|
||||||
|
asyncio.run(main())
|
||||||
|
"
|
||||||
14
.gitignore
vendored
14
.gitignore
vendored
@@ -42,7 +42,17 @@ botpy.log*
|
|||||||
test.py
|
test.py
|
||||||
/web_ui
|
/web_ui
|
||||||
.venv/
|
.venv/
|
||||||
uv.lock
|
|
||||||
/test
|
/test
|
||||||
|
plugins.bak
|
||||||
coverage.xml
|
coverage.xml
|
||||||
.coverage
|
.coverage
|
||||||
|
src/langbot/web/
|
||||||
|
testsdk/
|
||||||
|
|
||||||
|
# Build artifacts
|
||||||
|
/dist
|
||||||
|
/build
|
||||||
|
*.egg-info
|
||||||
|
|
||||||
|
# Next.js build cache (legacy)
|
||||||
|
web/.next/
|
||||||
|
|||||||
37
.mcp.json
Normal file
37
.mcp.json
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"shadcn": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": [
|
||||||
|
"shadcn@latest",
|
||||||
|
"mcp"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"sequential-thinking": {
|
||||||
|
"type": "stdio",
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["-y", "@modelcontextprotocol/server-sequential-thinking"],
|
||||||
|
"env": {}
|
||||||
|
},
|
||||||
|
"github": {
|
||||||
|
"type": "stdio",
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["-y", "@modelcontextprotocol/server-github"],
|
||||||
|
"env": {
|
||||||
|
"GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_PERSONAL_ACCESS_TOKEN}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fetch": {
|
||||||
|
"type": "stdio",
|
||||||
|
"command": "uvx",
|
||||||
|
"args": ["mcp-server-fetch"],
|
||||||
|
"env": {}
|
||||||
|
},
|
||||||
|
"playwright": {
|
||||||
|
"type": "stdio",
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["-y", "@playwright/mcp@latest"],
|
||||||
|
"env": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,16 +9,14 @@ repos:
|
|||||||
# Run the formatter of backend.
|
# Run the formatter of backend.
|
||||||
- id: ruff-format
|
- id: ruff-format
|
||||||
|
|
||||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
|
||||||
rev: v3.1.0
|
|
||||||
hooks:
|
|
||||||
- id: prettier
|
|
||||||
types_or: [javascript, jsx, ts, tsx, css, scss]
|
|
||||||
additional_dependencies:
|
|
||||||
- prettier@3.1.0
|
|
||||||
|
|
||||||
- repo: local
|
- repo: local
|
||||||
hooks:
|
hooks:
|
||||||
|
- id: prettier
|
||||||
|
name: prettier
|
||||||
|
entry: npx --prefix web prettier --write --ignore-unknown
|
||||||
|
language: system
|
||||||
|
types_or: [javascript, jsx, ts, tsx, css, scss]
|
||||||
|
|
||||||
- id: lint-staged
|
- id: lint-staged
|
||||||
name: lint-staged
|
name: lint-staged
|
||||||
entry: cd web && pnpm lint-staged
|
entry: cd web && pnpm lint-staged
|
||||||
|
|||||||
88
AGENTS.md
Normal file
88
AGENTS.md
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
This file is for guiding code agents (like Claude Code, GitHub Copilot, OpenAI Codex, etc.) to work in LangBot project.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
LangBot is a open-source LLM native instant messaging bot development platform, aiming to provide an out-of-the-box IM robot development experience, with Agent, RAG, MCP and other LLM application functions, supporting global instant messaging platforms, and providing rich API interfaces, supporting custom development.
|
||||||
|
|
||||||
|
LangBot has a comprehensive frontend, all operations can be performed through the frontend. The project splited into these major parts:
|
||||||
|
|
||||||
|
- `./src/langbot`: The main python package of the project, below are the main modules in this package:
|
||||||
|
- `./pkg`: The core python package of the project backend.
|
||||||
|
- `./pkg/platform`: The platform module of the project, containing the logic of message platform adapters, bot managers, message session managers, etc.
|
||||||
|
- `./pkg/provider`: The provider module of the project, containing the logic of LLM providers, tool providers, etc.
|
||||||
|
- `./pkg/pipeline`: The pipeline module of the project, containing the logic of pipelines, stages, query pool, etc.
|
||||||
|
- `./pkg/api`: The api module of the project, containing the http api controllers and services.
|
||||||
|
- `./pkg/plugin`: LangBot bridge for connecting with plugin system.
|
||||||
|
- `./libs`: Some SDKs we previously developed for the project, such as `qq_official_api`, `wecom_api`, etc.
|
||||||
|
- `./templates`: Templates of config files, components, etc.
|
||||||
|
- `./web`: Frontend codebase, built with Next.js + **shadcn** + **Tailwind CSS**.
|
||||||
|
- `./docker`: docker-compose deployment files.
|
||||||
|
|
||||||
|
## Backend Development
|
||||||
|
|
||||||
|
We use `uv` to manage dependencies.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install uv
|
||||||
|
uv sync --dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Start the backend and run the project in development mode.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Then you can access the project at `http://127.0.0.1:5300`.
|
||||||
|
|
||||||
|
## Frontend Development
|
||||||
|
|
||||||
|
We use `pnpm` to manage dependencies.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd web
|
||||||
|
cp .env.example .env
|
||||||
|
pnpm install
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Then you can access the project at `http://127.0.0.1:3000`.
|
||||||
|
|
||||||
|
## Plugin System Architecture
|
||||||
|
|
||||||
|
LangBot is composed of various internal components such as Large Language Model tools, commands, messaging platform adapters, LLM requesters, and more. To meet extensibility and flexibility requirements, we have implemented a production-grade plugin system.
|
||||||
|
|
||||||
|
Each plugin runs in an independent process, managed uniformly by the Plugin Runtime. It has two operating modes: `stdio` and `websocket`. When LangBot is started directly by users (not running in a container), it uses `stdio` mode, which is common for personal users or lightweight environments. When LangBot runs in a container, it uses `websocket` mode, designed specifically for production environments.
|
||||||
|
|
||||||
|
Plugin Runtime automatically starts each installed plugin and interacts through stdio. In plugin development scenarios, developers can use the lbp command-line tool to start plugins and connect to the running Runtime via WebSocket for debugging.
|
||||||
|
|
||||||
|
> Plugin SDK, CLI, Runtime, and entities definitions shared between LangBot and plugins are contained in the [`langbot-plugin-sdk`](https://github.com/langbot-app/langbot-plugin-sdk) repository.
|
||||||
|
|
||||||
|
## Some Development Tips and Standards
|
||||||
|
|
||||||
|
- LangBot is a global project, any comments in code should be in English, and user experience should be considered in all aspects.
|
||||||
|
- Thus you should consider the i18n support in all aspects.
|
||||||
|
- LangBot is widely adopted in both toC and toB scenarios, so you should consider the compatibility and security in all aspects.
|
||||||
|
- If you were asked to make a commit, please follow the commit message format:
|
||||||
|
- format: <type>(<scope>): <subject>
|
||||||
|
- type: must be a specific type, such as feat (new feature), fix (bug fix), docs (documentation), style (code style), refactor (refactoring), perf (performance optimization), etc.
|
||||||
|
- scope: the scope of the commit, such as the package name, the file name, the function name, the class name, the module name, etc.
|
||||||
|
- subject: the subject of the commit, such as the description of the commit, the reason for the commit, the impact of the commit, etc.
|
||||||
|
- LangBot uses [Alembic](https://alembic.sqlalchemy.org/) to manage database migrations, supporting both SQLite and PostgreSQL. Migration files are located in `src/langbot/pkg/persistence/alembic/versions/`. If you changed the definition of database entities (ORM models), generate a new migration script by running `uv run python -m langbot.pkg.persistence.alembic_runner autogenerate "description of your change"` in the project root (requires `data/config.yaml` to exist). Review and edit the generated script before committing. Migrations are executed automatically on LangBot startup. For data migrations (e.g. modifying JSON field content), you need to manually add the migration code in the generated script.
|
||||||
|
|
||||||
|
## Some Principles
|
||||||
|
|
||||||
|
- Keep it simple, stupid.
|
||||||
|
- Entities should not be multiplied unnecessarily
|
||||||
|
- 八荣八耻
|
||||||
|
|
||||||
|
以瞎猜接口为耻,以认真查询为荣。
|
||||||
|
以模糊执行为耻,以寻求确认为荣。
|
||||||
|
以臆想业务为耻,以人类确认为荣。
|
||||||
|
以创造接口为耻,以复用现有为荣。
|
||||||
|
以跳过验证为耻,以主动测试为荣。
|
||||||
|
以破坏架构为耻,以遵循规范为荣。
|
||||||
|
以假装理解为耻,以诚实无知为荣。
|
||||||
|
以盲目修改为耻,以谨慎重构为荣。
|
||||||
@@ -4,7 +4,7 @@ WORKDIR /app
|
|||||||
|
|
||||||
COPY web ./web
|
COPY web ./web
|
||||||
|
|
||||||
RUN cd web && npm install && npm run build
|
RUN cd web && npm install && npx vite build
|
||||||
|
|
||||||
FROM python:3.12.7-slim
|
FROM python:3.12.7-slim
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ WORKDIR /app
|
|||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
COPY --from=node /app/web/out ./web/out
|
COPY --from=node /app/web/dist ./web/dist
|
||||||
|
|
||||||
RUN apt update \
|
RUN apt update \
|
||||||
&& apt install gcc -y \
|
&& apt install gcc -y \
|
||||||
@@ -20,4 +20,4 @@ RUN apt update \
|
|||||||
&& uv sync \
|
&& uv sync \
|
||||||
&& touch /.dockerenv
|
&& touch /.dockerenv
|
||||||
|
|
||||||
CMD [ "uv", "run", "main.py" ]
|
CMD [ "uv", "run", "--no-sync", "main.py" ]
|
||||||
221
README.md
221
README.md
@@ -1,37 +1,69 @@
|
|||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://langbot.app">
|
<a href="https://langbot.app">
|
||||||
<img src="https://docs.langbot.app/social_zh.png" alt="LangBot"/>
|
<img width="130" src="res/logo-blue.png" alt="LangBot"/>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
<a href="https://hellogithub.com/repository/langbot-app/LangBot" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=5ce8ae2aa4f74316bf393b57b952433c&claim_uid=gtmc6YWjMZkT21R" alt="Featured|HelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
<a href="https://www.producthunt.com/products/langbot?utm_source=badge-follow&utm_medium=badge&utm_source=badge-langbot" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/follow.svg?product_id=1077185&theme=light" alt="LangBot - Production-grade IM bot made easy. | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||||
|
|
||||||
[English](README_EN.md) / 简体中文 / [繁體中文](README_TW.md) / [日本語](README_JP.md) / (PR for your language)
|
<h3>Production-grade platform for building agentic IM bots.</h3>
|
||||||
|
<h4>Quickly build, debug, and ship AI bots to Slack, Discord, Telegram, WeChat, and more.</h4>
|
||||||
|
|
||||||
|
English / [简体中文](README_CN.md) / [繁體中文](README_TW.md) / [日本語](README_JP.md) / [Español](README_ES.md) / [Français](README_FR.md) / [한국어](README_KO.md) / [Русский](README_RU.md) / [Tiếng Việt](README_VI.md)
|
||||||
|
|
||||||
[](https://discord.gg/wdNEHETs87)
|
[](https://discord.gg/wdNEHETs87)
|
||||||
[](https://qm.qq.com/q/JLi38whHum)
|
|
||||||
[](https://deepwiki.com/langbot-app/LangBot)
|
[](https://deepwiki.com/langbot-app/LangBot)
|
||||||
[](https://github.com/langbot-app/LangBot/releases/latest)
|
[](https://github.com/langbot-app/LangBot/releases/latest)
|
||||||
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
|
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
|
||||||
[](https://gitcode.com/RockChinQ/LangBot)
|
[](https://github.com/langbot-app/LangBot/stargazers)
|
||||||
|
|
||||||
<a href="https://langbot.app">项目主页</a> |
|
|
||||||
<a href="https://docs.langbot.app/zh/insight/guide.html">部署文档</a> |
|
|
||||||
<a href="https://docs.langbot.app/zh/plugin/plugin-intro.html">插件介绍</a> |
|
|
||||||
<a href="https://github.com/langbot-app/LangBot/issues/new?assignees=&labels=%E7%8B%AC%E7%AB%8B%E6%8F%92%E4%BB%B6&projects=&template=submit-plugin.yml&title=%5BPlugin%5D%3A+%E8%AF%B7%E6%B1%82%E7%99%BB%E8%AE%B0%E6%96%B0%E6%8F%92%E4%BB%B6">提交插件</a>
|
|
||||||
|
|
||||||
|
<a href="https://langbot.app">Website</a> |
|
||||||
|
<a href="https://link.langbot.app/en/docs/features">Features</a> |
|
||||||
|
<a href="https://link.langbot.app/en/docs/guide">Docs</a> |
|
||||||
|
<a href="https://link.langbot.app/en/docs/api">API</a> |
|
||||||
|
<a href="https://space.langbot.app/cloud">Cloud</a> |
|
||||||
|
<a href="https://space.langbot.app">Plugin Market</a> |
|
||||||
|
<a href="https://langbot.featurebase.app/roadmap">Roadmap</a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
LangBot 是一个开源的大语言模型原生即时通信机器人开发平台,旨在提供开箱即用的 IM 机器人开发体验,具有 Agent、RAG、MCP 等多种 LLM 应用功能,适配全球主流即时通信平台,并提供丰富的 API 接口,支持自定义开发。
|
---
|
||||||
|
|
||||||
## 📦 开始使用
|
## What is LangBot?
|
||||||
|
|
||||||
#### Docker Compose 部署
|
LangBot is an **open-source, production-grade platform** for building AI-powered instant messaging bots. It connects Large Language Models (LLMs) to any chat platform, enabling you to create intelligent agents that can converse, execute tasks, and integrate with your existing workflows.
|
||||||
|
|
||||||
|
### Key Capabilities
|
||||||
|
|
||||||
|
- **AI Conversations & Agents** — Multi-turn dialogues, tool calling, multi-modal support, streaming output. Built-in RAG (knowledge base) with deep integration to [Dify](https://dify.ai), [Coze](https://coze.com), [n8n](https://n8n.io), [Langflow](https://langflow.org).
|
||||||
|
- **Universal IM Platform Support** — One codebase for Discord, Telegram, Slack, LINE, QQ, WeChat, WeCom, Lark, DingTalk, KOOK.
|
||||||
|
- **Production-Ready** — Access control, rate limiting, sensitive word filtering, comprehensive monitoring, and exception handling. Trusted by enterprises.
|
||||||
|
- **Plugin Ecosystem** — Hundreds of plugins, event-driven architecture, component extensions, and [MCP protocol](https://modelcontextprotocol.io/) support.
|
||||||
|
- **Web Management Panel** — Configure, manage, and monitor your bots through an intuitive browser interface. No YAML editing required.
|
||||||
|
- **Multi-Pipeline Architecture** — Different bots for different scenarios, with comprehensive monitoring and exception handling.
|
||||||
|
|
||||||
|
[→ Learn more about all features](https://link.langbot.app/en/docs/features)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### ☁️ LangBot Cloud (Recommended)
|
||||||
|
|
||||||
|
**[LangBot Cloud](https://space.langbot.app/cloud)** — Zero deployment, ready to use.
|
||||||
|
|
||||||
|
### One-Line Launch
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uvx langbot
|
||||||
|
```
|
||||||
|
|
||||||
|
> Requires [uv](https://docs.astral.sh/uv/getting-started/installation/). Visit http://localhost:5300 — done.
|
||||||
|
|
||||||
|
### Docker Compose
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/langbot-app/LangBot
|
git clone https://github.com/langbot-app/LangBot
|
||||||
@@ -39,110 +71,105 @@ cd LangBot/docker
|
|||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
访问 http://localhost:5300 即可开始使用。
|
### One-Click Cloud Deploy
|
||||||
|
|
||||||
详细文档[Docker 部署](https://docs.langbot.app/zh/deploy/langbot/docker.html)。
|
|
||||||
|
|
||||||
#### 宝塔面板部署
|
|
||||||
|
|
||||||
已上架宝塔面板,若您已安装宝塔面板,可以根据[文档](https://docs.langbot.app/zh/deploy/langbot/one-click/bt.html)使用。
|
|
||||||
|
|
||||||
#### Zeabur 云部署
|
|
||||||
|
|
||||||
社区贡献的 Zeabur 模板。
|
|
||||||
|
|
||||||
[](https://zeabur.com/zh-CN/templates/ZKTBDH)
|
|
||||||
|
|
||||||
#### Railway 云部署
|
|
||||||
|
|
||||||
|
[](https://zeabur.com/en-US/templates/ZKTBDH)
|
||||||
[](https://railway.app/template/yRrAyL?referralCode=vogKPF)
|
[](https://railway.app/template/yRrAyL?referralCode=vogKPF)
|
||||||
|
|
||||||
#### 手动部署
|
**More options:** [Docker](https://link.langbot.app/en/docs/docker) · [Manual](https://link.langbot.app/en/docs/manual-deploy) · [BTPanel](https://link.langbot.app/en/docs/bt-panel) · [Kubernetes](./docker/README_K8S.md)
|
||||||
|
|
||||||
直接使用发行版运行,查看文档[手动部署](https://docs.langbot.app/zh/deploy/langbot/manual.html)。
|
---
|
||||||
|
|
||||||
## 😎 保持更新
|
## Supported Platforms
|
||||||
|
|
||||||
点击仓库右上角 Star 和 Watch 按钮,获取最新动态。
|
| Platform | Status | Notes |
|
||||||
|
|----------|--------|-------|
|
||||||
|
| Discord | ✅ | Official |
|
||||||
|
| Telegram | ✅ | Official |
|
||||||
|
| Slack | ✅ | Official |
|
||||||
|
| LINE | ✅ | Official |
|
||||||
|
| QQ | ✅ | Personal & Official API (Channel, DM, Group) |
|
||||||
|
| WeCom | ✅ | Enterprise WeChat, External CS, AI Bot |
|
||||||
|
| WeChat | ✅ | Personal & Official Account |
|
||||||
|
| Lark | ✅ | Official |
|
||||||
|
| DingTalk | ✅ | Official |
|
||||||
|
| KOOK | ✅ | Official |
|
||||||
|
| Satori | ✅ | |
|
||||||
|
| Email | ✅ | Matrix, Satori |
|
||||||
|
| Matrix | ✅ | Supports multiple bridged platforms such as Signal, WhatsApp, Messenger, iMessage, Mattermost, Google Chat, IRC, XMPP, Zulip, and more |
|
||||||
|
|
||||||

|
---
|
||||||
|
|
||||||
## ✨ 特性
|
## Supported LLMs & Integrations
|
||||||
|
|
||||||
- 💬 大模型对话、Agent:支持多种大模型,适配群聊和私聊;具有多轮对话、工具调用、多模态、流式输出能力,自带 RAG(知识库)实现,并深度适配 [Dify](https://dify.ai)。
|
| Provider | Type | Status |
|
||||||
- 🤖 多平台支持:目前支持 QQ、QQ频道、企业微信、个人微信、飞书、Discord、Telegram 等平台。
|
| ----------------------------------------------------------------------------------------------------------------- | ------------ | ------ |
|
||||||
- 🛠️ 高稳定性、功能完备:原生支持访问控制、限速、敏感词过滤等机制;配置简单,支持多种部署方式。支持多流水线配置,不同机器人用于不同应用场景。
|
| [OpenAI](https://platform.openai.com/) | LLM | ✅ |
|
||||||
- 🧩 插件扩展、活跃社区:支持事件驱动、组件扩展等插件机制;适配 Anthropic [MCP 协议](https://modelcontextprotocol.io/);目前已有数百个插件。
|
| [Anthropic](https://www.anthropic.com/) | LLM | ✅ |
|
||||||
- 😻 Web 管理面板:支持通过浏览器管理 LangBot 实例,不再需要手动编写配置文件。
|
| [DeepSeek](https://www.deepseek.com/) | LLM | ✅ |
|
||||||
|
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | LLM | ✅ |
|
||||||
|
| [xAI](https://x.ai/) | LLM | ✅ |
|
||||||
|
| [Moonshot](https://www.moonshot.cn/) | LLM | ✅ |
|
||||||
|
| [Zhipu AI](https://open.bigmodel.cn/) | LLM | ✅ |
|
||||||
|
| [Ollama](https://ollama.com/) | Local LLM | ✅ |
|
||||||
|
| [LM Studio](https://lmstudio.ai/) | Local LLM | ✅ |
|
||||||
|
| [Dify](https://dify.ai) | LLMOps | ✅ |
|
||||||
|
| [MCP](https://modelcontextprotocol.io/) | Protocol | ✅ |
|
||||||
|
| [SiliconFlow](https://siliconflow.cn/) | Gateway | ✅ |
|
||||||
|
| [Aliyun Bailian](https://bailian.console.aliyun.com/) | Gateway | ✅ |
|
||||||
|
| [Volc Engine Ark](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | Gateway | ✅ |
|
||||||
|
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | Gateway | ✅ |
|
||||||
|
| [GiteeAI](https://ai.gitee.com/) | Gateway | ✅ |
|
||||||
|
| [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | GPU Platform | ✅ |
|
||||||
|
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | GPU Platform | ✅ |
|
||||||
|
| [ShengSuanYun](https://www.shengsuanyun.com/?from=CH_KYIPP758) | GPU Platform | ✅ |
|
||||||
|
| [接口 AI](https://jiekou.ai/) | Gateway | ✅ |
|
||||||
|
| [302.AI](https://share.302.ai/SuTG99) | Gateway | ✅ |
|
||||||
|
| [Qiniu](https://www.qiniu.com/ai/agent) | Gateway | ✅ |
|
||||||
|
|
||||||
详细规格特性请访问[文档](https://docs.langbot.app/zh/insight/features.html)。
|
[→ View all integrations](https://link.langbot.app/en/docs/features)
|
||||||
|
|
||||||
或访问 demo 环境:https://demo.langbot.dev/
|
---
|
||||||
- 登录信息:邮箱:`demo@langbot.app` 密码:`langbot123456`
|
|
||||||
- 注意:仅展示 WebUI 效果,公开环境,请不要在其中填入您的任何敏感信息。
|
|
||||||
|
|
||||||
### 消息平台
|
## Why LangBot?
|
||||||
|
|
||||||
| 平台 | 状态 | 备注 |
|
| Use Case | How LangBot Helps |
|
||||||
| --- | --- | --- |
|
| --------------------------- | ------------------------------------------------------------------------------------------ |
|
||||||
| QQ 个人号 | ✅ | QQ 个人号私聊、群聊 |
|
| **Customer Support** | Deploy AI agents to Slack/Discord/Telegram that answer questions using your knowledge base |
|
||||||
| QQ 官方机器人 | ✅ | QQ 官方机器人,支持频道、私聊、群聊 |
|
| **Internal Tools** | Connect n8n/Dify workflows to WeCom/DingTalk for automated business processes |
|
||||||
| 企业微信 | ✅ | |
|
| **Community Management** | Moderate QQ/Discord groups with AI-powered content filtering and interaction |
|
||||||
| 企微对外客服 | ✅ | |
|
| **Multi-Platform Presence** | One bot, all platforms. Manage from a single dashboard |
|
||||||
| 企微智能机器人 | ✅ | |
|
|
||||||
| 个人微信 | ✅ | |
|
|
||||||
| 微信公众号 | ✅ | |
|
|
||||||
| 飞书 | ✅ | |
|
|
||||||
| 钉钉 | ✅ | |
|
|
||||||
| Discord | ✅ | |
|
|
||||||
| Telegram | ✅ | |
|
|
||||||
| Slack | ✅ | |
|
|
||||||
| LINE | ✅ | |
|
|
||||||
|
|
||||||
### 大模型能力
|
---
|
||||||
|
|
||||||
| 模型 | 状态 | 备注 |
|
## Live Demo
|
||||||
| --- | --- | --- |
|
|
||||||
| [OpenAI](https://platform.openai.com/) | ✅ | 可接入任何 OpenAI 接口格式模型 |
|
|
||||||
| [DeepSeek](https://www.deepseek.com/) | ✅ | |
|
|
||||||
| [Moonshot](https://www.moonshot.cn/) | ✅ | |
|
|
||||||
| [Anthropic](https://www.anthropic.com/) | ✅ | |
|
|
||||||
| [xAI](https://x.ai/) | ✅ | |
|
|
||||||
| [智谱AI](https://open.bigmodel.cn/) | ✅ | |
|
|
||||||
| [胜算云](https://www.shengsuanyun.com/?from=CH_KYIPP758) | ✅ | 全球大模型都可调用(友情推荐) |
|
|
||||||
| [优云智算](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | ✅ | 大模型和 GPU 资源平台 |
|
|
||||||
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | 大模型和 GPU 资源平台 |
|
|
||||||
| [302.AI](https://share.302.ai/SuTG99) | ✅ | 大模型聚合平台 |
|
|
||||||
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | ✅ | |
|
|
||||||
| [Dify](https://dify.ai) | ✅ | LLMOps 平台 |
|
|
||||||
| [Ollama](https://ollama.com/) | ✅ | 本地大模型运行平台 |
|
|
||||||
| [LMStudio](https://lmstudio.ai/) | ✅ | 本地大模型运行平台 |
|
|
||||||
| [GiteeAI](https://ai.gitee.com/) | ✅ | 大模型接口聚合平台 |
|
|
||||||
| [SiliconFlow](https://siliconflow.cn/) | ✅ | 大模型聚合平台 |
|
|
||||||
| [小马算力](https://www.tokenpony.cn/453z1) | ✅ | 大模型聚合平台 |
|
|
||||||
| [阿里云百炼](https://bailian.console.aliyun.com/) | ✅ | 大模型聚合平台, LLMOps 平台 |
|
|
||||||
| [火山方舟](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | ✅ | 大模型聚合平台, LLMOps 平台 |
|
|
||||||
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | ✅ | 大模型聚合平台 |
|
|
||||||
| [MCP](https://modelcontextprotocol.io/) | ✅ | 支持通过 MCP 协议获取工具 |
|
|
||||||
| [百宝箱Tbox](https://www.tbox.cn/open) | ✅ | 蚂蚁百宝箱智能体平台,每月免费10亿大模型Token |
|
|
||||||
|
|
||||||
### TTS
|
**Try it now:** https://demo.langbot.dev/
|
||||||
|
|
||||||
| 平台/模型 | 备注 |
|
- Email: `demo@langbot.app`
|
||||||
| --- | --- |
|
- Password: `langbot123456`
|
||||||
| [FishAudio](https://fish.audio/zh-CN/discovery/) | [插件](https://github.com/the-lazy-me/NewChatVoice) |
|
|
||||||
| [海豚 AI](https://www.ttson.cn/?source=thelazy) | [插件](https://github.com/the-lazy-me/NewChatVoice) |
|
|
||||||
| [AzureTTS](https://portal.azure.com/) | [插件](https://github.com/Ingnaryk/LangBot_AzureTTS) |
|
|
||||||
|
|
||||||
### 文生图
|
_Note: Public demo environment. Do not enter sensitive information._
|
||||||
|
|
||||||
| 平台/模型 | 备注 |
|
---
|
||||||
| --- | --- |
|
|
||||||
| 阿里云百炼 | [插件](https://github.com/Thetail001/LangBot_BailianTextToImagePlugin)
|
|
||||||
|
|
||||||
## 😘 社区贡献
|
## Community
|
||||||
|
|
||||||
感谢以下[代码贡献者](https://github.com/langbot-app/LangBot/graphs/contributors)和社区里其他成员对 LangBot 的贡献:
|
[](https://discord.gg/wdNEHETs87)
|
||||||
|
|
||||||
|
- [Discord Community](https://discord.gg/wdNEHETs87)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Star History
|
||||||
|
|
||||||
|
[](https://star-history.com/#langbot-app/LangBot&Date)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
|
||||||
|
Thanks to all [contributors](https://github.com/langbot-app/LangBot/graphs/contributors) who have helped make LangBot better:
|
||||||
|
|
||||||
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
|
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
|
||||||
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
|
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
|
||||||
|
|||||||
199
README_CN.md
Normal file
199
README_CN.md
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
<p align="center">
|
||||||
|
<a href="https://langbot.app">
|
||||||
|
<img width="130" src="res/logo-blue.png" alt="LangBot"/>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
<a href="https://hellogithub.com/repository/langbot-app/LangBot" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=5ce8ae2aa4f74316bf393b57b952433c&claim_uid=gtmc6YWjMZkT21R" alt="Featured|HelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||||
|
|
||||||
|
<h3>生产级 AI 即时通信机器人开发平台。</h3>
|
||||||
|
<h4>快速构建、调试和部署 AI 机器人到微信、QQ、飞书、Slack、Discord、Telegram 等平台。</h4>
|
||||||
|
|
||||||
|
[English](README.md) / 简体中文 / [繁體中文](README_TW.md) / [日本語](README_JP.md) / [Español](README_ES.md) / [Français](README_FR.md) / [한국어](README_KO.md) / [Русский](README_RU.md) / [Tiếng Việt](README_VI.md)
|
||||||
|
|
||||||
|
[](https://discord.gg/wdNEHETs87)
|
||||||
|
[](https://qm.qq.com/q/DxZZcNxM1W)
|
||||||
|
[](https://deepwiki.com/langbot-app/LangBot)
|
||||||
|
[](https://github.com/langbot-app/LangBot/releases/latest)
|
||||||
|
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
|
||||||
|
[](https://github.com/langbot-app/LangBot/stargazers)
|
||||||
|
[](https://gitcode.com/RockChinQ/LangBot)
|
||||||
|
|
||||||
|
<a href="https://langbot.app">官网</a> |
|
||||||
|
<a href="https://link.langbot.app/zh/docs/features">特性</a> |
|
||||||
|
<a href="https://link.langbot.app/zh/docs/guide">文档</a> |
|
||||||
|
<a href="https://link.langbot.app/zh/docs/api">API</a> |
|
||||||
|
<a href="https://space.langbot.app/cloud">Cloud</a> |
|
||||||
|
<a href="https://space.langbot.app">插件市场</a> |
|
||||||
|
<a href="https://langbot.featurebase.app/roadmap">路线图</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
LangBot 是一个**开源的生产级平台**,用于构建 AI 驱动的即时通信机器人。它将大语言模型(LLM)连接到各种聊天平台,帮助你创建能够对话、执行任务、并集成到现有工作流程中的智能 Agent。
|
||||||
|
|
||||||
|
### 核心能力
|
||||||
|
|
||||||
|
- **AI 对话与 Agent** — 多轮对话、工具调用、多模态、流式输出。自带 RAG(知识库),深度集成 [Dify](https://dify.ai)、[Coze](https://coze.com)、[n8n](https://n8n.io)、[Langflow](https://langflow.org) 等 LLMOps 平台。
|
||||||
|
- **全平台支持** — 一套代码,覆盖 QQ、微信、企业微信、飞书、钉钉、Discord、Telegram、Slack、LINE、KOOK 等平台。
|
||||||
|
- **生产就绪** — 访问控制、限速、敏感词过滤、全面监控与异常处理,已被多家企业采用。
|
||||||
|
- **插件生态** — 数百个插件,跨进程的事件驱动架构,组件扩展,适配 [MCP 协议](https://modelcontextprotocol.io/)。
|
||||||
|
- **Web 管理面板** — 通过浏览器直观地配置、管理和监控机器人,无需手动编辑配置文件。
|
||||||
|
- **多流水线架构** — 不同机器人用于不同场景,具备全面的监控和异常处理能力。
|
||||||
|
|
||||||
|
[→ 了解更多功能特性](https://link.langbot.app/zh/docs/features)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
### ☁️ LangBot Cloud(推荐)
|
||||||
|
|
||||||
|
**[LangBot Cloud](https://space.langbot.app/cloud)** — 免部署,开箱即用。
|
||||||
|
|
||||||
|
### 一键启动
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uvx langbot
|
||||||
|
```
|
||||||
|
|
||||||
|
> 需要安装 [uv](https://docs.astral.sh/uv/getting-started/installation/)。访问 http://localhost:5300 即可使用。
|
||||||
|
|
||||||
|
### Docker Compose
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/langbot-app/LangBot
|
||||||
|
cd LangBot/docker
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### 一键云部署
|
||||||
|
|
||||||
|
[](https://zeabur.com/zh-CN/templates/ZKTBDH)
|
||||||
|
[](https://railway.app/template/yRrAyL?referralCode=vogKPF)
|
||||||
|
|
||||||
|
**更多方式:** [Docker](https://link.langbot.app/zh/docs/docker) · [手动部署](https://link.langbot.app/zh/docs/manual-deploy) · [宝塔面板](https://link.langbot.app/zh/docs/bt-panel) · [Kubernetes](./docker/README_K8S.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 支持的平台
|
||||||
|
|
||||||
|
| 平台 | 状态 | 备注 |
|
||||||
|
|------|------|------|
|
||||||
|
| QQ | ✅ | 个人号、官方机器人(频道、私聊、群聊) |
|
||||||
|
| 微信 | ✅ | 个人微信、微信公众号 |
|
||||||
|
| 企业微信 | ✅ | 应用消息、对外客服、智能机器人 |
|
||||||
|
| 飞书 | ✅ | 官方 |
|
||||||
|
| 钉钉 | ✅ | 官方 |
|
||||||
|
| Satori | ✅ | |
|
||||||
|
| Discord | ✅ | 官方 |
|
||||||
|
| Telegram | ✅ | 官方 |
|
||||||
|
| Slack | ✅ | 官方 |
|
||||||
|
| LINE | ✅ | 官方 |
|
||||||
|
| KOOK | ✅ | 官方 |
|
||||||
|
| Email | ✅ | 只 Matrix、Satori |
|
||||||
|
| Matrix | ✅ | 支持多种桥接平台,如 Signal、WhatsApp、Messenger、iMessage、Mattermost、Google Chat、IRC、XMPP、Zulip 等 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 支持的大模型与集成
|
||||||
|
|
||||||
|
| 提供商 | 类型 | 状态 |
|
||||||
|
|--------|------|------|
|
||||||
|
| [OpenAI](https://platform.openai.com/) | LLM | ✅ |
|
||||||
|
| [Anthropic](https://www.anthropic.com/) | LLM | ✅ |
|
||||||
|
| [DeepSeek](https://www.deepseek.com/) | LLM | ✅ |
|
||||||
|
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | LLM | ✅ |
|
||||||
|
| [xAI](https://x.ai/) | LLM | ✅ |
|
||||||
|
| [Moonshot](https://www.moonshot.cn/) | LLM | ✅ |
|
||||||
|
| [智谱AI](https://open.bigmodel.cn/) | LLM | ✅ |
|
||||||
|
| [Ollama](https://ollama.com/) | 本地 LLM | ✅ |
|
||||||
|
| [LM Studio](https://lmstudio.ai/) | 本地 LLM | ✅ |
|
||||||
|
| [Dify](https://dify.ai) | LLMOps | ✅ |
|
||||||
|
| [MCP](https://modelcontextprotocol.io/) | 协议 | ✅ |
|
||||||
|
| [SiliconFlow](https://siliconflow.cn/) | 聚合平台 | ✅ |
|
||||||
|
| [阿里云百炼](https://bailian.console.aliyun.com/) | 聚合平台 | ✅ |
|
||||||
|
| [火山方舟](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | 聚合平台 | ✅ |
|
||||||
|
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | 聚合平台 | ✅ |
|
||||||
|
| [GiteeAI](https://ai.gitee.com/) | 聚合平台 | ✅ |
|
||||||
|
| [胜算云](https://www.shengsuanyun.com/?from=CH_KYIPP758) | GPU 平台 | ✅ |
|
||||||
|
| [优云智算](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | GPU 平台 | ✅ |
|
||||||
|
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | GPU 平台 | ✅ |
|
||||||
|
| [接口 AI](https://jiekou.ai/) | 聚合平台 | ✅ |
|
||||||
|
| [302.AI](https://share.302.ai/SuTG99) | 聚合平台 | ✅ |
|
||||||
|
| [小马算力](https://www.tokenpony.cn/453z1) | 聚合平台 | ✅ |
|
||||||
|
| [百宝箱Tbox](https://www.tbox.cn/open) | 智能体平台 | ✅ |
|
||||||
|
| [七牛云Qiniu](https://www.qiniu.com/ai/agent) | 聚合平台 | ✅ |
|
||||||
|
|
||||||
|
[→ 查看完整集成列表](https://link.langbot.app/zh/docs/features)
|
||||||
|
|
||||||
|
### TTS(语音合成)
|
||||||
|
|
||||||
|
| 平台/模型 | 备注 |
|
||||||
|
|-----------|------|
|
||||||
|
| [FishAudio](https://fish.audio/zh-CN/discovery/) | [插件](https://github.com/the-lazy-me/NewChatVoice) |
|
||||||
|
| [海豚 AI](https://www.ttson.cn/?source=thelazy) | [插件](https://github.com/the-lazy-me/NewChatVoice) |
|
||||||
|
| [AzureTTS](https://portal.azure.com/) | [插件](https://github.com/Ingnaryk/LangBot_AzureTTS) |
|
||||||
|
|
||||||
|
### 文生图
|
||||||
|
|
||||||
|
| 平台/模型 | 备注 |
|
||||||
|
|-----------|------|
|
||||||
|
| 阿里云百炼 | [插件](https://github.com/Thetail001/LangBot_BailianTextToImagePlugin) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 为什么选择 LangBot?
|
||||||
|
|
||||||
|
| 使用场景 | LangBot 如何帮助 |
|
||||||
|
|----------|------------------|
|
||||||
|
| **客户服务** | 将 AI Agent 部署到微信/企微/钉钉/飞书,基于知识库自动回答用户问题 |
|
||||||
|
| **内部工具** | 将 n8n/Dify 工作流接入企微/钉钉,实现业务流程自动化 |
|
||||||
|
| **社群运营** | 在 QQ/Discord 群中使用 AI 驱动的内容审核与智能互动 |
|
||||||
|
| **多平台触达** | 一个机器人,覆盖所有平台。通过统一面板集中管理 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 在线演示
|
||||||
|
|
||||||
|
**立即体验:** https://demo.langbot.dev/
|
||||||
|
- 邮箱:`demo@langbot.app`
|
||||||
|
- 密码:`langbot123456`
|
||||||
|
|
||||||
|
*注意:公开演示环境,请不要在其中填入任何敏感信息。*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 社区
|
||||||
|
|
||||||
|
[](https://discord.gg/wdNEHETs87)
|
||||||
|
[](https://qm.qq.com/q/DxZZcNxM1W)
|
||||||
|
|
||||||
|
- [Discord 社区](https://discord.gg/wdNEHETs87)
|
||||||
|
- [QQ 社区群](https://qm.qq.com/q/DxZZcNxM1W)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Star 趋势
|
||||||
|
|
||||||
|
[](https://star-history.com/#langbot-app/LangBot&Date)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 贡献者
|
||||||
|
|
||||||
|
感谢所有[贡献者](https://github.com/langbot-app/LangBot/graphs/contributors)对 LangBot 的帮助:
|
||||||
|
|
||||||
|
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
|
||||||
|
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
## For Code Agents
|
||||||
|
|
||||||
|
If you're a code agent based on LLM, you should read [AGENTS.md](AGENTS.md) to understand the project structure and development process before you start any development.
|
||||||
|
-->
|
||||||
126
README_EN.md
126
README_EN.md
@@ -1,126 +0,0 @@
|
|||||||
<p align="center">
|
|
||||||
<a href="https://langbot.app">
|
|
||||||
<img src="https://docs.langbot.app/social_en.png" alt="LangBot"/>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<div align="center">
|
|
||||||
|
|
||||||
English / [简体中文](README.md) / [繁體中文](README_TW.md) / [日本語](README_JP.md) / (PR for your language)
|
|
||||||
|
|
||||||
[](https://discord.gg/wdNEHETs87)
|
|
||||||
[](https://deepwiki.com/langbot-app/LangBot)
|
|
||||||
[](https://github.com/langbot-app/LangBot/releases/latest)
|
|
||||||
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
|
|
||||||
|
|
||||||
<a href="https://langbot.app">Home</a> |
|
|
||||||
<a href="https://docs.langbot.app/en/insight/guide.html">Deployment</a> |
|
|
||||||
<a href="https://docs.langbot.app/en/plugin/plugin-intro.html">Plugin</a> |
|
|
||||||
<a href="https://github.com/langbot-app/LangBot/issues/new?assignees=&labels=%E7%8B%AC%E7%AB%8B%E6%8F%92%E4%BB%B6&projects=&template=submit-plugin.yml&title=%5BPlugin%5D%3A+%E8%AF%B7%E6%B1%82%E7%99%BB%E8%AE%B0%E6%96%B0%E6%8F%92%E4%BB%B6">Submit Plugin</a>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</p>
|
|
||||||
|
|
||||||
LangBot is an open-source LLM native instant messaging robot development platform, aiming to provide out-of-the-box IM robot development experience, with Agent, RAG, MCP and other LLM application functions, adapting to global instant messaging platforms, and providing rich API interfaces, supporting custom development.
|
|
||||||
|
|
||||||
## 📦 Getting Started
|
|
||||||
|
|
||||||
#### Docker Compose Deployment
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/langbot-app/LangBot
|
|
||||||
cd LangBot/docker
|
|
||||||
docker compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
Visit http://localhost:5300 to start using it.
|
|
||||||
|
|
||||||
Detailed documentation [Docker Deployment](https://docs.langbot.app/en/deploy/langbot/docker.html).
|
|
||||||
|
|
||||||
#### One-click Deployment on BTPanel
|
|
||||||
|
|
||||||
LangBot has been listed on the BTPanel, if you have installed the BTPanel, you can use the [document](https://docs.langbot.app/en/deploy/langbot/one-click/bt.html) to use it.
|
|
||||||
|
|
||||||
#### Zeabur Cloud Deployment
|
|
||||||
|
|
||||||
Community contributed Zeabur template.
|
|
||||||
|
|
||||||
[](https://zeabur.com/en-US/templates/ZKTBDH)
|
|
||||||
|
|
||||||
#### Railway Cloud Deployment
|
|
||||||
|
|
||||||
[](https://railway.app/template/yRrAyL?referralCode=vogKPF)
|
|
||||||
|
|
||||||
#### Other Deployment Methods
|
|
||||||
|
|
||||||
Directly use the released version to run, see the [Manual Deployment](https://docs.langbot.app/en/deploy/langbot/manual.html) documentation.
|
|
||||||
|
|
||||||
## 😎 Stay Ahead
|
|
||||||
|
|
||||||
Click the Star and Watch button in the upper right corner of the repository to get the latest updates.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## ✨ Features
|
|
||||||
|
|
||||||
- 💬 Chat with LLM / Agent: Supports multiple LLMs, adapt to group chats and private chats; Supports multi-round conversations, tool calls, multi-modal, and streaming output capabilities. Built-in RAG (knowledge base) implementation, and deeply integrates with [Dify](https://dify.ai).
|
|
||||||
- 🤖 Multi-platform Support: Currently supports QQ, QQ Channel, WeCom, personal WeChat, Lark, DingTalk, Discord, Telegram, etc.
|
|
||||||
- 🛠️ High Stability, Feature-rich: Native access control, rate limiting, sensitive word filtering, etc. mechanisms; Easy to use, supports multiple deployment methods. Supports multiple pipeline configurations, different bots can be used for different scenarios.
|
|
||||||
- 🧩 Plugin Extension, Active Community: Support event-driven, component extension, etc. plugin mechanisms; Integrate Anthropic [MCP protocol](https://modelcontextprotocol.io/); Currently has hundreds of plugins.
|
|
||||||
- 😻 Web UI: Support management LangBot instance through the browser. No need to manually write configuration files.
|
|
||||||
|
|
||||||
For more detailed specifications, please refer to the [documentation](https://docs.langbot.app/en/insight/features.html).
|
|
||||||
|
|
||||||
Or visit the demo environment: https://demo.langbot.dev/
|
|
||||||
- Login information: Email: `demo@langbot.app` Password: `langbot123456`
|
|
||||||
- Note: For WebUI demo only, please do not fill in any sensitive information in the public environment.
|
|
||||||
|
|
||||||
### Message Platform
|
|
||||||
|
|
||||||
| Platform | Status | Remarks |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| Discord | ✅ | |
|
|
||||||
| Telegram | ✅ | |
|
|
||||||
| Slack | ✅ | |
|
|
||||||
| LINE | ✅ | |
|
|
||||||
| Personal QQ | ✅ | |
|
|
||||||
| QQ Official API | ✅ | |
|
|
||||||
| WeCom | ✅ | |
|
|
||||||
| WeComCS | ✅ | |
|
|
||||||
| WeCom AI Bot | ✅ | |
|
|
||||||
| Personal WeChat | ✅ | |
|
|
||||||
| Lark | ✅ | |
|
|
||||||
| DingTalk | ✅ | |
|
|
||||||
|
|
||||||
### LLMs
|
|
||||||
|
|
||||||
| LLM | Status | Remarks |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| [OpenAI](https://platform.openai.com/) | ✅ | Available for any OpenAI interface format model |
|
|
||||||
| [DeepSeek](https://www.deepseek.com/) | ✅ | |
|
|
||||||
| [Moonshot](https://www.moonshot.cn/) | ✅ | |
|
|
||||||
| [Anthropic](https://www.anthropic.com/) | ✅ | |
|
|
||||||
| [xAI](https://x.ai/) | ✅ | |
|
|
||||||
| [Zhipu AI](https://open.bigmodel.cn/) | ✅ | |
|
|
||||||
| [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | ✅ | LLM and GPU resource platform |
|
|
||||||
| [Dify](https://dify.ai) | ✅ | LLMOps platform |
|
|
||||||
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | LLM and GPU resource platform |
|
|
||||||
| [ShengSuanYun](https://www.shengsuanyun.com/?from=CH_KYIPP758) | ✅ | LLM and GPU resource platform |
|
|
||||||
| [302.AI](https://share.302.ai/SuTG99) | ✅ | LLM gateway(MaaS) |
|
|
||||||
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | ✅ | |
|
|
||||||
| [Ollama](https://ollama.com/) | ✅ | Local LLM running platform |
|
|
||||||
| [LMStudio](https://lmstudio.ai/) | ✅ | Local LLM running platform |
|
|
||||||
| [GiteeAI](https://ai.gitee.com/) | ✅ | LLM interface gateway(MaaS) |
|
|
||||||
| [SiliconFlow](https://siliconflow.cn/) | ✅ | LLM gateway(MaaS) |
|
|
||||||
| [Aliyun Bailian](https://bailian.console.aliyun.com/) | ✅ | LLM gateway(MaaS), LLMOps platform |
|
|
||||||
| [Volc Engine Ark](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | ✅ | LLM gateway(MaaS), LLMOps platform |
|
|
||||||
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | ✅ | LLM gateway(MaaS) |
|
|
||||||
| [MCP](https://modelcontextprotocol.io/) | ✅ | Support tool access through MCP protocol |
|
|
||||||
|
|
||||||
## 🤝 Community Contribution
|
|
||||||
|
|
||||||
Thank you for the following [code contributors](https://github.com/langbot-app/LangBot/graphs/contributors) and other members in the community for their contributions to LangBot:
|
|
||||||
|
|
||||||
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
|
|
||||||
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
|
|
||||||
</a>
|
|
||||||
174
README_ES.md
Normal file
174
README_ES.md
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
<p align="center">
|
||||||
|
<a href="https://langbot.app">
|
||||||
|
<img width="130" src="res/logo-blue.png" alt="LangBot"/>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
<a href="https://www.producthunt.com/products/langbot?utm_source=badge-follow&utm_medium=badge&utm_source=badge-langbot" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/follow.svg?product_id=1077185&theme=light" alt="LangBot - Production-grade IM bot made easy. | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||||
|
|
||||||
|
<h3>Plataforma de grado de producción para construir bots de mensajería instantánea con agentes de IA.</h3>
|
||||||
|
<h4>Construya, depure y despliegue bots de IA rápidamente en Slack, Discord, Telegram, WeChat y más.</h4>
|
||||||
|
|
||||||
|
[English](README.md) / [简体中文](README_CN.md) / [繁體中文](README_TW.md) / [日本語](README_JP.md) / Español / [Français](README_FR.md) / [한국어](README_KO.md) / [Русский](README_RU.md) / [Tiếng Việt](README_VI.md)
|
||||||
|
|
||||||
|
[](https://discord.gg/wdNEHETs87)
|
||||||
|
[](https://deepwiki.com/langbot-app/LangBot)
|
||||||
|
[](https://github.com/langbot-app/LangBot/releases/latest)
|
||||||
|
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
|
||||||
|
[](https://github.com/langbot-app/LangBot/stargazers)
|
||||||
|
|
||||||
|
<a href="https://langbot.app">Inicio</a> |
|
||||||
|
<a href="https://link.langbot.app/en/docs/features">Características</a> |
|
||||||
|
<a href="https://link.langbot.app/en/docs/guide">Documentación</a> |
|
||||||
|
<a href="https://link.langbot.app/en/docs/api">API</a> |
|
||||||
|
<a href="https://space.langbot.app">Mercado de Plugins</a> |
|
||||||
|
<a href="https://langbot.featurebase.app/roadmap">Hoja de Ruta</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ¿Qué es LangBot?
|
||||||
|
|
||||||
|
LangBot es una **plataforma de código abierto y grado de producción** para construir bots de mensajería instantánea impulsados por IA. Conecta modelos de lenguaje de gran escala (LLMs) con cualquier plataforma de chat, permitiéndole crear agentes inteligentes que pueden conversar, ejecutar tareas e integrarse con sus flujos de trabajo existentes.
|
||||||
|
|
||||||
|
### Capacidades Clave
|
||||||
|
|
||||||
|
- **Conversaciones e Agentes IA** — Diálogos de múltiples turnos, llamadas a herramientas, soporte multimodal, salida en streaming. RAG (base de conocimientos) incorporado con integración profunda con [Dify](https://dify.ai), [Coze](https://coze.com), [n8n](https://n8n.io), [Langflow](https://langflow.org).
|
||||||
|
- **Soporte Universal de Plataformas de MI** — Un solo código base para Discord, Telegram, Slack, LINE, QQ, WeChat, WeCom, Lark, DingTalk, KOOK.
|
||||||
|
- **Listo para Producción** — Control de acceso, limitación de velocidad, filtrado de palabras sensibles, monitoreo completo y manejo de excepciones. De confianza para empresas.
|
||||||
|
- **Ecosistema de Plugins** — Cientos de plugins, arquitectura basada en eventos, extensiones de componentes y soporte del [protocolo MCP](https://modelcontextprotocol.io/).
|
||||||
|
- **Panel de Gestión Web** — Configure, gestione y monitoree sus bots a través de una interfaz de navegador intuitiva. Sin necesidad de editar YAML.
|
||||||
|
- **Arquitectura Multi-Pipeline** — Diferentes bots para diferentes escenarios, con monitoreo completo y manejo de excepciones.
|
||||||
|
|
||||||
|
[→ Conocer más sobre todas las funcionalidades](https://link.langbot.app/en/docs/features)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Inicio Rápido
|
||||||
|
|
||||||
|
### ☁️ LangBot Cloud (Recomendado)
|
||||||
|
|
||||||
|
**[LangBot Cloud](https://space.langbot.app/cloud)** — Sin despliegue, listo para usar.
|
||||||
|
|
||||||
|
### Lanzamiento en una línea
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uvx langbot
|
||||||
|
```
|
||||||
|
|
||||||
|
> Requiere [uv](https://docs.astral.sh/uv/getting-started/installation/). Visite http://localhost:5300 — listo.
|
||||||
|
|
||||||
|
### Docker Compose
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/langbot-app/LangBot
|
||||||
|
cd LangBot/docker
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Despliegue en la Nube con un Clic
|
||||||
|
|
||||||
|
[](https://zeabur.com/en-US/templates/ZKTBDH)
|
||||||
|
[](https://railway.app/template/yRrAyL?referralCode=vogKPF)
|
||||||
|
|
||||||
|
**Más opciones:** [Docker](https://link.langbot.app/en/docs/docker) · [Manual](https://link.langbot.app/en/docs/manual-deploy) · [BTPanel](https://link.langbot.app/en/docs/bt-panel) · [Kubernetes](./docker/README_K8S.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Plataformas Soportadas
|
||||||
|
|
||||||
|
| Plataforma | Estado | Notas |
|
||||||
|
|----------|--------|-------|
|
||||||
|
| Discord | ✅ | Oficial |
|
||||||
|
| Telegram | ✅ | Oficial |
|
||||||
|
| Slack | ✅ | Oficial |
|
||||||
|
| LINE | ✅ | Oficial |
|
||||||
|
| QQ | ✅ | Personal y API Oficial (Canal, DM, Grupo) |
|
||||||
|
| WeCom | ✅ | WeChat Empresarial, CS Externo, AI Bot |
|
||||||
|
| WeChat | ✅ | Personal y Cuenta Oficial |
|
||||||
|
| Lark | ✅ | Oficial |
|
||||||
|
| DingTalk | ✅ | Oficial |
|
||||||
|
| KOOK | ✅ | Oficial |
|
||||||
|
| Satori | ✅ | |
|
||||||
|
| Email | ✅ | Matrix, Satori |
|
||||||
|
| Matrix | ✅ | Admite varias plataformas puenteadas como Signal, WhatsApp, Messenger, iMessage, Mattermost, Google Chat, IRC, XMPP, Zulip y más |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## LLMs e Integraciones Soportadas
|
||||||
|
|
||||||
|
| Proveedor | Tipo | Estado |
|
||||||
|
|----------|------|--------|
|
||||||
|
| [OpenAI](https://platform.openai.com/) | LLM | ✅ |
|
||||||
|
| [Anthropic](https://www.anthropic.com/) | LLM | ✅ |
|
||||||
|
| [DeepSeek](https://www.deepseek.com/) | LLM | ✅ |
|
||||||
|
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | LLM | ✅ |
|
||||||
|
| [xAI](https://x.ai/) | LLM | ✅ |
|
||||||
|
| [Moonshot](https://www.moonshot.cn/) | LLM | ✅ |
|
||||||
|
| [Zhipu AI](https://open.bigmodel.cn/) | LLM | ✅ |
|
||||||
|
| [Ollama](https://ollama.com/) | LLM Local | ✅ |
|
||||||
|
| [LM Studio](https://lmstudio.ai/) | LLM Local | ✅ |
|
||||||
|
| [Dify](https://dify.ai) | LLMOps | ✅ |
|
||||||
|
| [MCP](https://modelcontextprotocol.io/) | Protocolo | ✅ |
|
||||||
|
| [SiliconFlow](https://siliconflow.cn/) | Pasarela | ✅ |
|
||||||
|
| [Aliyun Bailian](https://bailian.console.aliyun.com/) | Pasarela | ✅ |
|
||||||
|
| [Volc Engine Ark](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | Pasarela | ✅ |
|
||||||
|
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | Pasarela | ✅ |
|
||||||
|
| [GiteeAI](https://ai.gitee.com/) | Pasarela | ✅ |
|
||||||
|
| [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | Plataforma GPU | ✅ |
|
||||||
|
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | Plataforma GPU | ✅ |
|
||||||
|
| [ShengSuanYun](https://www.shengsuanyun.com/?from=CH_KYIPP758) | Plataforma GPU | ✅ |
|
||||||
|
| [接口 AI](https://jiekou.ai/) | Pasarela | ✅ |
|
||||||
|
| [302.AI](https://share.302.ai/SuTG99) | Pasarela | ✅ |
|
||||||
|
| [Qiniu](https://www.qiniu.com/ai/agent) | Pasarela | ✅ |
|
||||||
|
|
||||||
|
[→ Ver todas las integraciones](https://link.langbot.app/en/docs/features)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ¿Por qué LangBot?
|
||||||
|
|
||||||
|
| Caso de Uso | Cómo Ayuda LangBot |
|
||||||
|
|----------|-------------------|
|
||||||
|
| **Atención al cliente** | Despliegue agentes de IA en Slack/Discord/Telegram que respondan preguntas usando su base de conocimientos |
|
||||||
|
| **Herramientas internas** | Conecte flujos de trabajo de n8n/Dify a WeCom/DingTalk para procesos empresariales automatizados |
|
||||||
|
| **Gestión de comunidades** | Modere grupos de QQ/Discord con filtrado de contenido e interacción impulsados por IA |
|
||||||
|
| **Presencia multiplataforma** | Un solo bot, todas las plataformas. Gestione desde un único panel de control |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Demo en Vivo
|
||||||
|
|
||||||
|
**Pruébelo ahora:** https://demo.langbot.dev/
|
||||||
|
- Correo electrónico: `demo@langbot.app`
|
||||||
|
- Contraseña: `langbot123456`
|
||||||
|
|
||||||
|
*Nota: Entorno de demostración público. No ingrese información confidencial.*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Comunidad
|
||||||
|
|
||||||
|
[](https://discord.gg/wdNEHETs87)
|
||||||
|
|
||||||
|
- [Comunidad de Discord](https://discord.gg/wdNEHETs87)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Historial de Stars
|
||||||
|
|
||||||
|
[](https://star-history.com/#langbot-app/LangBot&Date)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Colaboradores
|
||||||
|
|
||||||
|
Gracias a todos los [colaboradores](https://github.com/langbot-app/LangBot/graphs/contributors) que han ayudado a mejorar LangBot:
|
||||||
|
|
||||||
|
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
|
||||||
|
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
|
||||||
|
</a>
|
||||||
174
README_FR.md
Normal file
174
README_FR.md
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
<p align="center">
|
||||||
|
<a href="https://langbot.app">
|
||||||
|
<img width="130" src="res/logo-blue.png" alt="LangBot"/>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
<a href="https://www.producthunt.com/products/langbot?utm_source=badge-follow&utm_medium=badge&utm_source=badge-langbot" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/follow.svg?product_id=1077185&theme=light" alt="LangBot - Production-grade IM bot made easy. | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||||
|
|
||||||
|
<h3>Plateforme de niveau production pour construire des bots de messagerie instantanée avec agents IA.</h3>
|
||||||
|
<h4>Créez, déboguez et déployez rapidement des bots IA sur Slack, Discord, Telegram, WeChat et plus.</h4>
|
||||||
|
|
||||||
|
[English](README.md) / [简体中文](README_CN.md) / [繁體中文](README_TW.md) / [日本語](README_JP.md) / [Español](README_ES.md) / Français / [한국어](README_KO.md) / [Русский](README_RU.md) / [Tiếng Việt](README_VI.md)
|
||||||
|
|
||||||
|
[](https://discord.gg/wdNEHETs87)
|
||||||
|
[](https://deepwiki.com/langbot-app/LangBot)
|
||||||
|
[](https://github.com/langbot-app/LangBot/releases/latest)
|
||||||
|
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
|
||||||
|
[](https://github.com/langbot-app/LangBot/stargazers)
|
||||||
|
|
||||||
|
<a href="https://langbot.app">Accueil</a> |
|
||||||
|
<a href="https://link.langbot.app/en/docs/features">Fonctionnalités</a> |
|
||||||
|
<a href="https://link.langbot.app/en/docs/guide">Documentation</a> |
|
||||||
|
<a href="https://link.langbot.app/en/docs/api">API</a> |
|
||||||
|
<a href="https://space.langbot.app">Marché des Plugins</a> |
|
||||||
|
<a href="https://langbot.featurebase.app/roadmap">Feuille de Route</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Qu'est-ce que LangBot ?
|
||||||
|
|
||||||
|
LangBot est une **plateforme open-source de niveau production** pour créer des bots de messagerie instantanée alimentés par l'IA. Elle connecte les grands modèles de langage (LLMs) à n'importe quelle plateforme de chat, vous permettant de créer des agents intelligents capables de converser, d'exécuter des tâches et de s'intégrer à vos workflows existants.
|
||||||
|
|
||||||
|
### Capacités Clés
|
||||||
|
|
||||||
|
- **Conversations IA & Agents** — Dialogues multi-tours, appels d'outils, support multimodal, sortie en streaming. RAG (base de connaissances) intégré avec intégration profonde de [Dify](https://dify.ai), [Coze](https://coze.com), [n8n](https://n8n.io), [Langflow](https://langflow.org).
|
||||||
|
- **Support Universel des Plateformes de MI** — Un seul code pour Discord, Telegram, Slack, LINE, QQ, WeChat, WeCom, Lark, DingTalk, KOOK.
|
||||||
|
- **Prêt pour la Production** — Contrôle d'accès, limitation de débit, filtrage de mots sensibles, surveillance complète et gestion des exceptions. Approuvé par les entreprises.
|
||||||
|
- **Écosystème de Plugins** — Des centaines de plugins, architecture événementielle, extensions de composants, et support du [protocole MCP](https://modelcontextprotocol.io/).
|
||||||
|
- **Panneau de Gestion Web** — Configurez, gérez et surveillez vos bots via une interface navigateur intuitive. Aucune édition de YAML requise.
|
||||||
|
- **Architecture Multi-Pipeline** — Différents bots pour différents scénarios, avec surveillance complète et gestion des exceptions.
|
||||||
|
|
||||||
|
[→ En savoir plus sur toutes les fonctionnalités](https://link.langbot.app/en/docs/features)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Démarrage Rapide
|
||||||
|
|
||||||
|
### ☁️ LangBot Cloud (Recommandé)
|
||||||
|
|
||||||
|
**[LangBot Cloud](https://space.langbot.app/cloud)** — Sans déploiement, prêt à utiliser.
|
||||||
|
|
||||||
|
### Lancement en une ligne
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uvx langbot
|
||||||
|
```
|
||||||
|
|
||||||
|
> Nécessite [uv](https://docs.astral.sh/uv/getting-started/installation/). Visitez http://localhost:5300 — c'est prêt.
|
||||||
|
|
||||||
|
### Docker Compose
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/langbot-app/LangBot
|
||||||
|
cd LangBot/docker
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Déploiement Cloud en un Clic
|
||||||
|
|
||||||
|
[](https://zeabur.com/en-US/templates/ZKTBDH)
|
||||||
|
[](https://railway.app/template/yRrAyL?referralCode=vogKPF)
|
||||||
|
|
||||||
|
**Plus d'options :** [Docker](https://link.langbot.app/en/docs/docker) · [Manuel](https://link.langbot.app/en/docs/manual-deploy) · [BTPanel](https://link.langbot.app/en/docs/bt-panel) · [Kubernetes](./docker/README_K8S.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Plateformes Supportées
|
||||||
|
|
||||||
|
| Plateforme | Statut | Notes |
|
||||||
|
|----------|--------|-------|
|
||||||
|
| Discord | ✅ | Officiel |
|
||||||
|
| Telegram | ✅ | Officiel |
|
||||||
|
| Slack | ✅ | Officiel |
|
||||||
|
| LINE | ✅ | Officiel |
|
||||||
|
| QQ | ✅ | Personnel & API Officielle (Canal, DM, Groupe) |
|
||||||
|
| WeCom | ✅ | WeChat Entreprise, CS Externe, AI Bot |
|
||||||
|
| WeChat | ✅ | Personnel & Compte Officiel |
|
||||||
|
| Lark | ✅ | Officiel |
|
||||||
|
| DingTalk | ✅ | Officiel |
|
||||||
|
| KOOK | ✅ | Officiel |
|
||||||
|
| Satori | ✅ | |
|
||||||
|
| Email | ✅ | Matrix, Satori |
|
||||||
|
| Matrix | ✅ | Prend en charge plusieurs plateformes via ponts, comme Signal, WhatsApp, Messenger, iMessage, Mattermost, Google Chat, IRC, XMPP, Zulip, etc. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## LLMs et Intégrations Supportés
|
||||||
|
|
||||||
|
| Fournisseur | Type | Statut |
|
||||||
|
|----------|------|--------|
|
||||||
|
| [OpenAI](https://platform.openai.com/) | LLM | ✅ |
|
||||||
|
| [Anthropic](https://www.anthropic.com/) | LLM | ✅ |
|
||||||
|
| [DeepSeek](https://www.deepseek.com/) | LLM | ✅ |
|
||||||
|
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | LLM | ✅ |
|
||||||
|
| [xAI](https://x.ai/) | LLM | ✅ |
|
||||||
|
| [Moonshot](https://www.moonshot.cn/) | LLM | ✅ |
|
||||||
|
| [Zhipu AI](https://open.bigmodel.cn/) | LLM | ✅ |
|
||||||
|
| [Ollama](https://ollama.com/) | LLM Local | ✅ |
|
||||||
|
| [LM Studio](https://lmstudio.ai/) | LLM Local | ✅ |
|
||||||
|
| [Dify](https://dify.ai) | LLMOps | ✅ |
|
||||||
|
| [MCP](https://modelcontextprotocol.io/) | Protocole | ✅ |
|
||||||
|
| [SiliconFlow](https://siliconflow.cn/) | Passerelle | ✅ |
|
||||||
|
| [Aliyun Bailian](https://bailian.console.aliyun.com/) | Passerelle | ✅ |
|
||||||
|
| [Volc Engine Ark](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | Passerelle | ✅ |
|
||||||
|
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | Passerelle | ✅ |
|
||||||
|
| [GiteeAI](https://ai.gitee.com/) | Passerelle | ✅ |
|
||||||
|
| [接口 AI](https://jiekou.ai/) | Passerelle | ✅ |
|
||||||
|
| [302.AI](https://share.302.ai/SuTG99) | Passerelle | ✅ |
|
||||||
|
| [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | Plateforme GPU | ✅ |
|
||||||
|
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | Plateforme GPU | ✅ |
|
||||||
|
| [ShengSuanYun](https://www.shengsuanyun.com/?from=CH_KYIPP758) | Plateforme GPU | ✅ |
|
||||||
|
| [Qiniu](https://www.qiniu.com/ai/agent) | Passerelle | ✅ |
|
||||||
|
|
||||||
|
[→ Voir toutes les intégrations](https://link.langbot.app/en/docs/features)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pourquoi LangBot ?
|
||||||
|
|
||||||
|
| Cas d'Usage | Comment LangBot Aide |
|
||||||
|
|----------|-------------------|
|
||||||
|
| **Support Client** | Déployez des agents IA sur Slack/Discord/Telegram qui répondent aux questions en utilisant votre base de connaissances |
|
||||||
|
| **Outils Internes** | Connectez les workflows n8n/Dify à WeCom/DingTalk pour automatiser vos processus métier |
|
||||||
|
| **Gestion de Communauté** | Modérez les groupes QQ/Discord avec un filtrage de contenu et des interactions alimentés par l'IA |
|
||||||
|
| **Présence Multi-plateforme** | Un seul bot, toutes les plateformes. Gérez tout depuis un tableau de bord unique |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Démo en Ligne
|
||||||
|
|
||||||
|
**Essayez maintenant :** https://demo.langbot.dev/
|
||||||
|
- Email : `demo@langbot.app`
|
||||||
|
- Mot de passe : `langbot123456`
|
||||||
|
|
||||||
|
*Note : Environnement de démonstration public. Ne saisissez pas d'informations sensibles.*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Communauté
|
||||||
|
|
||||||
|
[](https://discord.gg/wdNEHETs87)
|
||||||
|
|
||||||
|
- [Communauté Discord](https://discord.gg/wdNEHETs87)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Historique des Stars
|
||||||
|
|
||||||
|
[](https://star-history.com/#langbot-app/LangBot&Date)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Contributeurs
|
||||||
|
|
||||||
|
Merci à tous les [contributeurs](https://github.com/langbot-app/LangBot/graphs/contributors) qui ont aidé à améliorer LangBot :
|
||||||
|
|
||||||
|
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
|
||||||
|
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
|
||||||
|
</a>
|
||||||
214
README_JP.md
214
README_JP.md
@@ -1,31 +1,68 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://langbot.app">
|
<a href="https://langbot.app">
|
||||||
<img src="https://docs.langbot.app/social_en.png" alt="LangBot"/>
|
<img width="130" src="res/logo-blue.png" alt="LangBot"/>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
[English](README_EN.md) / [简体中文](README.md) / [繁體中文](README_TW.md) / 日本語 / (PR for your language)
|
<a href="https://www.producthunt.com/products/langbot?utm_source=badge-follow&utm_medium=badge&utm_source=badge-langbot" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/follow.svg?product_id=1077185&theme=light" alt="LangBot - Production-grade IM bot made easy. | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||||
|
|
||||||
|
<h3>AIエージェント搭載IMボットを構築するための本番グレードプラットフォーム。</h3>
|
||||||
|
<h4>Slack、Discord、Telegram、WeChat などに AI ボットを素早く構築、デバッグ、デプロイ。</h4>
|
||||||
|
|
||||||
|
[English](README.md) / [简体中文](README_CN.md) / [繁體中文](README_TW.md) / 日本語 / [Español](README_ES.md) / [Français](README_FR.md) / [한국어](README_KO.md) / [Русский](README_RU.md) / [Tiếng Việt](README_VI.md)
|
||||||
|
|
||||||
[](https://discord.gg/wdNEHETs87)
|
[](https://discord.gg/wdNEHETs87)
|
||||||
[](https://deepwiki.com/langbot-app/LangBot)
|
[](https://deepwiki.com/langbot-app/LangBot)
|
||||||
[](https://github.com/langbot-app/LangBot/releases/latest)
|
[](https://github.com/langbot-app/LangBot/releases/latest)
|
||||||
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
|
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
|
||||||
|
[](https://github.com/langbot-app/LangBot/stargazers)
|
||||||
|
|
||||||
<a href="https://langbot.app">ホーム</a> |
|
<a href="https://langbot.app">ホーム</a> |
|
||||||
<a href="https://docs.langbot.app/en/insight/guide.html">デプロイ</a> |
|
<a href="https://link.langbot.app/ja/docs/features">機能</a> |
|
||||||
<a href="https://docs.langbot.app/en/plugin/plugin-intro.html">プラグイン</a> |
|
<a href="https://link.langbot.app/ja/docs/guide">ドキュメント</a> |
|
||||||
<a href="https://github.com/langbot-app/LangBot/issues/new?assignees=&labels=%E7%8B%AC%E7%AB%8B%E6%8F%92%E4%BB%B6&projects=&template=submit-plugin.yml&title=%5BPlugin%5D%3A+%E8%AF%B7%E6%B1%82%E7%99%BB%E8%AE%B0%E6%96%B0%E6%8F%92%E4%BB%B6">プラグインの提出</a>
|
<a href="https://link.langbot.app/ja/docs/api">API</a> |
|
||||||
|
<a href="https://space.langbot.app">プラグインマーケット</a> |
|
||||||
|
<a href="https://langbot.featurebase.app/roadmap">ロードマップ</a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
LangBot は、エージェント、RAG、MCP などの LLM アプリケーション機能を備えた、オープンソースの LLM ネイティブのインスタントメッセージングロボット開発プラットフォームです。世界中のインスタントメッセージングプラットフォームに適応し、豊富な API インターフェースを提供し、カスタム開発をサポートします。
|
---
|
||||||
|
|
||||||
## 📦 始め方
|
## LangBot とは?
|
||||||
|
|
||||||
#### Docker Compose デプロイ
|
LangBot は、AI搭載のインスタントメッセージングボットを構築するための**オープンソースの本番グレードプラットフォーム**です。大規模言語モデル(LLM)をあらゆるチャットプラットフォームに接続し、会話、タスク実行、既存のワークフローとの統合が可能なインテリジェントエージェントを作成できます。
|
||||||
|
|
||||||
|
### 主な機能
|
||||||
|
|
||||||
|
- **AI対話とエージェント** — マルチターン対話、ツール呼び出し、マルチモーダル対応、ストリーミング出力。RAG(ナレッジベース)を内蔵し、[Dify](https://dify.ai)、[Coze](https://coze.com)、[n8n](https://n8n.io)、[Langflow](https://langflow.org) と深く統合。
|
||||||
|
- **ユニバーサルIMプラットフォーム対応** — 単一のコードベースで Discord、Telegram、Slack、LINE、QQ、WeChat、WeCom、Lark、DingTalk、KOOK に対応。
|
||||||
|
- **本番環境対応** — アクセス制御、レート制限、センシティブワードフィルタリング、包括的な監視、例外処理を搭載。エンタープライズの信頼に応える品質。
|
||||||
|
- **プラグインエコシステム** — 数百のプラグイン、イベント駆動アーキテクチャ、コンポーネント拡張、[MCPプロトコル](https://modelcontextprotocol.io/)対応。
|
||||||
|
- **Web管理パネル** — 直感的なブラウザインターフェースからボットの設定、管理、監視が可能。YAML編集は不要。
|
||||||
|
- **マルチパイプラインアーキテクチャ** — 異なるシナリオに異なるボットを配置し、包括的な監視と例外処理を実現。
|
||||||
|
|
||||||
|
[→ すべての機能について詳しく見る](https://link.langbot.app/ja/docs/features)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## クイックスタート
|
||||||
|
|
||||||
|
### ☁️ LangBot Cloud(推奨)
|
||||||
|
|
||||||
|
**[LangBot Cloud](https://space.langbot.app/cloud)** — デプロイ不要、すぐに使えます。
|
||||||
|
|
||||||
|
### ワンライン起動
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uvx langbot
|
||||||
|
```
|
||||||
|
|
||||||
|
> [uv](https://docs.astral.sh/uv/getting-started/installation/) が必要です。http://localhost:5300 にアクセスして完了。
|
||||||
|
|
||||||
|
### Docker Compose
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/langbot-app/LangBot
|
git clone https://github.com/langbot-app/LangBot
|
||||||
@@ -33,93 +70,104 @@ cd LangBot/docker
|
|||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
http://localhost:5300 にアクセスして使用を開始します。
|
### ワンクリッククラウドデプロイ
|
||||||
|
|
||||||
詳細なドキュメントは[Dockerデプロイ](https://docs.langbot.app/en/deploy/langbot/docker.html)を参照してください。
|
|
||||||
|
|
||||||
#### Panelでのワンクリックデプロイ
|
|
||||||
|
|
||||||
LangBotはBTPanelにリストされています。BTPanelをインストールしている場合は、[ドキュメント](https://docs.langbot.app/en/deploy/langbot/one-click/bt.html)を使用して使用できます。
|
|
||||||
|
|
||||||
#### Zeaburクラウドデプロイ
|
|
||||||
|
|
||||||
コミュニティが提供するZeaburテンプレート。
|
|
||||||
|
|
||||||
[](https://zeabur.com/en-US/templates/ZKTBDH)
|
[](https://zeabur.com/en-US/templates/ZKTBDH)
|
||||||
|
|
||||||
#### Railwayクラウドデプロイ
|
|
||||||
|
|
||||||
[](https://railway.app/template/yRrAyL?referralCode=vogKPF)
|
[](https://railway.app/template/yRrAyL?referralCode=vogKPF)
|
||||||
|
|
||||||
#### その他のデプロイ方法
|
**その他:** [Docker](https://link.langbot.app/en/docs/docker) · [手動デプロイ](https://link.langbot.app/en/docs/manual-deploy) · [BTPanel](https://link.langbot.app/en/docs/bt-panel) · [Kubernetes](./docker/README_K8S.md)
|
||||||
|
|
||||||
リリースバージョンを直接使用して実行します。[手動デプロイ](https://docs.langbot.app/en/deploy/langbot/manual.html)のドキュメントを参照してください。
|
---
|
||||||
|
|
||||||
## 😎 最新情報を入手
|
## 対応プラットフォーム
|
||||||
|
|
||||||
リポジトリの右上にある Star と Watch ボタンをクリックして、最新の更新を取得してください。
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## ✨ 機能
|
|
||||||
|
|
||||||
- 💬 LLM / エージェントとのチャット: 複数のLLMをサポートし、グループチャットとプライベートチャットに対応。マルチラウンドの会話、ツールの呼び出し、マルチモーダル、ストリーミング出力機能をサポート、RAG(知識ベース)を組み込み、[Dify](https://dify.ai) と深く統合。
|
|
||||||
- 🤖 多プラットフォーム対応: 現在、QQ、QQ チャンネル、WeChat、個人 WeChat、Lark、DingTalk、Discord、Telegram など、複数のプラットフォームをサポートしています。
|
|
||||||
- 🛠️ 高い安定性、豊富な機能: ネイティブのアクセス制御、レート制限、敏感な単語のフィルタリングなどのメカニズムをサポート。使いやすく、複数のデプロイ方法をサポート。複数のパイプライン設定をサポートし、異なるボットを異なる用途に使用できます。
|
|
||||||
- 🧩 プラグイン拡張、活発なコミュニティ: イベント駆動、コンポーネント拡張などのプラグインメカニズムをサポート。適配 Anthropic [MCP プロトコル](https://modelcontextprotocol.io/);豊富なエコシステム、現在数百のプラグインが存在。
|
|
||||||
- 😻 Web UI: ブラウザを通じてLangBotインスタンスを管理することをサポート。
|
|
||||||
|
|
||||||
詳細な仕様については、[ドキュメント](https://docs.langbot.app/en/insight/features.html)を参照してください。
|
|
||||||
|
|
||||||
または、デモ環境にアクセスしてください: https://demo.langbot.dev/
|
|
||||||
- ログイン情報: メール: `demo@langbot.app` パスワード: `langbot123456`
|
|
||||||
- 注意: WebUI のデモンストレーションのみの場合、公開環境では機密情報を入力しないでください。
|
|
||||||
|
|
||||||
### メッセージプラットフォーム
|
|
||||||
|
|
||||||
| プラットフォーム | ステータス | 備考 |
|
| プラットフォーム | ステータス | 備考 |
|
||||||
| --- | --- | --- |
|
|----------|--------|-------|
|
||||||
| Discord | ✅ | |
|
| Discord | ✅ | 公式 |
|
||||||
| Telegram | ✅ | |
|
| Telegram | ✅ | 公式 |
|
||||||
| Slack | ✅ | |
|
| Slack | ✅ | 公式 |
|
||||||
| LINE | ✅ | |
|
| LINE | ✅ | 公式 |
|
||||||
| 個人QQ | ✅ | |
|
| QQ | ✅ | 個人・公式API(チャンネル・DM・グループ) |
|
||||||
| QQ公式API | ✅ | |
|
| WeCom | ✅ | 企業WeChat、外部CS、AIボット |
|
||||||
| WeCom | ✅ | |
|
| WeChat | ✅ | 個人・公式アカウント |
|
||||||
| WeComCS | ✅ | |
|
| Lark | ✅ | 公式 |
|
||||||
| WeCom AI Bot | ✅ | |
|
| DingTalk | ✅ | 公式 |
|
||||||
| 個人WeChat | ✅ | |
|
| KOOK | ✅ | 公式 |
|
||||||
| Lark | ✅ | |
|
| Satori | ✅ | |
|
||||||
| DingTalk | ✅ | |
|
| Email | ✅ | Matrix、Satori |
|
||||||
|
| Matrix | ✅ | Signal、WhatsApp、Messenger、iMessage、Mattermost、Google Chat、IRC、XMPP、Zulip など複数のブリッジ先プラットフォームに対応 |
|
||||||
|
|
||||||
### LLMs
|
---
|
||||||
|
|
||||||
| LLM | ステータス | 備考 |
|
## 対応LLMと統合
|
||||||
| --- | --- | --- |
|
|
||||||
| [OpenAI](https://platform.openai.com/) | ✅ | 任意のOpenAIインターフェース形式モデルに対応 |
|
|
||||||
| [DeepSeek](https://www.deepseek.com/) | ✅ | |
|
|
||||||
| [Moonshot](https://www.moonshot.cn/) | ✅ | |
|
|
||||||
| [Anthropic](https://www.anthropic.com/) | ✅ | |
|
|
||||||
| [xAI](https://x.ai/) | ✅ | |
|
|
||||||
| [Zhipu AI](https://open.bigmodel.cn/) | ✅ | |
|
|
||||||
| [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | ✅ | 大模型とGPUリソースプラットフォーム |
|
|
||||||
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | 大模型とGPUリソースプラットフォーム |
|
|
||||||
| [ShengSuanYun](https://www.shengsuanyun.com/?from=CH_KYIPP758) | ✅ | LLMとGPUリソースプラットフォーム |
|
|
||||||
| [302.AI](https://share.302.ai/SuTG99) | ✅ | LLMゲートウェイ(MaaS) |
|
|
||||||
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | ✅ | |
|
|
||||||
| [Dify](https://dify.ai) | ✅ | LLMOpsプラットフォーム |
|
|
||||||
| [Ollama](https://ollama.com/) | ✅ | ローカルLLM実行プラットフォーム |
|
|
||||||
| [LMStudio](https://lmstudio.ai/) | ✅ | ローカルLLM実行プラットフォーム |
|
|
||||||
| [GiteeAI](https://ai.gitee.com/) | ✅ | LLMインターフェースゲートウェイ(MaaS) |
|
|
||||||
| [SiliconFlow](https://siliconflow.cn/) | ✅ | LLMゲートウェイ(MaaS) |
|
|
||||||
| [Aliyun Bailian](https://bailian.console.aliyun.com/) | ✅ | LLMゲートウェイ(MaaS), LLMOpsプラットフォーム |
|
|
||||||
| [Volc Engine Ark](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | ✅ | LLMゲートウェイ(MaaS), LLMOpsプラットフォーム |
|
|
||||||
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | ✅ | LLMゲートウェイ(MaaS) |
|
|
||||||
| [MCP](https://modelcontextprotocol.io/) | ✅ | MCPプロトコルをサポート |
|
|
||||||
|
|
||||||
## 🤝 コミュニティ貢献
|
| プロバイダー | タイプ | ステータス |
|
||||||
|
|----------|------|--------|
|
||||||
|
| [OpenAI](https://platform.openai.com/) | LLM | ✅ |
|
||||||
|
| [Anthropic](https://www.anthropic.com/) | LLM | ✅ |
|
||||||
|
| [DeepSeek](https://www.deepseek.com/) | LLM | ✅ |
|
||||||
|
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | LLM | ✅ |
|
||||||
|
| [xAI](https://x.ai/) | LLM | ✅ |
|
||||||
|
| [Moonshot](https://www.moonshot.cn/) | LLM | ✅ |
|
||||||
|
| [Zhipu AI](https://open.bigmodel.cn/) | LLM | ✅ |
|
||||||
|
| [Ollama](https://ollama.com/) | ローカルLLM | ✅ |
|
||||||
|
| [LM Studio](https://lmstudio.ai/) | ローカルLLM | ✅ |
|
||||||
|
| [Dify](https://dify.ai) | LLMOps | ✅ |
|
||||||
|
| [MCP](https://modelcontextprotocol.io/) | プロトコル | ✅ |
|
||||||
|
| [SiliconFlow](https://siliconflow.cn/) | ゲートウェイ | ✅ |
|
||||||
|
| [Aliyun Bailian](https://bailian.console.aliyun.com/) | ゲートウェイ | ✅ |
|
||||||
|
| [Volc Engine Ark](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | ゲートウェイ | ✅ |
|
||||||
|
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | ゲートウェイ | ✅ |
|
||||||
|
| [GiteeAI](https://ai.gitee.com/) | ゲートウェイ | ✅ |
|
||||||
|
| [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | GPUプラットフォーム | ✅ |
|
||||||
|
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | GPUプラットフォーム | ✅ |
|
||||||
|
| [ShengSuanYun](https://www.shengsuanyun.com/?from=CH_KYIPP758) | GPUプラットフォーム | ✅ |
|
||||||
|
| [接口 AI](https://jiekou.ai/) | ゲートウェイ | ✅ |
|
||||||
|
| [302.AI](https://share.302.ai/SuTG99) | ゲートウェイ | ✅ |
|
||||||
|
| [Qiniu](https://www.qiniu.com/ai/agent) | ゲートウェイ | ✅ |
|
||||||
|
|
||||||
LangBot への貢献に対して、以下の [コード貢献者](https://github.com/langbot-app/LangBot/graphs/contributors) とコミュニティの他のメンバーに感謝します。
|
[→ すべての統合を表示](https://link.langbot.app/en/docs/features)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## なぜ LangBot?
|
||||||
|
|
||||||
|
| ユースケース | LangBot の活用方法 |
|
||||||
|
|----------|-------------------|
|
||||||
|
| **カスタマーサポート** | ナレッジベースを活用して質問に回答するAIエージェントをSlack/Discord/Telegramにデプロイ |
|
||||||
|
| **社内ツール** | n8n/Difyのワークフローを WeCom/DingTalk に接続し、業務プロセスを自動化 |
|
||||||
|
| **コミュニティ管理** | AI搭載のコンテンツフィルタリングとインタラクションでQQ/Discordグループをモデレーション |
|
||||||
|
| **マルチプラットフォーム展開** | 1つのボットで全プラットフォームに対応。単一のダッシュボードから管理 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ライブデモ
|
||||||
|
|
||||||
|
**今すぐ試す:** https://demo.langbot.dev/
|
||||||
|
- メール: `demo@langbot.app`
|
||||||
|
- パスワード: `langbot123456`
|
||||||
|
|
||||||
|
*注意: 公開デモ環境です。機密情報を入力しないでください。*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## コミュニティ
|
||||||
|
|
||||||
|
[](https://discord.gg/wdNEHETs87)
|
||||||
|
|
||||||
|
- [Discord コミュニティ](https://discord.gg/wdNEHETs87)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Star 推移
|
||||||
|
|
||||||
|
[](https://star-history.com/#langbot-app/LangBot&Date)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## コントリビューター
|
||||||
|
|
||||||
|
LangBot をより良くするために貢献してくださったすべての[コントリビューター](https://github.com/langbot-app/LangBot/graphs/contributors)に感謝します:
|
||||||
|
|
||||||
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
|
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
|
||||||
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
|
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
|
||||||
|
|||||||
174
README_KO.md
Normal file
174
README_KO.md
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
<p align="center">
|
||||||
|
<a href="https://langbot.app">
|
||||||
|
<img width="130" src="res/logo-blue.png" alt="LangBot"/>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
<a href="https://www.producthunt.com/products/langbot?utm_source=badge-follow&utm_medium=badge&utm_source=badge-langbot" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/follow.svg?product_id=1077185&theme=light" alt="LangBot - Production-grade IM bot made easy. | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||||
|
|
||||||
|
<h3>AI 에이전트 IM 봇 구축을 위한 프로덕션 등급 플랫폼.</h3>
|
||||||
|
<h4>Slack, Discord, Telegram, WeChat 등에 AI 봇을 빠르게 구축, 디버그 및 배포.</h4>
|
||||||
|
|
||||||
|
[English](README.md) / [简体中文](README_CN.md) / [繁體中文](README_TW.md) / [日本語](README_JP.md) / [Español](README_ES.md) / [Français](README_FR.md) / 한국어 / [Русский](README_RU.md) / [Tiếng Việt](README_VI.md)
|
||||||
|
|
||||||
|
[](https://discord.gg/wdNEHETs87)
|
||||||
|
[](https://deepwiki.com/langbot-app/LangBot)
|
||||||
|
[](https://github.com/langbot-app/LangBot/releases/latest)
|
||||||
|
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
|
||||||
|
[](https://github.com/langbot-app/LangBot/stargazers)
|
||||||
|
|
||||||
|
<a href="https://langbot.app">홈</a> |
|
||||||
|
<a href="https://link.langbot.app/en/docs/features">기능</a> |
|
||||||
|
<a href="https://link.langbot.app/en/docs/guide">문서</a> |
|
||||||
|
<a href="https://link.langbot.app/en/docs/api">API</a> |
|
||||||
|
<a href="https://space.langbot.app">플러그인 마켓</a> |
|
||||||
|
<a href="https://langbot.featurebase.app/roadmap">로드맵</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## LangBot이란?
|
||||||
|
|
||||||
|
LangBot은 AI 기반 인스턴트 메시징 봇을 구축하기 위한 **오픈소스 프로덕션 등급 플랫폼**입니다. 대규모 언어 모델(LLM)을 모든 채팅 플랫폼에 연결하여 대화, 작업 실행, 기존 워크플로우와의 통합이 가능한 지능형 에이전트를 만들 수 있습니다.
|
||||||
|
|
||||||
|
### 핵심 기능
|
||||||
|
|
||||||
|
- **AI 대화 및 에이전트** — 멀티턴 대화, 도구 호출, 멀티모달 지원, 스트리밍 출력. 내장 RAG(지식 베이스)와 [Dify](https://dify.ai), [Coze](https://coze.com), [n8n](https://n8n.io), [Langflow](https://langflow.org) 심층 통합.
|
||||||
|
- **유니버설 IM 플랫폼 지원** — 단일 코드베이스로 Discord, Telegram, Slack, LINE, QQ, WeChat, WeCom, Lark, DingTalk, KOOK 지원.
|
||||||
|
- **프로덕션 레디** — 접근 제어, 속도 제한, 민감어 필터링, 종합 모니터링 및 예외 처리. 기업 환경에서 검증됨.
|
||||||
|
- **플러그인 생태계** — 수백 개의 플러그인, 이벤트 기반 아키텍처, 컴포넌트 확장, [MCP 프로토콜](https://modelcontextprotocol.io/) 지원.
|
||||||
|
- **웹 관리 패널** — 직관적인 브라우저 인터페이스로 봇을 구성, 관리 및 모니터링. YAML 편집 불필요.
|
||||||
|
- **멀티 파이프라인 아키텍처** — 다양한 시나리오에 맞는 다양한 봇 구성, 종합 모니터링 및 예외 처리.
|
||||||
|
|
||||||
|
[→ 모든 기능 자세히 보기](https://link.langbot.app/en/docs/features)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 빠른 시작
|
||||||
|
|
||||||
|
### ☁️ LangBot Cloud (추천)
|
||||||
|
|
||||||
|
**[LangBot Cloud](https://space.langbot.app/cloud)** — 배포 없이 바로 사용.
|
||||||
|
|
||||||
|
### 원라인 실행
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uvx langbot
|
||||||
|
```
|
||||||
|
|
||||||
|
> [uv](https://docs.astral.sh/uv/getting-started/installation/) 설치 필요. http://localhost:5300 방문 — 완료.
|
||||||
|
|
||||||
|
### Docker Compose
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/langbot-app/LangBot
|
||||||
|
cd LangBot/docker
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### 원클릭 클라우드 배포
|
||||||
|
|
||||||
|
[](https://zeabur.com/en-US/templates/ZKTBDH)
|
||||||
|
[](https://railway.app/template/yRrAyL?referralCode=vogKPF)
|
||||||
|
|
||||||
|
**더 많은 옵션:** [Docker](https://link.langbot.app/en/docs/docker) · [수동 배포](https://link.langbot.app/en/docs/manual-deploy) · [BTPanel](https://link.langbot.app/en/docs/bt-panel) · [Kubernetes](./docker/README_K8S.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 지원 플랫폼
|
||||||
|
|
||||||
|
| 플랫폼 | 상태 | 비고 |
|
||||||
|
|--------|------|------|
|
||||||
|
| Discord | ✅ | 공식 |
|
||||||
|
| Telegram | ✅ | 공식 |
|
||||||
|
| Slack | ✅ | 공식 |
|
||||||
|
| LINE | ✅ | 공식 |
|
||||||
|
| QQ | ✅ | 개인 및 공식 API (채널, DM, 그룹) |
|
||||||
|
| WeCom | ✅ | 기업 WeChat, 외부 CS, AI Bot |
|
||||||
|
| WeChat | ✅ | 개인 및 공식 계정 |
|
||||||
|
| Lark | ✅ | 공식 |
|
||||||
|
| DingTalk | ✅ | 공식 |
|
||||||
|
| KOOK | ✅ | 공식 |
|
||||||
|
| Satori | ✅ | |
|
||||||
|
| Email | ✅ | Matrix, Satori |
|
||||||
|
| Matrix | ✅ | Signal, WhatsApp, Messenger, iMessage, Mattermost, Google Chat, IRC, XMPP, Zulip 등 여러 브리지 플랫폼 지원 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 지원 LLM 및 통합
|
||||||
|
|
||||||
|
| 제공자 | 유형 | 상태 |
|
||||||
|
|--------|------|------|
|
||||||
|
| [OpenAI](https://platform.openai.com/) | LLM | ✅ |
|
||||||
|
| [Anthropic](https://www.anthropic.com/) | LLM | ✅ |
|
||||||
|
| [DeepSeek](https://www.deepseek.com/) | LLM | ✅ |
|
||||||
|
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | LLM | ✅ |
|
||||||
|
| [xAI](https://x.ai/) | LLM | ✅ |
|
||||||
|
| [Moonshot](https://www.moonshot.cn/) | LLM | ✅ |
|
||||||
|
| [Zhipu AI](https://open.bigmodel.cn/) | LLM | ✅ |
|
||||||
|
| [Ollama](https://ollama.com/) | 로컬 LLM | ✅ |
|
||||||
|
| [LM Studio](https://lmstudio.ai/) | 로컬 LLM | ✅ |
|
||||||
|
| [Dify](https://dify.ai) | LLMOps | ✅ |
|
||||||
|
| [MCP](https://modelcontextprotocol.io/) | 프로토콜 | ✅ |
|
||||||
|
| [SiliconFlow](https://siliconflow.cn/) | 게이트웨이 | ✅ |
|
||||||
|
| [Aliyun Bailian](https://bailian.console.aliyun.com/) | 게이트웨이 | ✅ |
|
||||||
|
| [Volc Engine Ark](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | 게이트웨이 | ✅ |
|
||||||
|
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | 게이트웨이 | ✅ |
|
||||||
|
| [GiteeAI](https://ai.gitee.com/) | 게이트웨이 | ✅ |
|
||||||
|
| [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | GPU 플랫폼 | ✅ |
|
||||||
|
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | GPU 플랫폼 | ✅ |
|
||||||
|
| [ShengSuanYun](https://www.shengsuanyun.com/?from=CH_KYIPP758) | GPU 플랫폼 | ✅ |
|
||||||
|
| [接口 AI](https://jiekou.ai/) | 게이트웨이 | ✅ |
|
||||||
|
| [302.AI](https://share.302.ai/SuTG99) | 게이트웨이 | ✅ |
|
||||||
|
| [Qiniu](https://www.qiniu.com/ai/agent) | 게이트웨이 | ✅ |
|
||||||
|
|
||||||
|
[→ 모든 통합 보기](https://link.langbot.app/en/docs/features)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 왜 LangBot인가?
|
||||||
|
|
||||||
|
| 사용 사례 | LangBot 활용 방법 |
|
||||||
|
|-----------|-------------------|
|
||||||
|
| **고객 지원** | 지식 베이스를 활용하여 질문에 답변하는 AI 에이전트를 Slack/Discord/Telegram에 배포 |
|
||||||
|
| **내부 도구** | n8n/Dify 워크플로우를 WeCom/DingTalk에 연결하여 비즈니스 프로세스 자동화 |
|
||||||
|
| **커뮤니티 관리** | AI 기반 콘텐츠 필터링 및 상호작용으로 QQ/Discord 그룹 관리 |
|
||||||
|
| **멀티 플랫폼** | 하나의 봇으로 모든 플랫폼 지원. 단일 대시보드에서 관리 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 라이브 데모
|
||||||
|
|
||||||
|
**지금 체험:** https://demo.langbot.dev/
|
||||||
|
- 이메일: `demo@langbot.app`
|
||||||
|
- 비밀번호: `langbot123456`
|
||||||
|
|
||||||
|
*참고: 공개 데모 환경입니다. 민감한 정보를 입력하지 마세요.*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 커뮤니티
|
||||||
|
|
||||||
|
[](https://discord.gg/wdNEHETs87)
|
||||||
|
|
||||||
|
- [Discord 커뮤니티](https://discord.gg/wdNEHETs87)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Star 추이
|
||||||
|
|
||||||
|
[](https://star-history.com/#langbot-app/LangBot&Date)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 기여자
|
||||||
|
|
||||||
|
LangBot을 더 나은 프로젝트로 만들어 주신 모든 [기여자](https://github.com/langbot-app/LangBot/graphs/contributors)분들께 감사드립니다:
|
||||||
|
|
||||||
|
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
|
||||||
|
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
|
||||||
|
</a>
|
||||||
174
README_RU.md
Normal file
174
README_RU.md
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
<p align="center">
|
||||||
|
<a href="https://langbot.app">
|
||||||
|
<img width="130" src="res/logo-blue.png" alt="LangBot"/>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
<a href="https://www.producthunt.com/products/langbot?utm_source=badge-follow&utm_medium=badge&utm_source=badge-langbot" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/follow.svg?product_id=1077185&theme=light" alt="LangBot - Production-grade IM bot made easy. | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||||
|
|
||||||
|
<h3>Платформа производственного уровня для создания агентных IM-ботов.</h3>
|
||||||
|
<h4>Быстро создавайте, отлаживайте и развертывайте ИИ-ботов в Slack, Discord, Telegram, WeChat и других платформах.</h4>
|
||||||
|
|
||||||
|
[English](README.md) / [简体中文](README_CN.md) / [繁體中文](README_TW.md) / [日本語](README_JP.md) / [Español](README_ES.md) / [Français](README_FR.md) / [한국어](README_KO.md) / Русский / [Tiếng Việt](README_VI.md)
|
||||||
|
|
||||||
|
[](https://discord.gg/wdNEHETs87)
|
||||||
|
[](https://deepwiki.com/langbot-app/LangBot)
|
||||||
|
[](https://github.com/langbot-app/LangBot/releases/latest)
|
||||||
|
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
|
||||||
|
[](https://github.com/langbot-app/LangBot/stargazers)
|
||||||
|
|
||||||
|
<a href="https://langbot.app">Главная</a> |
|
||||||
|
<a href="https://link.langbot.app/en/docs/features">Возможности</a> |
|
||||||
|
<a href="https://link.langbot.app/en/docs/guide">Документация</a> |
|
||||||
|
<a href="https://link.langbot.app/en/docs/api">API</a> |
|
||||||
|
<a href="https://space.langbot.app">Магазин плагинов</a> |
|
||||||
|
<a href="https://langbot.featurebase.app/roadmap">Дорожная карта</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Что такое LangBot?
|
||||||
|
|
||||||
|
LangBot — это **платформа с открытым исходным кодом производственного уровня** для создания ИИ-ботов в мессенджерах. Она связывает большие языковые модели (LLM) с любой чат-платформой, позволяя создавать интеллектуальных агентов, которые могут вести диалоги, выполнять задачи и интегрироваться с вашими существующими рабочими процессами.
|
||||||
|
|
||||||
|
### Ключевые возможности
|
||||||
|
|
||||||
|
- **ИИ-диалоги и агенты** — Многораундовые диалоги, вызов инструментов, мультимодальная поддержка, потоковый вывод. Встроенная реализация RAG (база знаний) с глубокой интеграцией в [Dify](https://dify.ai), [Coze](https://coze.com), [n8n](https://n8n.io), [Langflow](https://langflow.org).
|
||||||
|
- **Универсальная поддержка IM-платформ** — Единая кодовая база для Discord, Telegram, Slack, LINE, QQ, WeChat, WeCom, Lark, DingTalk, KOOK.
|
||||||
|
- **Готовность к продакшену** — Контроль доступа, ограничение скорости, фильтрация чувствительных слов, комплексный мониторинг и обработка исключений. Проверено в корпоративной среде.
|
||||||
|
- **Экосистема плагинов** — Сотни плагинов, событийно-ориентированная архитектура, расширения компонентов и поддержка [протокола MCP](https://modelcontextprotocol.io/).
|
||||||
|
- **Веб-панель управления** — Настраивайте, управляйте и мониторьте ваших ботов через интуитивный браузерный интерфейс. Ручное редактирование YAML не требуется.
|
||||||
|
- **Мультиконвейерная архитектура** — Разные боты для разных сценариев с комплексным мониторингом и обработкой исключений.
|
||||||
|
|
||||||
|
[→ Подробнее обо всех возможностях](https://link.langbot.app/en/docs/features)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Быстрый старт
|
||||||
|
|
||||||
|
### ☁️ LangBot Cloud (Рекомендуется)
|
||||||
|
|
||||||
|
**[LangBot Cloud](https://space.langbot.app/cloud)** — Без развёртывания, готово к использованию.
|
||||||
|
|
||||||
|
### Запуск одной командой
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uvx langbot
|
||||||
|
```
|
||||||
|
|
||||||
|
> Требуется [uv](https://docs.astral.sh/uv/getting-started/installation/). Откройте http://localhost:5300 — готово.
|
||||||
|
|
||||||
|
### Docker Compose
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/langbot-app/LangBot
|
||||||
|
cd LangBot/docker
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Облачное развертывание одним кликом
|
||||||
|
|
||||||
|
[](https://zeabur.com/en-US/templates/ZKTBDH)
|
||||||
|
[](https://railway.app/template/yRrAyL?referralCode=vogKPF)
|
||||||
|
|
||||||
|
**Другие варианты:** [Docker](https://link.langbot.app/en/docs/docker) · [Ручная установка](https://link.langbot.app/en/docs/manual-deploy) · [BTPanel](https://link.langbot.app/en/docs/bt-panel) · [Kubernetes](./docker/README_K8S.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Поддерживаемые платформы
|
||||||
|
|
||||||
|
| Платформа | Статус | Примечания |
|
||||||
|
|-----------|--------|------------|
|
||||||
|
| Discord | ✅ | Официальный |
|
||||||
|
| Telegram | ✅ | Официальный |
|
||||||
|
| Slack | ✅ | Официальный |
|
||||||
|
| LINE | ✅ | Официальный |
|
||||||
|
| QQ | ✅ | Личный и официальный API (Канал, ЛС, Группа) |
|
||||||
|
| WeCom | ✅ | Корпоративный WeChat, внешний CS, AI-бот |
|
||||||
|
| WeChat | ✅ | Личный и официальный аккаунт |
|
||||||
|
| Lark | ✅ | Официальный |
|
||||||
|
| DingTalk | ✅ | Официальный |
|
||||||
|
| KOOK | ✅ | Официальный |
|
||||||
|
| Satori | ✅ | |
|
||||||
|
| Email | ✅ | Matrix, Satori |
|
||||||
|
| Matrix | ✅ | Поддерживает несколько платформ через мосты, включая Signal, WhatsApp, Messenger, iMessage, Mattermost, Google Chat, IRC, XMPP, Zulip и другие |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Поддерживаемые LLM и интеграции
|
||||||
|
|
||||||
|
| Провайдер | Тип | Статус |
|
||||||
|
|-----------|-----|--------|
|
||||||
|
| [OpenAI](https://platform.openai.com/) | LLM | ✅ |
|
||||||
|
| [Anthropic](https://www.anthropic.com/) | LLM | ✅ |
|
||||||
|
| [DeepSeek](https://www.deepseek.com/) | LLM | ✅ |
|
||||||
|
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | LLM | ✅ |
|
||||||
|
| [xAI](https://x.ai/) | LLM | ✅ |
|
||||||
|
| [Moonshot](https://www.moonshot.cn/) | LLM | ✅ |
|
||||||
|
| [Zhipu AI](https://open.bigmodel.cn/) | LLM | ✅ |
|
||||||
|
| [Ollama](https://ollama.com/) | Локальный LLM | ✅ |
|
||||||
|
| [LM Studio](https://lmstudio.ai/) | Локальный LLM | ✅ |
|
||||||
|
| [Dify](https://dify.ai) | LLMOps | ✅ |
|
||||||
|
| [MCP](https://modelcontextprotocol.io/) | Протокол | ✅ |
|
||||||
|
| [SiliconFlow](https://siliconflow.cn/) | Шлюз | ✅ |
|
||||||
|
| [Aliyun Bailian](https://bailian.console.aliyun.com/) | Шлюз | ✅ |
|
||||||
|
| [Volc Engine Ark](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | Шлюз | ✅ |
|
||||||
|
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | Шлюз | ✅ |
|
||||||
|
| [GiteeAI](https://ai.gitee.com/) | Шлюз | ✅ |
|
||||||
|
| [302.AI](https://share.302.ai/SuTG99) | Шлюз | ✅ |
|
||||||
|
| [接口 AI](https://jiekou.ai/) | Шлюз | ✅ |
|
||||||
|
| [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | Платформа GPU | ✅ |
|
||||||
|
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | Платформа GPU | ✅ |
|
||||||
|
| [ShengSuanYun](https://www.shengsuanyun.com/?from=CH_KYIPP758) | Платформа GPU | ✅ |
|
||||||
|
| [Qiniu](https://www.qiniu.com/ai/agent) | Шлюз | ✅ |
|
||||||
|
|
||||||
|
[→ Смотреть все интеграции](https://link.langbot.app/en/docs/features)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Почему LangBot?
|
||||||
|
|
||||||
|
| Сценарий использования | Как помогает LangBot |
|
||||||
|
|------------------------|----------------------|
|
||||||
|
| **Поддержка клиентов** | Разверните ИИ-агентов в Slack/Discord/Telegram, которые отвечают на вопросы, используя вашу базу знаний |
|
||||||
|
| **Внутренние инструменты** | Подключите рабочие процессы n8n/Dify к WeCom/DingTalk для автоматизации бизнес-процессов |
|
||||||
|
| **Управление сообществом** | Модерируйте группы QQ/Discord с помощью ИИ-фильтрации контента и взаимодействия |
|
||||||
|
| **Мультиплатформенное присутствие** | Один бот — все платформы. Управляйте из единой панели |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Демо
|
||||||
|
|
||||||
|
**Попробуйте прямо сейчас:** https://demo.langbot.dev/
|
||||||
|
- Email: `demo@langbot.app`
|
||||||
|
- Пароль: `langbot123456`
|
||||||
|
|
||||||
|
*Примечание: Публичная демо-среда. Не вводите конфиденциальную информацию.*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Сообщество
|
||||||
|
|
||||||
|
[](https://discord.gg/wdNEHETs87)
|
||||||
|
|
||||||
|
- [Сообщество Discord](https://discord.gg/wdNEHETs87)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## История Stars
|
||||||
|
|
||||||
|
[](https://star-history.com/#langbot-app/LangBot&Date)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Участники
|
||||||
|
|
||||||
|
Спасибо всем [участникам](https://github.com/langbot-app/LangBot/graphs/contributors), которые помогли сделать LangBot лучше:
|
||||||
|
|
||||||
|
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
|
||||||
|
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
|
||||||
|
</a>
|
||||||
230
README_TW.md
230
README_TW.md
@@ -1,33 +1,70 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://langbot.app">
|
<a href="https://langbot.app">
|
||||||
<img src="https://docs.langbot.app/social_zh.png" alt="LangBot"/>
|
<img width="130" src="res/logo-blue.png" alt="LangBot"/>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div align="center"><a href="https://hellogithub.com/repository/langbot-app/LangBot" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=5ce8ae2aa4f74316bf393b57b952433c&claim_uid=gtmc6YWjMZkT21R" alt="Featured|HelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
<div align="center">
|
||||||
|
|
||||||
[English](README_EN.md) / [简体中文](README.md) / 繁體中文 / [日本語](README_JP.md) / (PR for your language)
|
<a href="https://hellogithub.com/repository/langbot-app/LangBot" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=5ce8ae2aa4f74316bf393b57b952433c&claim_uid=gtmc6YWjMZkT21R" alt="Featured|HelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||||
|
|
||||||
|
<h3>生產級 AI 即時通訊機器人開發平台。</h3>
|
||||||
|
<h4>快速建構、除錯和部署 AI 機器人到微信、QQ、飛書、Slack、Discord、Telegram 等平台。</h4>
|
||||||
|
|
||||||
|
[English](README.md) / [简体中文](README_CN.md) / 繁體中文 / [日本語](README_JP.md) / [Español](README_ES.md) / [Français](README_FR.md) / [한국어](README_KO.md) / [Русский](README_RU.md) / [Tiếng Việt](README_VI.md)
|
||||||
|
|
||||||
[](https://discord.gg/wdNEHETs87)
|
[](https://discord.gg/wdNEHETs87)
|
||||||
[](https://qm.qq.com/q/JLi38whHum)
|
[](https://qm.qq.com/q/JLi38whHum)
|
||||||
[](https://deepwiki.com/langbot-app/LangBot)
|
[](https://deepwiki.com/langbot-app/LangBot)
|
||||||
[](https://github.com/langbot-app/LangBot/releases/latest)
|
[](https://github.com/langbot-app/LangBot/releases/latest)
|
||||||
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
|
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
|
||||||
|
[](https://github.com/langbot-app/LangBot/stargazers)
|
||||||
[](https://gitcode.com/RockChinQ/LangBot)
|
[](https://gitcode.com/RockChinQ/LangBot)
|
||||||
|
|
||||||
<a href="https://langbot.app">主頁</a> |
|
<a href="https://langbot.app">官網</a> |
|
||||||
<a href="https://docs.langbot.app/zh/insight/guide.html">部署文件</a> |
|
<a href="https://link.langbot.app/zh/docs/features">特性</a> |
|
||||||
<a href="https://docs.langbot.app/zh/plugin/plugin-intro.html">外掛介紹</a> |
|
<a href="https://link.langbot.app/zh/docs/guide">文件</a> |
|
||||||
<a href="https://github.com/langbot-app/LangBot/issues/new?assignees=&labels=%E7%8B%AC%E7%AB%8B%E6%8F%92%E4%BB%B6&projects=&template=submit-plugin.yml&title=%5BPlugin%5D%3A+%E8%AF%B7%E6%B1%82%E7%99%BB%E8%AE%B0%E6%96%B0%E6%8F%92%E4%BB%B6">提交外掛</a>
|
<a href="https://link.langbot.app/zh/docs/api">API</a> |
|
||||||
|
<a href="https://space.langbot.app">外掛市場</a> |
|
||||||
|
<a href="https://langbot.featurebase.app/roadmap">路線圖</a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
LangBot 是一個開源的大語言模型原生即時通訊機器人開發平台,旨在提供開箱即用的 IM 機器人開發體驗,具有 Agent、RAG、MCP 等多種 LLM 應用功能,適配全球主流即時通訊平台,並提供豐富的 API 介面,支援自定義開發。
|
---
|
||||||
|
|
||||||
## 📦 開始使用
|
## 什麼是 LangBot?
|
||||||
|
|
||||||
#### Docker Compose 部署
|
LangBot 是一個**開源的生產級平台**,用於建構 AI 驅動的即時通訊機器人。它將大語言模型(LLM)連接到各種聊天平台,幫助你創建能夠對話、執行任務、並整合到現有工作流程中的智能 Agent。
|
||||||
|
|
||||||
|
### 核心能力
|
||||||
|
|
||||||
|
- **AI 對話與 Agent** — 多輪對話、工具調用、多模態、流式輸出。自帶 RAG(知識庫),深度整合 [Dify](https://dify.ai)、[Coze](https://coze.com)、[n8n](https://n8n.io)、[Langflow](https://langflow.org) 等 LLMOps 平台。
|
||||||
|
- **全平台支援** — 一套程式碼,覆蓋 QQ、微信、企業微信、飛書、釘釘、Discord、Telegram、Slack、LINE、KOOK 等平台。
|
||||||
|
- **生產就緒** — 存取控制、限速、敏感詞過濾、全面監控與異常處理,已被多家企業採用。
|
||||||
|
- **外掛生態** — 數百個外掛,事件驅動架構,組件擴展,適配 [MCP 協議](https://modelcontextprotocol.io/)。
|
||||||
|
- **Web 管理面板** — 透過瀏覽器直觀地配置、管理和監控機器人,無需手動編輯設定檔。
|
||||||
|
- **多流水線架構** — 不同機器人用於不同場景,具備全面的監控和異常處理能力。
|
||||||
|
|
||||||
|
[→ 了解更多功能特性](https://link.langbot.app/zh/docs/features)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 快速開始
|
||||||
|
|
||||||
|
### ☁️ LangBot Cloud(推薦)
|
||||||
|
|
||||||
|
**[LangBot Cloud](https://space.langbot.app/cloud)** — 免部署,開箱即用。
|
||||||
|
|
||||||
|
### 一鍵啟動
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uvx langbot
|
||||||
|
```
|
||||||
|
|
||||||
|
> 需要安裝 [uv](https://docs.astral.sh/uv/getting-started/installation/)。訪問 http://localhost:5300 即可使用。
|
||||||
|
|
||||||
|
### Docker Compose
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/langbot-app/LangBot
|
git clone https://github.com/langbot-app/LangBot
|
||||||
@@ -35,94 +72,66 @@ cd LangBot/docker
|
|||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
訪問 http://localhost:5300 即可開始使用。
|
### 一鍵雲端部署
|
||||||
|
|
||||||
詳細文件[Docker 部署](https://docs.langbot.app/zh/deploy/langbot/docker.html)。
|
|
||||||
|
|
||||||
#### 寶塔面板部署
|
|
||||||
|
|
||||||
已上架寶塔面板,若您已安裝寶塔面板,可以根據[文件](https://docs.langbot.app/zh/deploy/langbot/one-click/bt.html)使用。
|
|
||||||
|
|
||||||
#### Zeabur 雲端部署
|
|
||||||
|
|
||||||
社群貢獻的 Zeabur 模板。
|
|
||||||
|
|
||||||
[](https://zeabur.com/zh-CN/templates/ZKTBDH)
|
[](https://zeabur.com/zh-CN/templates/ZKTBDH)
|
||||||
|
|
||||||
#### Railway 雲端部署
|
|
||||||
|
|
||||||
[](https://railway.app/template/yRrAyL?referralCode=vogKPF)
|
[](https://railway.app/template/yRrAyL?referralCode=vogKPF)
|
||||||
|
|
||||||
#### 手動部署
|
**更多方式:** [Docker](https://link.langbot.app/zh/docs/docker) · [手動部署](https://link.langbot.app/zh/docs/manual-deploy) · [寶塔面板](https://link.langbot.app/zh/docs/bt-panel) · [Kubernetes](./docker/README_K8S.md)
|
||||||
|
|
||||||
直接使用發行版運行,查看文件[手動部署](https://docs.langbot.app/zh/deploy/langbot/manual.html)。
|
---
|
||||||
|
|
||||||
## 😎 保持更新
|
## 支援的平台
|
||||||
|
|
||||||
點擊倉庫右上角 Star 和 Watch 按鈕,獲取最新動態。
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## ✨ 特性
|
|
||||||
|
|
||||||
- 💬 大模型對話、Agent:支援多種大模型,適配群聊和私聊;具有多輪對話、工具調用、多模態、流式輸出能力,自帶 RAG(知識庫)實現,並深度適配 [Dify](https://dify.ai)。
|
|
||||||
- 🤖 多平台支援:目前支援 QQ、QQ頻道、企業微信、個人微信、飛書、Discord、Telegram 等平台。
|
|
||||||
- 🛠️ 高穩定性、功能完備:原生支援訪問控制、限速、敏感詞過濾等機制;配置簡單,支援多種部署方式。支援多流水線配置,不同機器人用於不同應用場景。
|
|
||||||
- 🧩 外掛擴展、活躍社群:支援事件驅動、組件擴展等外掛機制;適配 Anthropic [MCP 協議](https://modelcontextprotocol.io/);目前已有數百個外掛。
|
|
||||||
- 😻 Web 管理面板:支援通過瀏覽器管理 LangBot 實例,不再需要手動編寫配置文件。
|
|
||||||
|
|
||||||
詳細規格特性請訪問[文件](https://docs.langbot.app/zh/insight/features.html)。
|
|
||||||
|
|
||||||
或訪問 demo 環境:https://demo.langbot.dev/
|
|
||||||
- 登入資訊:郵箱:`demo@langbot.app` 密碼:`langbot123456`
|
|
||||||
- 注意:僅展示 WebUI 效果,公開環境,請不要在其中填入您的任何敏感資訊。
|
|
||||||
|
|
||||||
### 訊息平台
|
|
||||||
|
|
||||||
| 平台 | 狀態 | 備註 |
|
| 平台 | 狀態 | 備註 |
|
||||||
| --- | --- | --- |
|
|------|------|------|
|
||||||
| Discord | ✅ | |
|
| Discord | ✅ | 官方 |
|
||||||
| Telegram | ✅ | |
|
| Telegram | ✅ | 官方 |
|
||||||
| Slack | ✅ | |
|
| Slack | ✅ | 官方 |
|
||||||
| LINE | ✅ | |
|
| LINE | ✅ | 官方 |
|
||||||
| QQ 個人號 | ✅ | QQ 個人號私聊、群聊 |
|
| QQ | ✅ | 個人號、官方機器人(頻道、私聊、群聊) |
|
||||||
| QQ 官方機器人 | ✅ | QQ 官方機器人,支援頻道、私聊、群聊 |
|
| 企業微信 | ✅ | 應用訊息、對外客服、智能機器人 |
|
||||||
| 微信 | ✅ | |
|
| 微信 | ✅ | 個人微信、微信公眾號 |
|
||||||
| 企微對外客服 | ✅ | |
|
| 飛書 | ✅ | 官方 |
|
||||||
| 企微智能機器人 | ✅ | |
|
| 釘釘 | ✅ | 官方 |
|
||||||
| 微信公眾號 | ✅ | |
|
| KOOK | ✅ | 官方 |
|
||||||
| Lark | ✅ | |
|
| Satori | ✅ | |
|
||||||
| DingTalk | ✅ | |
|
| Email | ✅ | 只 Matrix、Satori |
|
||||||
|
| Matrix | ✅ | 支援多種橋接平台,如 Signal、WhatsApp、Messenger、iMessage、Mattermost、Google Chat、IRC、XMPP、Zulip 等 |
|
||||||
|
|
||||||
### 大模型能力
|
---
|
||||||
|
|
||||||
| 模型 | 狀態 | 備註 |
|
## 支援的大模型與整合
|
||||||
| --- | --- | --- |
|
|
||||||
| [OpenAI](https://platform.openai.com/) | ✅ | 可接入任何 OpenAI 介面格式模型 |
|
|
||||||
| [DeepSeek](https://www.deepseek.com/) | ✅ | |
|
|
||||||
| [Moonshot](https://www.moonshot.cn/) | ✅ | |
|
|
||||||
| [Anthropic](https://www.anthropic.com/) | ✅ | |
|
|
||||||
| [xAI](https://x.ai/) | ✅ | |
|
|
||||||
| [智譜AI](https://open.bigmodel.cn/) | ✅ | |
|
|
||||||
| [勝算雲](https://www.shengsuanyun.com/?from=CH_KYIPP758) | ✅ | 大模型和 GPU 資源平台 |
|
|
||||||
| [優雲智算](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | ✅ | 大模型和 GPU 資源平台 |
|
|
||||||
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | 大模型和 GPU 資源平台 |
|
|
||||||
| [302.AI](https://share.302.ai/SuTG99) | ✅ | 大模型聚合平台 |
|
|
||||||
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | ✅ | |
|
|
||||||
| [Dify](https://dify.ai) | ✅ | LLMOps 平台 |
|
|
||||||
| [Ollama](https://ollama.com/) | ✅ | 本地大模型運行平台 |
|
|
||||||
| [LMStudio](https://lmstudio.ai/) | ✅ | 本地大模型運行平台 |
|
|
||||||
| [GiteeAI](https://ai.gitee.com/) | ✅ | 大模型介面聚合平台 |
|
|
||||||
| [SiliconFlow](https://siliconflow.cn/) | ✅ | 大模型聚合平台 |
|
|
||||||
| [阿里雲百煉](https://bailian.console.aliyun.com/) | ✅ | 大模型聚合平台, LLMOps 平台 |
|
|
||||||
| [火山方舟](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | ✅ | 大模型聚合平台, LLMOps 平台 |
|
|
||||||
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | ✅ | 大模型聚合平台 |
|
|
||||||
| [MCP](https://modelcontextprotocol.io/) | ✅ | 支援通過 MCP 協議獲取工具 |
|
|
||||||
|
|
||||||
### TTS
|
| 提供商 | 類型 | 狀態 |
|
||||||
|
|--------|------|------|
|
||||||
|
| [OpenAI](https://platform.openai.com/) | LLM | ✅ |
|
||||||
|
| [Anthropic](https://www.anthropic.com/) | LLM | ✅ |
|
||||||
|
| [DeepSeek](https://www.deepseek.com/) | LLM | ✅ |
|
||||||
|
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | LLM | ✅ |
|
||||||
|
| [xAI](https://x.ai/) | LLM | ✅ |
|
||||||
|
| [Moonshot](https://www.moonshot.cn/) | LLM | ✅ |
|
||||||
|
| [智譜AI](https://open.bigmodel.cn/) | LLM | ✅ |
|
||||||
|
| [Ollama](https://ollama.com/) | 本地 LLM | ✅ |
|
||||||
|
| [LM Studio](https://lmstudio.ai/) | 本地 LLM | ✅ |
|
||||||
|
| [Dify](https://dify.ai) | LLMOps | ✅ |
|
||||||
|
| [MCP](https://modelcontextprotocol.io/) | 協議 | ✅ |
|
||||||
|
| [SiliconFlow](https://siliconflow.cn/) | 聚合平台 | ✅ |
|
||||||
|
| [阿里雲百煉](https://bailian.console.aliyun.com/) | 聚合平台 | ✅ |
|
||||||
|
| [火山方舟](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | 聚合平台 | ✅ |
|
||||||
|
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | 聚合平台 | ✅ |
|
||||||
|
| [GiteeAI](https://ai.gitee.com/) | 聚合平台 | ✅ |
|
||||||
|
| [勝算雲](https://www.shengsuanyun.com/?from=CH_KYIPP758) | GPU 平台 | ✅ |
|
||||||
|
| [優雲智算](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | GPU 平台 | ✅ |
|
||||||
|
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | GPU 平台 | ✅ |
|
||||||
|
| [接口 AI](https://jiekou.ai/) | 聚合平台 | ✅ |
|
||||||
|
| [302.AI](https://share.302.ai/SuTG99) | 聚合平台 | ✅ |
|
||||||
|
| [Qiniu](https://www.qiniu.com/ai/agent) | 聚合平台 | ✅ |
|
||||||
|
|
||||||
|
### TTS(語音合成)
|
||||||
|
|
||||||
| 平台/模型 | 備註 |
|
| 平台/模型 | 備註 |
|
||||||
| --- | --- |
|
|-----------|------|
|
||||||
| [FishAudio](https://fish.audio/zh-CN/discovery/) | [外掛](https://github.com/the-lazy-me/NewChatVoice) |
|
| [FishAudio](https://fish.audio/zh-CN/discovery/) | [外掛](https://github.com/the-lazy-me/NewChatVoice) |
|
||||||
| [海豚 AI](https://www.ttson.cn/?source=thelazy) | [外掛](https://github.com/the-lazy-me/NewChatVoice) |
|
| [海豚 AI](https://www.ttson.cn/?source=thelazy) | [外掛](https://github.com/the-lazy-me/NewChatVoice) |
|
||||||
| [AzureTTS](https://portal.azure.com/) | [外掛](https://github.com/Ingnaryk/LangBot_AzureTTS) |
|
| [AzureTTS](https://portal.azure.com/) | [外掛](https://github.com/Ingnaryk/LangBot_AzureTTS) |
|
||||||
@@ -130,13 +139,54 @@ docker compose up -d
|
|||||||
### 文生圖
|
### 文生圖
|
||||||
|
|
||||||
| 平台/模型 | 備註 |
|
| 平台/模型 | 備註 |
|
||||||
| --- | --- |
|
|-----------|------|
|
||||||
| 阿里雲百煉 | [外掛](https://github.com/Thetail001/LangBot_BailianTextToImagePlugin)
|
| 阿里雲百煉 | [外掛](https://github.com/Thetail001/LangBot_BailianTextToImagePlugin) |
|
||||||
|
|
||||||
## 😘 社群貢獻
|
[→ 查看完整整合列表](https://link.langbot.app/zh/docs/features)
|
||||||
|
|
||||||
感謝以下[程式碼貢獻者](https://github.com/langbot-app/LangBot/graphs/contributors)和社群裡其他成員對 LangBot 的貢獻:
|
---
|
||||||
|
|
||||||
|
## 為什麼選擇 LangBot?
|
||||||
|
|
||||||
|
| 使用場景 | LangBot 如何幫助 |
|
||||||
|
|----------|------------------|
|
||||||
|
| **客戶服務** | 將 AI Agent 部署到微信/企微/釘釘/飛書,基於知識庫自動回答使用者問題 |
|
||||||
|
| **內部工具** | 將 n8n/Dify 工作流接入企微/釘釘,實現業務流程自動化 |
|
||||||
|
| **社群運營** | 在 QQ/Discord 群中使用 AI 驅動的內容審核與智能互動 |
|
||||||
|
| **多平台觸達** | 一個機器人,覆蓋所有平台。透過統一面板集中管理 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 線上演示
|
||||||
|
|
||||||
|
**立即體驗:** https://demo.langbot.dev/
|
||||||
|
- 信箱:`demo@langbot.app`
|
||||||
|
- 密碼:`langbot123456`
|
||||||
|
|
||||||
|
*注意:公開演示環境,請不要在其中填入任何敏感資訊。*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 社群
|
||||||
|
|
||||||
|
[](https://discord.gg/wdNEHETs87)
|
||||||
|
[](https://qm.qq.com/q/JLi38whHum)
|
||||||
|
|
||||||
|
- [Discord 社群](https://discord.gg/wdNEHETs87)
|
||||||
|
- [QQ 社群群](https://qm.qq.com/q/JLi38whHum)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Star 趨勢
|
||||||
|
|
||||||
|
[](https://star-history.com/#langbot-app/LangBot&Date)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 貢獻者
|
||||||
|
|
||||||
|
感謝所有[貢獻者](https://github.com/langbot-app/LangBot/graphs/contributors)對 LangBot 的幫助:
|
||||||
|
|
||||||
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
|
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
|
||||||
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
|
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
174
README_VI.md
Normal file
174
README_VI.md
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
<p align="center">
|
||||||
|
<a href="https://langbot.app">
|
||||||
|
<img width="130" src="res/logo-blue.png" alt="LangBot"/>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
<a href="https://www.producthunt.com/products/langbot?utm_source=badge-follow&utm_medium=badge&utm_source=badge-langbot" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/follow.svg?product_id=1077185&theme=light" alt="LangBot - Production-grade IM bot made easy. | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||||
|
|
||||||
|
<h3>Nền tảng cấp sản xuất để xây dựng bot IM với AI agent.</h3>
|
||||||
|
<h4>Xây dựng, gỡ lỗi và triển khai bot AI nhanh chóng trên Slack, Discord, Telegram, WeChat và nhiều nền tảng khác.</h4>
|
||||||
|
|
||||||
|
[English](README.md) / [简体中文](README_CN.md) / [繁體中文](README_TW.md) / [日本語](README_JP.md) / [Español](README_ES.md) / [Français](README_FR.md) / [한국어](README_KO.md) / [Русский](README_RU.md) / Tiếng Việt
|
||||||
|
|
||||||
|
[](https://discord.gg/wdNEHETs87)
|
||||||
|
[](https://deepwiki.com/langbot-app/LangBot)
|
||||||
|
[](https://github.com/langbot-app/LangBot/releases/latest)
|
||||||
|
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
|
||||||
|
[](https://github.com/langbot-app/LangBot/stargazers)
|
||||||
|
|
||||||
|
<a href="https://langbot.app">Trang chủ</a> |
|
||||||
|
<a href="https://link.langbot.app/en/docs/features">Tính năng</a> |
|
||||||
|
<a href="https://link.langbot.app/en/docs/guide">Tài liệu</a> |
|
||||||
|
<a href="https://link.langbot.app/en/docs/api">API</a> |
|
||||||
|
<a href="https://space.langbot.app">Chợ Plugin</a> |
|
||||||
|
<a href="https://langbot.featurebase.app/roadmap">Lộ trình</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## LangBot là gì?
|
||||||
|
|
||||||
|
LangBot là một **nền tảng mã nguồn mở, cấp sản xuất** để xây dựng bot nhắn tin tức thời được hỗ trợ bởi AI. Nó kết nối các Mô hình Ngôn ngữ Lớn (LLM) với bất kỳ nền tảng chat nào, cho phép bạn tạo các agent thông minh có thể trò chuyện, thực hiện tác vụ và tích hợp với quy trình làm việc hiện có của bạn.
|
||||||
|
|
||||||
|
### Khả năng chính
|
||||||
|
|
||||||
|
- **Hội thoại AI & Agent** — Đối thoại nhiều lượt, gọi công cụ, hỗ trợ đa phương thức, đầu ra streaming. RAG (cơ sở kiến thức) tích hợp sẵn với tích hợp sâu vào [Dify](https://dify.ai), [Coze](https://coze.com), [n8n](https://n8n.io), [Langflow](https://langflow.org).
|
||||||
|
- **Hỗ trợ đa nền tảng IM** — Một mã nguồn cho Discord, Telegram, Slack, LINE, QQ, WeChat, WeCom, Lark, DingTalk, KOOK.
|
||||||
|
- **Sẵn sàng cho sản xuất** — Kiểm soát truy cập, giới hạn tốc độ, lọc từ nhạy cảm, giám sát toàn diện và xử lý ngoại lệ. Được doanh nghiệp tin dùng.
|
||||||
|
- **Hệ sinh thái Plugin** — Hàng trăm plugin, kiến trúc hướng sự kiện, mở rộng thành phần, và hỗ trợ [giao thức MCP](https://modelcontextprotocol.io/).
|
||||||
|
- **Bảng quản lý Web** — Cấu hình, quản lý và giám sát bot thông qua giao diện trình duyệt trực quan. Không cần chỉnh sửa YAML.
|
||||||
|
- **Kiến trúc đa Pipeline** — Các bot khác nhau cho các kịch bản khác nhau, với giám sát toàn diện và xử lý ngoại lệ.
|
||||||
|
|
||||||
|
[→ Tìm hiểu thêm về tất cả tính năng](https://link.langbot.app/en/docs/features)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bắt đầu nhanh
|
||||||
|
|
||||||
|
### ☁️ LangBot Cloud (Khuyên dùng)
|
||||||
|
|
||||||
|
**[LangBot Cloud](https://space.langbot.app/cloud)** — Không cần triển khai, sẵn sàng sử dụng.
|
||||||
|
|
||||||
|
### Khởi chạy một dòng
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uvx langbot
|
||||||
|
```
|
||||||
|
|
||||||
|
> Yêu cầu [uv](https://docs.astral.sh/uv/getting-started/installation/). Truy cập http://localhost:5300 — xong.
|
||||||
|
|
||||||
|
### Docker Compose
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/langbot-app/LangBot
|
||||||
|
cd LangBot/docker
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Triển khai đám mây một cú nhấp
|
||||||
|
|
||||||
|
[](https://zeabur.com/en-US/templates/ZKTBDH)
|
||||||
|
[](https://railway.app/template/yRrAyL?referralCode=vogKPF)
|
||||||
|
|
||||||
|
**Thêm tùy chọn:** [Docker](https://link.langbot.app/en/docs/docker) · [Thủ công](https://link.langbot.app/en/docs/manual-deploy) · [BTPanel](https://link.langbot.app/en/docs/bt-panel) · [Kubernetes](./docker/README_K8S.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Nền tảng được hỗ trợ
|
||||||
|
|
||||||
|
| Nền tảng | Trạng thái | Ghi chú |
|
||||||
|
|----------|--------|-------|
|
||||||
|
| Discord | ✅ | Chính thức |
|
||||||
|
| Telegram | ✅ | Chính thức |
|
||||||
|
| Slack | ✅ | Chính thức |
|
||||||
|
| LINE | ✅ | Chính thức |
|
||||||
|
| QQ | ✅ | Cá nhân & API chính thức (Kênh, DM, Nhóm) |
|
||||||
|
| WeCom | ✅ | WeChat doanh nghiệp, CS bên ngoài, AI Bot |
|
||||||
|
| WeChat | ✅ | Cá nhân & Tài khoản công khai |
|
||||||
|
| Lark | ✅ | Chính thức |
|
||||||
|
| DingTalk | ✅ | Chính thức |
|
||||||
|
| KOOK | ✅ | Chính thức |
|
||||||
|
| Satori | ✅ | |
|
||||||
|
| Email | ✅ | Matrix, Satori |
|
||||||
|
| Matrix | ✅ | Hỗ trợ nhiều nền tảng qua bridge như Signal, WhatsApp, Messenger, iMessage, Mattermost, Google Chat, IRC, XMPP, Zulip và hơn thế nữa |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## LLM và tích hợp được hỗ trợ
|
||||||
|
|
||||||
|
| Nhà cung cấp | Loại | Trạng thái |
|
||||||
|
|----------|------|--------|
|
||||||
|
| [OpenAI](https://platform.openai.com/) | LLM | ✅ |
|
||||||
|
| [Anthropic](https://www.anthropic.com/) | LLM | ✅ |
|
||||||
|
| [DeepSeek](https://www.deepseek.com/) | LLM | ✅ |
|
||||||
|
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | LLM | ✅ |
|
||||||
|
| [xAI](https://x.ai/) | LLM | ✅ |
|
||||||
|
| [Moonshot](https://www.moonshot.cn/) | LLM | ✅ |
|
||||||
|
| [Zhipu AI](https://open.bigmodel.cn/) | LLM | ✅ |
|
||||||
|
| [Ollama](https://ollama.com/) | LLM cục bộ | ✅ |
|
||||||
|
| [LM Studio](https://lmstudio.ai/) | LLM cục bộ | ✅ |
|
||||||
|
| [Dify](https://dify.ai) | LLMOps | ✅ |
|
||||||
|
| [MCP](https://modelcontextprotocol.io/) | Giao thức | ✅ |
|
||||||
|
| [SiliconFlow](https://siliconflow.cn/) | Cổng | ✅ |
|
||||||
|
| [Aliyun Bailian](https://bailian.console.aliyun.com/) | Cổng | ✅ |
|
||||||
|
| [Volc Engine Ark](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | Cổng | ✅ |
|
||||||
|
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | Cổng | ✅ |
|
||||||
|
| [GiteeAI](https://ai.gitee.com/) | Cổng | ✅ |
|
||||||
|
| [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | Nền tảng GPU | ✅ |
|
||||||
|
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | Nền tảng GPU | ✅ |
|
||||||
|
| [ShengSuanYun](https://www.shengsuanyun.com/?from=CH_KYIPP758) | Nền tảng GPU | ✅ |
|
||||||
|
| [接口 AI](https://jiekou.ai/) | Cổng | ✅ |
|
||||||
|
| [302.AI](https://share.302.ai/SuTG99) | Cổng | ✅ |
|
||||||
|
| [Qiniu](https://www.qiniu.com/ai/agent) | Cổng | ✅ |
|
||||||
|
|
||||||
|
[→ Xem tất cả tích hợp](https://link.langbot.app/en/docs/features)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tại sao chọn LangBot?
|
||||||
|
|
||||||
|
| Trường hợp sử dụng | LangBot giúp như thế nào |
|
||||||
|
|----------|-------------------|
|
||||||
|
| **Hỗ trợ khách hàng** | Triển khai agent AI trên Slack/Discord/Telegram để trả lời câu hỏi bằng cơ sở kiến thức của bạn |
|
||||||
|
| **Công cụ nội bộ** | Kết nối quy trình n8n/Dify với WeCom/DingTalk để tự động hóa quy trình kinh doanh |
|
||||||
|
| **Quản lý cộng đồng** | Quản lý nhóm QQ/Discord với tính năng lọc nội dung và tương tác được hỗ trợ bởi AI |
|
||||||
|
| **Đa nền tảng** | Một bot, tất cả nền tảng. Quản lý từ một bảng điều khiển duy nhất |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Demo trực tuyến
|
||||||
|
|
||||||
|
**Thử ngay:** https://demo.langbot.dev/
|
||||||
|
- Email: `demo@langbot.app`
|
||||||
|
- Mật khẩu: `langbot123456`
|
||||||
|
|
||||||
|
*Lưu ý: Môi trường demo công khai. Không nhập thông tin nhạy cảm.*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cộng đồng
|
||||||
|
|
||||||
|
[](https://discord.gg/wdNEHETs87)
|
||||||
|
|
||||||
|
- [Cộng đồng Discord](https://discord.gg/wdNEHETs87)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Lịch sử Star
|
||||||
|
|
||||||
|
[](https://star-history.com/#langbot-app/LangBot&Date)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Người đóng góp
|
||||||
|
|
||||||
|
Cảm ơn tất cả [người đóng góp](https://github.com/langbot-app/LangBot/graphs/contributors) đã giúp LangBot trở nên tốt hơn:
|
||||||
|
|
||||||
|
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
|
||||||
|
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
|
||||||
|
</a>
|
||||||
629
docker/README_K8S.md
Normal file
629
docker/README_K8S.md
Normal file
@@ -0,0 +1,629 @@
|
|||||||
|
# LangBot Kubernetes 部署指南 / Kubernetes Deployment Guide
|
||||||
|
|
||||||
|
[简体中文](#简体中文) | [English](#english)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 简体中文
|
||||||
|
|
||||||
|
### 概述
|
||||||
|
|
||||||
|
本指南提供了在 Kubernetes 集群中部署 LangBot 的完整步骤。Kubernetes 部署配置基于 `docker-compose.yaml`,适用于生产环境的容器化部署。
|
||||||
|
|
||||||
|
### 前置要求
|
||||||
|
|
||||||
|
- Kubernetes 集群(版本 1.19+)
|
||||||
|
- `kubectl` 命令行工具已配置并可访问集群
|
||||||
|
- 集群中有可用的存储类(StorageClass)用于持久化存储(可选但推荐)
|
||||||
|
- 至少 2 vCPU 和 4GB RAM 的可用资源
|
||||||
|
|
||||||
|
### 架构说明
|
||||||
|
|
||||||
|
Kubernetes 部署包含以下组件:
|
||||||
|
|
||||||
|
1. **langbot**: 主应用服务
|
||||||
|
- 提供 Web UI(端口 5300)
|
||||||
|
- 处理平台 webhook(端口 2280-2290)
|
||||||
|
- 数据持久化卷
|
||||||
|
|
||||||
|
2. **langbot-plugin-runtime**: 插件运行时服务
|
||||||
|
- WebSocket 通信(端口 5400)
|
||||||
|
- 插件数据持久化卷
|
||||||
|
|
||||||
|
3. **持久化存储**:
|
||||||
|
- `langbot-data`: LangBot 主数据
|
||||||
|
- `langbot-plugins`: 插件文件
|
||||||
|
- `langbot-plugin-runtime-data`: 插件运行时数据
|
||||||
|
|
||||||
|
### 快速开始
|
||||||
|
|
||||||
|
#### 1. 下载部署文件
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 克隆仓库
|
||||||
|
git clone https://github.com/langbot-app/LangBot
|
||||||
|
cd LangBot/docker
|
||||||
|
|
||||||
|
# 或直接下载 kubernetes.yaml
|
||||||
|
wget https://raw.githubusercontent.com/langbot-app/LangBot/main/docker/kubernetes.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. 部署到 Kubernetes
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 应用所有配置
|
||||||
|
kubectl apply -f kubernetes.yaml
|
||||||
|
|
||||||
|
# 检查部署状态
|
||||||
|
kubectl get all -n langbot
|
||||||
|
|
||||||
|
# 查看 Pod 日志
|
||||||
|
kubectl logs -n langbot -l app=langbot -f
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. 访问 LangBot
|
||||||
|
|
||||||
|
默认情况下,LangBot 服务使用 ClusterIP 类型,只能在集群内部访问。您可以选择以下方式之一来访问:
|
||||||
|
|
||||||
|
**选项 A: 端口转发(推荐用于测试)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl port-forward -n langbot svc/langbot 5300:5300
|
||||||
|
```
|
||||||
|
|
||||||
|
然后访问 http://localhost:5300
|
||||||
|
|
||||||
|
**选项 B: NodePort(适用于开发环境)**
|
||||||
|
|
||||||
|
编辑 `kubernetes.yaml`,取消注释 NodePort Service 部分,然后:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl apply -f kubernetes.yaml
|
||||||
|
# 获取节点 IP
|
||||||
|
kubectl get nodes -o wide
|
||||||
|
# 访问 http://<NODE_IP>:30300
|
||||||
|
```
|
||||||
|
|
||||||
|
**选项 C: LoadBalancer(适用于云环境)**
|
||||||
|
|
||||||
|
编辑 `kubernetes.yaml`,取消注释 LoadBalancer Service 部分,然后:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl apply -f kubernetes.yaml
|
||||||
|
# 获取外部 IP
|
||||||
|
kubectl get svc -n langbot langbot-loadbalancer
|
||||||
|
# 访问 http://<EXTERNAL_IP>
|
||||||
|
```
|
||||||
|
|
||||||
|
**选项 D: Ingress(推荐用于生产环境)**
|
||||||
|
|
||||||
|
确保集群中已安装 Ingress Controller(如 nginx-ingress),然后:
|
||||||
|
|
||||||
|
1. 编辑 `kubernetes.yaml` 中的 Ingress 配置
|
||||||
|
2. 修改域名为您的实际域名
|
||||||
|
3. 应用配置:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl apply -f kubernetes.yaml
|
||||||
|
# 访问 http://langbot.yourdomain.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### 配置说明
|
||||||
|
|
||||||
|
#### 环境变量
|
||||||
|
|
||||||
|
在 `ConfigMap` 中配置环境变量:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: langbot-config
|
||||||
|
namespace: langbot
|
||||||
|
data:
|
||||||
|
TZ: "Asia/Shanghai" # 修改为您的时区
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 存储配置
|
||||||
|
|
||||||
|
默认使用动态存储分配。如果您有特定的 StorageClass,请在 PVC 中指定:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
spec:
|
||||||
|
storageClassName: your-storage-class-name
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 10Gi
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 资源限制
|
||||||
|
|
||||||
|
根据您的需求调整资源限制:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: "1Gi"
|
||||||
|
cpu: "500m"
|
||||||
|
limits:
|
||||||
|
memory: "4Gi"
|
||||||
|
cpu: "2000m"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 常用操作
|
||||||
|
|
||||||
|
#### 查看日志
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查看 LangBot 主服务日志
|
||||||
|
kubectl logs -n langbot -l app=langbot -f
|
||||||
|
|
||||||
|
# 查看插件运行时日志
|
||||||
|
kubectl logs -n langbot -l app=langbot-plugin-runtime -f
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 重启服务
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 重启 LangBot
|
||||||
|
kubectl rollout restart deployment/langbot -n langbot
|
||||||
|
|
||||||
|
# 重启插件运行时
|
||||||
|
kubectl rollout restart deployment/langbot-plugin-runtime -n langbot
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 更新镜像
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 更新到最新版本
|
||||||
|
kubectl set image deployment/langbot -n langbot langbot=rockchin/langbot:latest
|
||||||
|
kubectl set image deployment/langbot-plugin-runtime -n langbot langbot-plugin-runtime=rockchin/langbot:latest
|
||||||
|
|
||||||
|
# 检查更新状态
|
||||||
|
kubectl rollout status deployment/langbot -n langbot
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 扩容(不推荐)
|
||||||
|
|
||||||
|
注意:由于 LangBot 使用 ReadWriteOnce 的持久化存储,不支持多副本扩容。如需高可用,请考虑使用 ReadWriteMany 存储或其他架构方案。
|
||||||
|
|
||||||
|
#### 备份数据
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 备份 PVC 数据
|
||||||
|
kubectl exec -n langbot -it <langbot-pod-name> -- tar czf /tmp/backup.tar.gz /app/data
|
||||||
|
kubectl cp langbot/<langbot-pod-name>:/tmp/backup.tar.gz ./backup.tar.gz
|
||||||
|
```
|
||||||
|
|
||||||
|
### 卸载
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 删除所有资源(保留 PVC)
|
||||||
|
kubectl delete deployment,service,configmap -n langbot --all
|
||||||
|
|
||||||
|
# 删除 PVC(会删除数据)
|
||||||
|
kubectl delete pvc -n langbot --all
|
||||||
|
|
||||||
|
# 删除命名空间
|
||||||
|
kubectl delete namespace langbot
|
||||||
|
```
|
||||||
|
|
||||||
|
### 故障排查
|
||||||
|
|
||||||
|
#### Pod 无法启动
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查看 Pod 状态
|
||||||
|
kubectl get pods -n langbot
|
||||||
|
|
||||||
|
# 查看详细信息
|
||||||
|
kubectl describe pod -n langbot <pod-name>
|
||||||
|
|
||||||
|
# 查看事件
|
||||||
|
kubectl get events -n langbot --sort-by='.lastTimestamp'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 存储问题
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查 PVC 状态
|
||||||
|
kubectl get pvc -n langbot
|
||||||
|
|
||||||
|
# 检查 PV
|
||||||
|
kubectl get pv
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 网络访问问题
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查 Service
|
||||||
|
kubectl get svc -n langbot
|
||||||
|
|
||||||
|
# 检查端口转发
|
||||||
|
kubectl port-forward -n langbot svc/langbot 5300:5300
|
||||||
|
```
|
||||||
|
|
||||||
|
### 生产环境建议
|
||||||
|
|
||||||
|
1. **使用特定版本标签**:避免使用 `latest` 标签,使用具体版本号如 `rockchin/langbot:v1.0.0`
|
||||||
|
2. **配置资源限制**:根据实际负载调整 CPU 和内存限制
|
||||||
|
3. **使用 Ingress + TLS**:配置 HTTPS 访问和证书管理
|
||||||
|
4. **配置监控和告警**:集成 Prometheus、Grafana 等监控工具
|
||||||
|
5. **定期备份**:配置自动备份策略保护数据
|
||||||
|
6. **使用专用 StorageClass**:为生产环境配置高性能存储
|
||||||
|
7. **配置亲和性规则**:确保 Pod 调度到合适的节点
|
||||||
|
|
||||||
|
### 高级配置
|
||||||
|
|
||||||
|
#### 使用 Secrets 管理敏感信息
|
||||||
|
|
||||||
|
如果需要配置 API 密钥等敏感信息:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: langbot-secrets
|
||||||
|
namespace: langbot
|
||||||
|
type: Opaque
|
||||||
|
data:
|
||||||
|
api_key: <base64-encoded-value>
|
||||||
|
```
|
||||||
|
|
||||||
|
然后在 Deployment 中引用:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
env:
|
||||||
|
- name: API_KEY
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: langbot-secrets
|
||||||
|
key: api_key
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 配置水平自动扩缩容(HPA)
|
||||||
|
|
||||||
|
注意:需要确保使用 ReadWriteMany 存储类型
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: autoscaling/v2
|
||||||
|
kind: HorizontalPodAutoscaler
|
||||||
|
metadata:
|
||||||
|
name: langbot-hpa
|
||||||
|
namespace: langbot
|
||||||
|
spec:
|
||||||
|
scaleTargetRef:
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
name: langbot
|
||||||
|
minReplicas: 1
|
||||||
|
maxReplicas: 3
|
||||||
|
metrics:
|
||||||
|
- type: Resource
|
||||||
|
resource:
|
||||||
|
name: cpu
|
||||||
|
target:
|
||||||
|
type: Utilization
|
||||||
|
averageUtilization: 70
|
||||||
|
```
|
||||||
|
|
||||||
|
### 参考资源
|
||||||
|
|
||||||
|
- [LangBot 官方文档](https://docs.langbot.app)
|
||||||
|
- [Docker 部署文档](https://link.langbot.app/zh/docs/docker)
|
||||||
|
- [Kubernetes 官方文档](https://kubernetes.io/docs/)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## English
|
||||||
|
|
||||||
|
### Overview
|
||||||
|
|
||||||
|
This guide provides complete steps for deploying LangBot in a Kubernetes cluster. The Kubernetes deployment configuration is based on `docker-compose.yaml` and is suitable for production containerized deployments.
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Kubernetes cluster (version 1.19+)
|
||||||
|
- `kubectl` command-line tool configured with cluster access
|
||||||
|
- Available StorageClass in the cluster for persistent storage (optional but recommended)
|
||||||
|
- At least 2 vCPU and 4GB RAM of available resources
|
||||||
|
|
||||||
|
### Architecture
|
||||||
|
|
||||||
|
The Kubernetes deployment includes the following components:
|
||||||
|
|
||||||
|
1. **langbot**: Main application service
|
||||||
|
- Provides Web UI (port 5300)
|
||||||
|
- Handles platform webhooks (ports 2280-2290)
|
||||||
|
- Data persistence volume
|
||||||
|
|
||||||
|
2. **langbot-plugin-runtime**: Plugin runtime service
|
||||||
|
- WebSocket communication (port 5400)
|
||||||
|
- Plugin data persistence volume
|
||||||
|
|
||||||
|
3. **Persistent Storage**:
|
||||||
|
- `langbot-data`: LangBot main data
|
||||||
|
- `langbot-plugins`: Plugin files
|
||||||
|
- `langbot-plugin-runtime-data`: Plugin runtime data
|
||||||
|
|
||||||
|
### Quick Start
|
||||||
|
|
||||||
|
#### 1. Download Deployment Files
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone repository
|
||||||
|
git clone https://github.com/langbot-app/LangBot
|
||||||
|
cd LangBot/docker
|
||||||
|
|
||||||
|
# Or download kubernetes.yaml directly
|
||||||
|
wget https://raw.githubusercontent.com/langbot-app/LangBot/main/docker/kubernetes.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Deploy to Kubernetes
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Apply all configurations
|
||||||
|
kubectl apply -f kubernetes.yaml
|
||||||
|
|
||||||
|
# Check deployment status
|
||||||
|
kubectl get all -n langbot
|
||||||
|
|
||||||
|
# View Pod logs
|
||||||
|
kubectl logs -n langbot -l app=langbot -f
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Access LangBot
|
||||||
|
|
||||||
|
By default, LangBot service uses ClusterIP type, accessible only within the cluster. Choose one of the following methods to access:
|
||||||
|
|
||||||
|
**Option A: Port Forwarding (Recommended for testing)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl port-forward -n langbot svc/langbot 5300:5300
|
||||||
|
```
|
||||||
|
|
||||||
|
Then visit http://localhost:5300
|
||||||
|
|
||||||
|
**Option B: NodePort (Suitable for development)**
|
||||||
|
|
||||||
|
Edit `kubernetes.yaml`, uncomment the NodePort Service section, then:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl apply -f kubernetes.yaml
|
||||||
|
# Get node IP
|
||||||
|
kubectl get nodes -o wide
|
||||||
|
# Visit http://<NODE_IP>:30300
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option C: LoadBalancer (Suitable for cloud environments)**
|
||||||
|
|
||||||
|
Edit `kubernetes.yaml`, uncomment the LoadBalancer Service section, then:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl apply -f kubernetes.yaml
|
||||||
|
# Get external IP
|
||||||
|
kubectl get svc -n langbot langbot-loadbalancer
|
||||||
|
# Visit http://<EXTERNAL_IP>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option D: Ingress (Recommended for production)**
|
||||||
|
|
||||||
|
Ensure an Ingress Controller (e.g., nginx-ingress) is installed in the cluster, then:
|
||||||
|
|
||||||
|
1. Edit the Ingress configuration in `kubernetes.yaml`
|
||||||
|
2. Change the domain to your actual domain
|
||||||
|
3. Apply configuration:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl apply -f kubernetes.yaml
|
||||||
|
# Visit http://langbot.yourdomain.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
#### Environment Variables
|
||||||
|
|
||||||
|
Configure environment variables in ConfigMap:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: langbot-config
|
||||||
|
namespace: langbot
|
||||||
|
data:
|
||||||
|
TZ: "Asia/Shanghai" # Change to your timezone
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Storage Configuration
|
||||||
|
|
||||||
|
Uses dynamic storage provisioning by default. If you have a specific StorageClass, specify it in PVC:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
spec:
|
||||||
|
storageClassName: your-storage-class-name
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 10Gi
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Resource Limits
|
||||||
|
|
||||||
|
Adjust resource limits based on your needs:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: "1Gi"
|
||||||
|
cpu: "500m"
|
||||||
|
limits:
|
||||||
|
memory: "4Gi"
|
||||||
|
cpu: "2000m"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Common Operations
|
||||||
|
|
||||||
|
#### View Logs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# View LangBot main service logs
|
||||||
|
kubectl logs -n langbot -l app=langbot -f
|
||||||
|
|
||||||
|
# View plugin runtime logs
|
||||||
|
kubectl logs -n langbot -l app=langbot-plugin-runtime -f
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Restart Services
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Restart LangBot
|
||||||
|
kubectl rollout restart deployment/langbot -n langbot
|
||||||
|
|
||||||
|
# Restart plugin runtime
|
||||||
|
kubectl rollout restart deployment/langbot-plugin-runtime -n langbot
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Update Images
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Update to latest version
|
||||||
|
kubectl set image deployment/langbot -n langbot langbot=rockchin/langbot:latest
|
||||||
|
kubectl set image deployment/langbot-plugin-runtime -n langbot langbot-plugin-runtime=rockchin/langbot:latest
|
||||||
|
|
||||||
|
# Check update status
|
||||||
|
kubectl rollout status deployment/langbot -n langbot
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Scaling (Not Recommended)
|
||||||
|
|
||||||
|
Note: Due to LangBot using ReadWriteOnce persistent storage, multi-replica scaling is not supported. For high availability, consider using ReadWriteMany storage or alternative architectures.
|
||||||
|
|
||||||
|
#### Backup Data
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Backup PVC data
|
||||||
|
kubectl exec -n langbot -it <langbot-pod-name> -- tar czf /tmp/backup.tar.gz /app/data
|
||||||
|
kubectl cp langbot/<langbot-pod-name>:/tmp/backup.tar.gz ./backup.tar.gz
|
||||||
|
```
|
||||||
|
|
||||||
|
### Uninstall
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Delete all resources (keep PVCs)
|
||||||
|
kubectl delete deployment,service,configmap -n langbot --all
|
||||||
|
|
||||||
|
# Delete PVCs (will delete data)
|
||||||
|
kubectl delete pvc -n langbot --all
|
||||||
|
|
||||||
|
# Delete namespace
|
||||||
|
kubectl delete namespace langbot
|
||||||
|
```
|
||||||
|
|
||||||
|
### Troubleshooting
|
||||||
|
|
||||||
|
#### Pods Not Starting
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check Pod status
|
||||||
|
kubectl get pods -n langbot
|
||||||
|
|
||||||
|
# View detailed information
|
||||||
|
kubectl describe pod -n langbot <pod-name>
|
||||||
|
|
||||||
|
# View events
|
||||||
|
kubectl get events -n langbot --sort-by='.lastTimestamp'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Storage Issues
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check PVC status
|
||||||
|
kubectl get pvc -n langbot
|
||||||
|
|
||||||
|
# Check PV
|
||||||
|
kubectl get pv
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Network Access Issues
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check Service
|
||||||
|
kubectl get svc -n langbot
|
||||||
|
|
||||||
|
# Test port forwarding
|
||||||
|
kubectl port-forward -n langbot svc/langbot 5300:5300
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production Recommendations
|
||||||
|
|
||||||
|
1. **Use specific version tags**: Avoid using `latest` tag, use specific version like `rockchin/langbot:v1.0.0`
|
||||||
|
2. **Configure resource limits**: Adjust CPU and memory limits based on actual load
|
||||||
|
3. **Use Ingress + TLS**: Configure HTTPS access and certificate management
|
||||||
|
4. **Configure monitoring and alerts**: Integrate monitoring tools like Prometheus, Grafana
|
||||||
|
5. **Regular backups**: Configure automated backup strategy to protect data
|
||||||
|
6. **Use dedicated StorageClass**: Configure high-performance storage for production
|
||||||
|
7. **Configure affinity rules**: Ensure Pods are scheduled to appropriate nodes
|
||||||
|
|
||||||
|
### Advanced Configuration
|
||||||
|
|
||||||
|
#### Using Secrets for Sensitive Information
|
||||||
|
|
||||||
|
If you need to configure sensitive information like API keys:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: langbot-secrets
|
||||||
|
namespace: langbot
|
||||||
|
type: Opaque
|
||||||
|
data:
|
||||||
|
api_key: <base64-encoded-value>
|
||||||
|
```
|
||||||
|
|
||||||
|
Then reference in Deployment:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
env:
|
||||||
|
- name: API_KEY
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: langbot-secrets
|
||||||
|
key: api_key
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Configure Horizontal Pod Autoscaling (HPA)
|
||||||
|
|
||||||
|
Note: Requires ReadWriteMany storage type
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: autoscaling/v2
|
||||||
|
kind: HorizontalPodAutoscaler
|
||||||
|
metadata:
|
||||||
|
name: langbot-hpa
|
||||||
|
namespace: langbot
|
||||||
|
spec:
|
||||||
|
scaleTargetRef:
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
name: langbot
|
||||||
|
minReplicas: 1
|
||||||
|
maxReplicas: 3
|
||||||
|
metrics:
|
||||||
|
- type: Resource
|
||||||
|
resource:
|
||||||
|
name: cpu
|
||||||
|
target:
|
||||||
|
type: Utilization
|
||||||
|
averageUtilization: 70
|
||||||
|
```
|
||||||
|
|
||||||
|
### References
|
||||||
|
|
||||||
|
- [LangBot Official Documentation](https://docs.langbot.app)
|
||||||
|
- [Docker Deployment Guide](https://link.langbot.app/zh/docs/docker)
|
||||||
|
- [Kubernetes Official Documentation](https://kubernetes.io/docs/)
|
||||||
74
docker/deploy-k8s-test.sh
Executable file
74
docker/deploy-k8s-test.sh
Executable file
@@ -0,0 +1,74 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Quick test script for LangBot Kubernetes deployment
|
||||||
|
# This script helps you test the Kubernetes deployment locally
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "🚀 LangBot Kubernetes Deployment Test Script"
|
||||||
|
echo "=============================================="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check for kubectl
|
||||||
|
if ! command -v kubectl &> /dev/null; then
|
||||||
|
echo "❌ kubectl is not installed. Please install kubectl first."
|
||||||
|
echo "Visit: https://kubernetes.io/docs/tasks/tools/"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✓ kubectl is installed"
|
||||||
|
|
||||||
|
# Check if kubectl can connect to a cluster
|
||||||
|
if ! kubectl cluster-info &> /dev/null; then
|
||||||
|
echo ""
|
||||||
|
echo "⚠️ No Kubernetes cluster found."
|
||||||
|
echo ""
|
||||||
|
echo "To test locally, you can use:"
|
||||||
|
echo " - kind: https://kind.sigs.k8s.io/"
|
||||||
|
echo " - minikube: https://minikube.sigs.k8s.io/"
|
||||||
|
echo " - k3s: https://k3s.io/"
|
||||||
|
echo ""
|
||||||
|
echo "Example with kind:"
|
||||||
|
echo " kind create cluster --name langbot-test"
|
||||||
|
echo ""
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✓ Connected to Kubernetes cluster"
|
||||||
|
kubectl cluster-info
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Ask user to confirm
|
||||||
|
read -p "Do you want to deploy LangBot to this cluster? (y/N) " -n 1 -r
|
||||||
|
echo
|
||||||
|
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||||
|
echo "Deployment cancelled."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "📦 Deploying LangBot..."
|
||||||
|
kubectl apply -f kubernetes.yaml
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "⏳ Waiting for pods to be ready..."
|
||||||
|
kubectl wait --for=condition=ready pod -l app=langbot -n langbot --timeout=300s
|
||||||
|
kubectl wait --for=condition=ready pod -l app=langbot-plugin-runtime -n langbot --timeout=300s
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ Deployment complete!"
|
||||||
|
echo ""
|
||||||
|
echo "📊 Deployment status:"
|
||||||
|
kubectl get all -n langbot
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "🌐 To access LangBot Web UI, run:"
|
||||||
|
echo " kubectl port-forward -n langbot svc/langbot 5300:5300"
|
||||||
|
echo ""
|
||||||
|
echo "Then visit: http://localhost:5300"
|
||||||
|
echo ""
|
||||||
|
echo "📝 To view logs:"
|
||||||
|
echo " kubectl logs -n langbot -l app=langbot -f"
|
||||||
|
echo ""
|
||||||
|
echo "🗑️ To uninstall:"
|
||||||
|
echo " kubectl delete namespace langbot"
|
||||||
|
echo ""
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# Docker Compose configuration for LangBot
|
||||||
|
# For Kubernetes deployment, see kubernetes.yaml and README_K8S.md
|
||||||
version: "3"
|
version: "3"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
@@ -12,7 +14,7 @@ services:
|
|||||||
restart: on-failure
|
restart: on-failure
|
||||||
environment:
|
environment:
|
||||||
- TZ=Asia/Shanghai
|
- TZ=Asia/Shanghai
|
||||||
command: ["uv", "run", "-m", "langbot_plugin.cli.__init__", "rt"]
|
command: ["uv", "run", "--no-sync", "-m", "langbot_plugin.cli.__init__", "rt"]
|
||||||
networks:
|
networks:
|
||||||
- langbot_network
|
- langbot_network
|
||||||
|
|
||||||
@@ -21,16 +23,15 @@ services:
|
|||||||
container_name: langbot
|
container_name: langbot
|
||||||
volumes:
|
volumes:
|
||||||
- ./data:/app/data
|
- ./data:/app/data
|
||||||
- ./plugins:/app/plugins
|
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
environment:
|
environment:
|
||||||
- TZ=Asia/Shanghai
|
- TZ=Asia/Shanghai
|
||||||
ports:
|
ports:
|
||||||
- 5300:5300 # For web ui
|
- 5300:5300 # For web ui and webhook callback
|
||||||
- 2280-2290:2280-2290 # For platform webhook
|
- 2280-2285:2280-2285 # For platform reverse connection
|
||||||
networks:
|
networks:
|
||||||
- langbot_network
|
- langbot_network
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
langbot_network:
|
langbot_network:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
400
docker/kubernetes.yaml
Normal file
400
docker/kubernetes.yaml
Normal file
@@ -0,0 +1,400 @@
|
|||||||
|
# Kubernetes Deployment for LangBot
|
||||||
|
# This file provides Kubernetes deployment manifests for LangBot based on docker-compose.yaml
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# kubectl apply -f kubernetes.yaml
|
||||||
|
#
|
||||||
|
# Prerequisites:
|
||||||
|
# - A Kubernetes cluster (1.19+)
|
||||||
|
# - kubectl configured to communicate with your cluster
|
||||||
|
# - (Optional) A StorageClass for dynamic volume provisioning
|
||||||
|
#
|
||||||
|
# Components:
|
||||||
|
# - Namespace: langbot
|
||||||
|
# - PersistentVolumeClaims for data persistence
|
||||||
|
# - Deployments for langbot and langbot_plugin_runtime
|
||||||
|
# - Services for network access
|
||||||
|
# - ConfigMap for timezone configuration
|
||||||
|
|
||||||
|
---
|
||||||
|
# Namespace
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: langbot
|
||||||
|
labels:
|
||||||
|
app: langbot
|
||||||
|
|
||||||
|
---
|
||||||
|
# PersistentVolumeClaim for LangBot data
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: langbot-data
|
||||||
|
namespace: langbot
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 10Gi
|
||||||
|
# Uncomment and modify if you have a specific StorageClass
|
||||||
|
# storageClassName: your-storage-class
|
||||||
|
|
||||||
|
---
|
||||||
|
# PersistentVolumeClaim for LangBot plugins
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: langbot-plugins
|
||||||
|
namespace: langbot
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 5Gi
|
||||||
|
# Uncomment and modify if you have a specific StorageClass
|
||||||
|
# storageClassName: your-storage-class
|
||||||
|
|
||||||
|
---
|
||||||
|
# PersistentVolumeClaim for Plugin Runtime data
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: langbot-plugin-runtime-data
|
||||||
|
namespace: langbot
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 5Gi
|
||||||
|
# Uncomment and modify if you have a specific StorageClass
|
||||||
|
# storageClassName: your-storage-class
|
||||||
|
|
||||||
|
---
|
||||||
|
# ConfigMap for environment configuration
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: langbot-config
|
||||||
|
namespace: langbot
|
||||||
|
data:
|
||||||
|
TZ: "Asia/Shanghai"
|
||||||
|
PLUGIN__RUNTIME_WS_URL: "ws://langbot-plugin-runtime:5400/control/ws"
|
||||||
|
|
||||||
|
---
|
||||||
|
# Deployment for LangBot Plugin Runtime
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: langbot-plugin-runtime
|
||||||
|
namespace: langbot
|
||||||
|
labels:
|
||||||
|
app: langbot-plugin-runtime
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: langbot-plugin-runtime
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: langbot-plugin-runtime
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: langbot-plugin-runtime
|
||||||
|
image: rockchin/langbot:latest
|
||||||
|
imagePullPolicy: Always
|
||||||
|
command: ["uv", "run", "-m", "langbot_plugin.cli.__init__", "rt"]
|
||||||
|
ports:
|
||||||
|
- containerPort: 5400
|
||||||
|
name: runtime
|
||||||
|
protocol: TCP
|
||||||
|
env:
|
||||||
|
- name: TZ
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: langbot-config
|
||||||
|
key: TZ
|
||||||
|
volumeMounts:
|
||||||
|
- name: plugin-data
|
||||||
|
mountPath: /app/data/plugins
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: "512Mi"
|
||||||
|
cpu: "250m"
|
||||||
|
limits:
|
||||||
|
memory: "2Gi"
|
||||||
|
cpu: "1000m"
|
||||||
|
# Liveness probe to restart container if it becomes unresponsive
|
||||||
|
livenessProbe:
|
||||||
|
tcpSocket:
|
||||||
|
port: 5400
|
||||||
|
initialDelaySeconds: 30
|
||||||
|
periodSeconds: 10
|
||||||
|
timeoutSeconds: 5
|
||||||
|
failureThreshold: 3
|
||||||
|
# Readiness probe to know when container is ready to accept traffic
|
||||||
|
readinessProbe:
|
||||||
|
tcpSocket:
|
||||||
|
port: 5400
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 5
|
||||||
|
timeoutSeconds: 3
|
||||||
|
failureThreshold: 3
|
||||||
|
volumes:
|
||||||
|
- name: plugin-data
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: langbot-plugin-runtime-data
|
||||||
|
restartPolicy: Always
|
||||||
|
|
||||||
|
---
|
||||||
|
# Service for LangBot Plugin Runtime
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: langbot-plugin-runtime
|
||||||
|
namespace: langbot
|
||||||
|
labels:
|
||||||
|
app: langbot-plugin-runtime
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
selector:
|
||||||
|
app: langbot-plugin-runtime
|
||||||
|
ports:
|
||||||
|
- port: 5400
|
||||||
|
targetPort: 5400
|
||||||
|
protocol: TCP
|
||||||
|
name: runtime
|
||||||
|
|
||||||
|
---
|
||||||
|
# Deployment for LangBot
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: langbot
|
||||||
|
namespace: langbot
|
||||||
|
labels:
|
||||||
|
app: langbot
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: langbot
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: langbot
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: langbot
|
||||||
|
image: rockchin/langbot:latest
|
||||||
|
imagePullPolicy: Always
|
||||||
|
ports:
|
||||||
|
- containerPort: 5300
|
||||||
|
name: web
|
||||||
|
protocol: TCP
|
||||||
|
- containerPort: 2280
|
||||||
|
name: webhook-start
|
||||||
|
protocol: TCP
|
||||||
|
# Note: Kubernetes doesn't support port ranges directly in container ports
|
||||||
|
# The webhook ports 2280-2290 are available, but we only expose the start of the range
|
||||||
|
# If you need all ports exposed, consider using a Service with multiple port definitions
|
||||||
|
env:
|
||||||
|
- name: TZ
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: langbot-config
|
||||||
|
key: TZ
|
||||||
|
- name: PLUGIN__RUNTIME_WS_URL
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: langbot-config
|
||||||
|
key: PLUGIN__RUNTIME_WS_URL
|
||||||
|
volumeMounts:
|
||||||
|
- name: data
|
||||||
|
mountPath: /app/data
|
||||||
|
- name: plugins
|
||||||
|
mountPath: /app/plugins
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: "1Gi"
|
||||||
|
cpu: "500m"
|
||||||
|
limits:
|
||||||
|
memory: "4Gi"
|
||||||
|
cpu: "2000m"
|
||||||
|
# Liveness probe to restart container if it becomes unresponsive
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /
|
||||||
|
port: 5300
|
||||||
|
initialDelaySeconds: 60
|
||||||
|
periodSeconds: 10
|
||||||
|
timeoutSeconds: 5
|
||||||
|
failureThreshold: 3
|
||||||
|
# Readiness probe to know when container is ready to accept traffic
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /
|
||||||
|
port: 5300
|
||||||
|
initialDelaySeconds: 30
|
||||||
|
periodSeconds: 5
|
||||||
|
timeoutSeconds: 3
|
||||||
|
failureThreshold: 3
|
||||||
|
volumes:
|
||||||
|
- name: data
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: langbot-data
|
||||||
|
- name: plugins
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: langbot-plugins
|
||||||
|
restartPolicy: Always
|
||||||
|
|
||||||
|
---
|
||||||
|
# Service for LangBot (ClusterIP for internal access)
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: langbot
|
||||||
|
namespace: langbot
|
||||||
|
labels:
|
||||||
|
app: langbot
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
selector:
|
||||||
|
app: langbot
|
||||||
|
ports:
|
||||||
|
- port: 5300
|
||||||
|
targetPort: 5300
|
||||||
|
protocol: TCP
|
||||||
|
name: web
|
||||||
|
- port: 2280
|
||||||
|
targetPort: 2280
|
||||||
|
protocol: TCP
|
||||||
|
name: webhook-2280
|
||||||
|
- port: 2281
|
||||||
|
targetPort: 2281
|
||||||
|
protocol: TCP
|
||||||
|
name: webhook-2281
|
||||||
|
- port: 2282
|
||||||
|
targetPort: 2282
|
||||||
|
protocol: TCP
|
||||||
|
name: webhook-2282
|
||||||
|
- port: 2283
|
||||||
|
targetPort: 2283
|
||||||
|
protocol: TCP
|
||||||
|
name: webhook-2283
|
||||||
|
- port: 2284
|
||||||
|
targetPort: 2284
|
||||||
|
protocol: TCP
|
||||||
|
name: webhook-2284
|
||||||
|
- port: 2285
|
||||||
|
targetPort: 2285
|
||||||
|
protocol: TCP
|
||||||
|
name: webhook-2285
|
||||||
|
- port: 2286
|
||||||
|
targetPort: 2286
|
||||||
|
protocol: TCP
|
||||||
|
name: webhook-2286
|
||||||
|
- port: 2287
|
||||||
|
targetPort: 2287
|
||||||
|
protocol: TCP
|
||||||
|
name: webhook-2287
|
||||||
|
- port: 2288
|
||||||
|
targetPort: 2288
|
||||||
|
protocol: TCP
|
||||||
|
name: webhook-2288
|
||||||
|
- port: 2289
|
||||||
|
targetPort: 2289
|
||||||
|
protocol: TCP
|
||||||
|
name: webhook-2289
|
||||||
|
- port: 2290
|
||||||
|
targetPort: 2290
|
||||||
|
protocol: TCP
|
||||||
|
name: webhook-2290
|
||||||
|
|
||||||
|
---
|
||||||
|
# Ingress for external access (Optional - requires Ingress Controller)
|
||||||
|
# Uncomment and modify the following section if you want to expose LangBot via Ingress
|
||||||
|
# apiVersion: networking.k8s.io/v1
|
||||||
|
# kind: Ingress
|
||||||
|
# metadata:
|
||||||
|
# name: langbot-ingress
|
||||||
|
# namespace: langbot
|
||||||
|
# annotations:
|
||||||
|
# # Uncomment and modify based on your ingress controller
|
||||||
|
# # nginx.ingress.kubernetes.io/rewrite-target: /
|
||||||
|
# # cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||||
|
# spec:
|
||||||
|
# ingressClassName: nginx # Change based on your ingress controller
|
||||||
|
# rules:
|
||||||
|
# - host: langbot.yourdomain.com # Change to your domain
|
||||||
|
# http:
|
||||||
|
# paths:
|
||||||
|
# - path: /
|
||||||
|
# pathType: Prefix
|
||||||
|
# backend:
|
||||||
|
# service:
|
||||||
|
# name: langbot
|
||||||
|
# port:
|
||||||
|
# number: 5300
|
||||||
|
# # Uncomment for TLS/HTTPS
|
||||||
|
# # tls:
|
||||||
|
# # - hosts:
|
||||||
|
# # - langbot.yourdomain.com
|
||||||
|
# # secretName: langbot-tls
|
||||||
|
|
||||||
|
---
|
||||||
|
# Service for LangBot with LoadBalancer (Alternative to Ingress)
|
||||||
|
# Uncomment the following if you want to expose LangBot directly via LoadBalancer
|
||||||
|
# This is useful in cloud environments (AWS, GCP, Azure, etc.)
|
||||||
|
# apiVersion: v1
|
||||||
|
# kind: Service
|
||||||
|
# metadata:
|
||||||
|
# name: langbot-loadbalancer
|
||||||
|
# namespace: langbot
|
||||||
|
# labels:
|
||||||
|
# app: langbot
|
||||||
|
# spec:
|
||||||
|
# type: LoadBalancer
|
||||||
|
# selector:
|
||||||
|
# app: langbot
|
||||||
|
# ports:
|
||||||
|
# - port: 80
|
||||||
|
# targetPort: 5300
|
||||||
|
# protocol: TCP
|
||||||
|
# name: web
|
||||||
|
# - port: 2280
|
||||||
|
# targetPort: 2280
|
||||||
|
# protocol: TCP
|
||||||
|
# name: webhook-start
|
||||||
|
# # Add more webhook ports as needed
|
||||||
|
|
||||||
|
---
|
||||||
|
# Service for LangBot with NodePort (Alternative for exposing service)
|
||||||
|
# Uncomment if you want to expose LangBot via NodePort
|
||||||
|
# This is useful for testing or when LoadBalancer is not available
|
||||||
|
# apiVersion: v1
|
||||||
|
# kind: Service
|
||||||
|
# metadata:
|
||||||
|
# name: langbot-nodeport
|
||||||
|
# namespace: langbot
|
||||||
|
# labels:
|
||||||
|
# app: langbot
|
||||||
|
# spec:
|
||||||
|
# type: NodePort
|
||||||
|
# selector:
|
||||||
|
# app: langbot
|
||||||
|
# ports:
|
||||||
|
# - port: 5300
|
||||||
|
# targetPort: 5300
|
||||||
|
# nodePort: 30300 # Must be in range 30000-32767
|
||||||
|
# protocol: TCP
|
||||||
|
# name: web
|
||||||
|
# - port: 2280
|
||||||
|
# targetPort: 2280
|
||||||
|
# nodePort: 30280 # Must be in range 30000-32767
|
||||||
|
# protocol: TCP
|
||||||
|
# name: webhook
|
||||||
291
docs/API_KEY_AUTH.md
Normal file
291
docs/API_KEY_AUTH.md
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
# API Key Authentication
|
||||||
|
|
||||||
|
LangBot now supports API key authentication for external systems to access its HTTP service API.
|
||||||
|
|
||||||
|
## Managing API Keys
|
||||||
|
|
||||||
|
API keys can be managed through the web interface:
|
||||||
|
|
||||||
|
1. Log in to the LangBot web interface
|
||||||
|
2. Click the "API Keys" button at the bottom of the sidebar
|
||||||
|
3. Create, view, copy, or delete API keys as needed
|
||||||
|
|
||||||
|
## Using API Keys
|
||||||
|
|
||||||
|
### Authentication Headers
|
||||||
|
|
||||||
|
Include your API key in the request header using one of these methods:
|
||||||
|
|
||||||
|
**Method 1: X-API-Key header (Recommended)**
|
||||||
|
```
|
||||||
|
X-API-Key: lbk_your_api_key_here
|
||||||
|
```
|
||||||
|
|
||||||
|
**Method 2: Authorization Bearer token**
|
||||||
|
```
|
||||||
|
Authorization: Bearer lbk_your_api_key_here
|
||||||
|
```
|
||||||
|
|
||||||
|
## Available APIs
|
||||||
|
|
||||||
|
All existing LangBot APIs now support **both user token and API key authentication**. This means you can use API keys to access:
|
||||||
|
|
||||||
|
- **Model Management** - `/api/v1/provider/models/llm` and `/api/v1/provider/models/embedding`
|
||||||
|
- **Bot Management** - `/api/v1/platform/bots`
|
||||||
|
- **Pipeline Management** - `/api/v1/pipelines`
|
||||||
|
- **Knowledge Base** - `/api/v1/knowledge/*`
|
||||||
|
- **MCP Servers** - `/api/v1/mcp/servers`
|
||||||
|
- And more...
|
||||||
|
|
||||||
|
### Authentication Methods
|
||||||
|
|
||||||
|
Each endpoint accepts **either**:
|
||||||
|
1. **User Token** (via `Authorization: Bearer <user_jwt_token>`) - for web UI and authenticated users
|
||||||
|
2. **API Key** (via `X-API-Key` or `Authorization: Bearer <api_key>`) - for external services
|
||||||
|
|
||||||
|
## Example: Model Management
|
||||||
|
|
||||||
|
### List All LLM Models
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/provider/models/llm
|
||||||
|
X-API-Key: lbk_your_api_key_here
|
||||||
|
```
|
||||||
|
|
||||||
|
Response:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"msg": "ok",
|
||||||
|
"data": {
|
||||||
|
"models": [
|
||||||
|
{
|
||||||
|
"uuid": "model-uuid",
|
||||||
|
"name": "GPT-4",
|
||||||
|
"description": "OpenAI GPT-4 model",
|
||||||
|
"requester": "openai-chat-completions",
|
||||||
|
"requester_config": {...},
|
||||||
|
"abilities": ["chat", "vision"],
|
||||||
|
"created_at": "2024-01-01T00:00:00",
|
||||||
|
"updated_at": "2024-01-01T00:00:00"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Create a New LLM Model
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/v1/provider/models/llm
|
||||||
|
X-API-Key: lbk_your_api_key_here
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "My Custom Model",
|
||||||
|
"description": "Description of the model",
|
||||||
|
"requester": "openai-chat-completions",
|
||||||
|
"requester_config": {
|
||||||
|
"model": "gpt-4",
|
||||||
|
"args": {}
|
||||||
|
},
|
||||||
|
"api_keys": [
|
||||||
|
{
|
||||||
|
"name": "default",
|
||||||
|
"keys": ["sk-..."]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"abilities": ["chat"],
|
||||||
|
"extra_args": {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update an LLM Model
|
||||||
|
|
||||||
|
```http
|
||||||
|
PUT /api/v1/provider/models/llm/{model_uuid}
|
||||||
|
X-API-Key: lbk_your_api_key_here
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "Updated Model Name",
|
||||||
|
"description": "Updated description",
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Delete an LLM Model
|
||||||
|
|
||||||
|
```http
|
||||||
|
DELETE /api/v1/provider/models/llm/{model_uuid}
|
||||||
|
X-API-Key: lbk_your_api_key_here
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example: Bot Management
|
||||||
|
|
||||||
|
### List All Bots
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/platform/bots
|
||||||
|
X-API-Key: lbk_your_api_key_here
|
||||||
|
```
|
||||||
|
|
||||||
|
### Create a New Bot
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/v1/platform/bots
|
||||||
|
X-API-Key: lbk_your_api_key_here
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "My Bot",
|
||||||
|
"adapter": "telegram",
|
||||||
|
"config": {...}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example: Pipeline Management
|
||||||
|
|
||||||
|
### List All Pipelines
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/pipelines
|
||||||
|
X-API-Key: lbk_your_api_key_here
|
||||||
|
```
|
||||||
|
|
||||||
|
### Create a New Pipeline
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/v1/pipelines
|
||||||
|
X-API-Key: lbk_your_api_key_here
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "My Pipeline",
|
||||||
|
"config": {...}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Responses
|
||||||
|
|
||||||
|
### 401 Unauthorized
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": -1,
|
||||||
|
"msg": "No valid authentication provided (user token or API key required)"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
or
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": -1,
|
||||||
|
"msg": "Invalid API key"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 404 Not Found
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": -1,
|
||||||
|
"msg": "Resource not found"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 500 Internal Server Error
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": -2,
|
||||||
|
"msg": "Error message details"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Best Practices
|
||||||
|
|
||||||
|
1. **Keep API keys secure**: Store them securely and never commit them to version control
|
||||||
|
2. **Use HTTPS**: Always use HTTPS in production to encrypt API key transmission
|
||||||
|
3. **Rotate keys regularly**: Create new API keys periodically and delete old ones
|
||||||
|
4. **Use descriptive names**: Give your API keys meaningful names to track their usage
|
||||||
|
5. **Delete unused keys**: Remove API keys that are no longer needed
|
||||||
|
6. **Use X-API-Key header**: Prefer using the `X-API-Key` header for clarity
|
||||||
|
|
||||||
|
## Example: Python Client
|
||||||
|
|
||||||
|
```python
|
||||||
|
import requests
|
||||||
|
|
||||||
|
API_KEY = "lbk_your_api_key_here"
|
||||||
|
BASE_URL = "http://your-langbot-server:5300"
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"X-API-Key": API_KEY,
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
# List all models
|
||||||
|
response = requests.get(f"{BASE_URL}/api/v1/provider/models/llm", headers=headers)
|
||||||
|
models = response.json()["data"]["models"]
|
||||||
|
|
||||||
|
print(f"Found {len(models)} models")
|
||||||
|
for model in models:
|
||||||
|
print(f"- {model['name']}: {model['description']}")
|
||||||
|
|
||||||
|
# Create a new bot
|
||||||
|
bot_data = {
|
||||||
|
"name": "My Telegram Bot",
|
||||||
|
"adapter": "telegram",
|
||||||
|
"config": {
|
||||||
|
"token": "your-telegram-token"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.post(
|
||||||
|
f"{BASE_URL}/api/v1/platform/bots",
|
||||||
|
headers=headers,
|
||||||
|
json=bot_data
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
bot_uuid = response.json()["data"]["uuid"]
|
||||||
|
print(f"Bot created with UUID: {bot_uuid}")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example: cURL
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List all models
|
||||||
|
curl -X GET \
|
||||||
|
-H "X-API-Key: lbk_your_api_key_here" \
|
||||||
|
http://your-langbot-server:5300/api/v1/provider/models/llm
|
||||||
|
|
||||||
|
# Create a new pipeline
|
||||||
|
curl -X POST \
|
||||||
|
-H "X-API-Key: lbk_your_api_key_here" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"name": "My Pipeline",
|
||||||
|
"config": {...}
|
||||||
|
}' \
|
||||||
|
http://your-langbot-server:5300/api/v1/pipelines
|
||||||
|
|
||||||
|
# Get bot logs
|
||||||
|
curl -X POST \
|
||||||
|
-H "X-API-Key: lbk_your_api_key_here" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"from_index": -1,
|
||||||
|
"max_count": 10
|
||||||
|
}' \
|
||||||
|
http://your-langbot-server:5300/api/v1/platform/bots/{bot_uuid}/logs
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- The same endpoints work for both the web UI (with user tokens) and external services (with API keys)
|
||||||
|
- No need to learn different API paths - use the existing API documentation with API key authentication
|
||||||
|
- All endpoints that previously required user authentication now also accept API keys
|
||||||
|
|
||||||
412
docs/MIGRATION_SUMMARY.md
Normal file
412
docs/MIGRATION_SUMMARY.md
Normal file
@@ -0,0 +1,412 @@
|
|||||||
|
# WebChat 到 WebSocket 迁移总结
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
已完全移除旧的基于SSE的WebChat系统,并替换为基于WebSocket的双向实时通信系统。这是一个内置在LangBot中的完整IM系统,支持流式输出。
|
||||||
|
|
||||||
|
## 已删除的文件
|
||||||
|
|
||||||
|
### 后端
|
||||||
|
- ❌ `src/langbot/pkg/api/http/controller/groups/pipelines/webchat.py` - 旧的SSE路由
|
||||||
|
- ❌ `src/langbot/pkg/platform/sources/webchat.py` - 旧的WebChat适配器
|
||||||
|
- ❌ `src/langbot/pkg/platform/sources/webchat.yaml` - 旧的配置文件
|
||||||
|
|
||||||
|
### 前端
|
||||||
|
- ❌ BackendClient中所有SSE相关代码已完全移除
|
||||||
|
- ❌ DebugDialog中所有SSE相关逻辑已完全替换
|
||||||
|
|
||||||
|
## 新增的文件
|
||||||
|
|
||||||
|
### 后端核心文件
|
||||||
|
|
||||||
|
**1. WebSocket连接管理器**
|
||||||
|
```
|
||||||
|
src/langbot/pkg/platform/sources/websocket_manager.py
|
||||||
|
```
|
||||||
|
- 管理所有并发WebSocket连接
|
||||||
|
- 线程安全的连接池
|
||||||
|
- 按流水线、会话类型分组
|
||||||
|
- 广播和单播消息功能
|
||||||
|
- 连接统计和监控
|
||||||
|
|
||||||
|
**2. WebSocket适配器**
|
||||||
|
```
|
||||||
|
src/langbot/pkg/platform/sources/websocket_adapter.py
|
||||||
|
```
|
||||||
|
- 实现平台适配器接口
|
||||||
|
- **完整流式支持** (`reply_message_chunk` 方法)
|
||||||
|
- 双向消息流处理
|
||||||
|
- 消息历史管理
|
||||||
|
- 会话管理
|
||||||
|
|
||||||
|
**3. WebSocket路由控制器**
|
||||||
|
```
|
||||||
|
src/langbot/pkg/api/http/controller/groups/pipelines/websocket_chat.py
|
||||||
|
```
|
||||||
|
- WebSocket端点处理
|
||||||
|
- REST API接口
|
||||||
|
- 心跳机制
|
||||||
|
- 连接生命周期管理
|
||||||
|
|
||||||
|
**4. 配置文件**
|
||||||
|
```
|
||||||
|
src/langbot/pkg/platform/sources/websocket.yaml
|
||||||
|
```
|
||||||
|
- WebSocket适配器元数据
|
||||||
|
|
||||||
|
### 前端核心文件
|
||||||
|
|
||||||
|
**1. WebSocket客户端**
|
||||||
|
```
|
||||||
|
web/src/app/infra/websocket/WebSocketClient.ts
|
||||||
|
```
|
||||||
|
- WebSocket连接管理
|
||||||
|
- 自动重连(最多5次)
|
||||||
|
- 心跳机制(30秒)
|
||||||
|
- 事件回调系统
|
||||||
|
|
||||||
|
**2. 更新的组件**
|
||||||
|
```
|
||||||
|
web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx
|
||||||
|
```
|
||||||
|
- 完全重写,使用WebSocket
|
||||||
|
- 实时连接状态显示
|
||||||
|
- 流式消息支持
|
||||||
|
- 自动重连
|
||||||
|
|
||||||
|
**3. HTTP客户端更新**
|
||||||
|
```
|
||||||
|
web/src/app/infra/http/BackendClient.ts
|
||||||
|
```
|
||||||
|
- 移除所有旧的WebChat API
|
||||||
|
- 仅保留WebSocket API
|
||||||
|
|
||||||
|
### 测试工具
|
||||||
|
|
||||||
|
**Python测试客户端**
|
||||||
|
```
|
||||||
|
test_websocket_client.py
|
||||||
|
```
|
||||||
|
- 单连接交互测试
|
||||||
|
- 多连接并发测试
|
||||||
|
- 命令行工具
|
||||||
|
|
||||||
|
### 文档
|
||||||
|
|
||||||
|
**使用文档**
|
||||||
|
```
|
||||||
|
WEBSOCKET_README.md
|
||||||
|
```
|
||||||
|
- 完整的API文档
|
||||||
|
- 架构说明
|
||||||
|
- 使用示例
|
||||||
|
- 故障排查
|
||||||
|
|
||||||
|
## 核心变更
|
||||||
|
|
||||||
|
### 后端变更
|
||||||
|
|
||||||
|
**1. botmgr.py**
|
||||||
|
- ❌ 移除 `webchat_proxy_bot`
|
||||||
|
- ✅ 仅保留 `websocket_proxy_bot`
|
||||||
|
- ✅ 更新适配器过滤逻辑(排除`websocket`而非`webchat`)
|
||||||
|
|
||||||
|
**2. 适配器注册**
|
||||||
|
```python
|
||||||
|
# 旧代码(已删除)
|
||||||
|
webchat_adapter_class = self.adapter_dict['webchat']
|
||||||
|
self.webchat_proxy_bot = RuntimeBot(...)
|
||||||
|
|
||||||
|
# 新代码
|
||||||
|
websocket_adapter_class = self.adapter_dict['websocket']
|
||||||
|
self.websocket_proxy_bot = RuntimeBot(
|
||||||
|
uuid='websocket-proxy-bot',
|
||||||
|
name='WebSocket',
|
||||||
|
adapter='websocket',
|
||||||
|
...
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 前端变更
|
||||||
|
|
||||||
|
**1. API调用完全更换**
|
||||||
|
|
||||||
|
旧代码(已删除):
|
||||||
|
```typescript
|
||||||
|
// SSE流式请求
|
||||||
|
await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ is_stream: true })
|
||||||
|
})
|
||||||
|
// 手动解析 text/event-stream
|
||||||
|
```
|
||||||
|
|
||||||
|
新代码:
|
||||||
|
```typescript
|
||||||
|
// WebSocket实时通信
|
||||||
|
const wsClient = new WebSocketClient(pipelineId, sessionType);
|
||||||
|
await wsClient.connect();
|
||||||
|
|
||||||
|
wsClient.onMessage((message) => {
|
||||||
|
// 流式消息自动处理
|
||||||
|
setMessages(prev => [...prev, message]);
|
||||||
|
});
|
||||||
|
|
||||||
|
wsClient.sendMessage(messageChain);
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. 连接状态管理**
|
||||||
|
|
||||||
|
新增功能:
|
||||||
|
- ✅ 实时连接状态指示器(绿色/红色圆点)
|
||||||
|
- ✅ 连接/断开toast提示
|
||||||
|
- ✅ 自动重连逻辑
|
||||||
|
- ✅ 心跳保活
|
||||||
|
|
||||||
|
**3. 流式支持**
|
||||||
|
|
||||||
|
完整的流式消息处理:
|
||||||
|
```typescript
|
||||||
|
wsClient.onMessage((message) => {
|
||||||
|
if (message.is_final) {
|
||||||
|
// 最终消息
|
||||||
|
finalizeBotMessage(message);
|
||||||
|
} else {
|
||||||
|
// 中间消息块,实时更新UI
|
||||||
|
updateBotMessage(message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## API对比
|
||||||
|
|
||||||
|
### WebSocket端点
|
||||||
|
|
||||||
|
**连接**
|
||||||
|
```
|
||||||
|
ws://localhost:8000/api/v1/pipelines/<pipeline_uuid>/ws/connect?session_type=<person|group>
|
||||||
|
```
|
||||||
|
|
||||||
|
**消息格式**
|
||||||
|
|
||||||
|
客户端发送:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "message",
|
||||||
|
"message": [
|
||||||
|
{"type": "Plain", "text": "你好"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
服务器响应(流式):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "response",
|
||||||
|
"data": {
|
||||||
|
"id": 1,
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "你好,我是...",
|
||||||
|
"is_final": false,
|
||||||
|
"timestamp": "2025-01-28T..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### REST API
|
||||||
|
|
||||||
|
| 端点 | 方法 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `/api/v1/pipelines/<uuid>/ws/messages/<type>` | GET | 获取消息历史 |
|
||||||
|
| `/api/v1/pipelines/<uuid>/ws/reset/<type>` | POST | 重置会话 |
|
||||||
|
| `/api/v1/pipelines/<uuid>/ws/connections` | GET | 获取连接统计 |
|
||||||
|
| `/api/v1/pipelines/<uuid>/ws/broadcast` | POST | 广播消息 |
|
||||||
|
|
||||||
|
## 流式支持详解
|
||||||
|
|
||||||
|
### 后端流式实现
|
||||||
|
|
||||||
|
**WebSocket Adapter**
|
||||||
|
```python
|
||||||
|
async def reply_message_chunk(
|
||||||
|
self,
|
||||||
|
message_source: platform_events.MessageEvent,
|
||||||
|
bot_message,
|
||||||
|
message: platform_message.MessageChain,
|
||||||
|
quote_origin: bool = False,
|
||||||
|
is_final: bool = False,
|
||||||
|
) -> dict:
|
||||||
|
"""回复消息块 - 流式"""
|
||||||
|
message_data = WebSocketMessage(
|
||||||
|
id=-1,
|
||||||
|
role='assistant',
|
||||||
|
content=str(message),
|
||||||
|
message_chain=[component.__dict__ for component in message],
|
||||||
|
timestamp=datetime.now().isoformat(),
|
||||||
|
is_final=is_final and bot_message.tool_calls is None,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 发送到队列,由WebSocket连接处理发送
|
||||||
|
await session.resp_queues[message_id].put(message_data)
|
||||||
|
return message_data.model_dump()
|
||||||
|
|
||||||
|
async def is_stream_output_supported(self) -> bool:
|
||||||
|
"""WebSocket始终支持流式输出"""
|
||||||
|
return True
|
||||||
|
```
|
||||||
|
|
||||||
|
### 前端流式处理
|
||||||
|
|
||||||
|
**DebugDialog组件**
|
||||||
|
```typescript
|
||||||
|
wsClient.onMessage((message) => {
|
||||||
|
setMessages((prevMessages) => {
|
||||||
|
const existingIndex = prevMessages.findIndex(
|
||||||
|
(msg) => msg.role === 'assistant' && msg.content === 'Generating...'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingIndex !== -1) {
|
||||||
|
// 更新正在生成的消息
|
||||||
|
const updatedMessages = [...prevMessages];
|
||||||
|
updatedMessages[existingIndex] = message;
|
||||||
|
return updatedMessages;
|
||||||
|
} else {
|
||||||
|
// 添加新消息
|
||||||
|
return [...prevMessages, message];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 兼容性说明
|
||||||
|
|
||||||
|
### ⚠️ 不兼容旧版本
|
||||||
|
|
||||||
|
此次迁移**完全不兼容**旧的WebChat系统:
|
||||||
|
|
||||||
|
1. **API端点变更**
|
||||||
|
- 旧: `/api/v1/pipelines/<uuid>/chat/send`
|
||||||
|
- 新: `ws://.../<uuid>/ws/connect`
|
||||||
|
|
||||||
|
2. **通信协议变更**
|
||||||
|
- 旧: HTTP + SSE (Server-Sent Events)
|
||||||
|
- 新: WebSocket (双向)
|
||||||
|
|
||||||
|
3. **流式实现变更**
|
||||||
|
- 旧: `text/event-stream` 格式
|
||||||
|
- 新: WebSocket JSON消息
|
||||||
|
|
||||||
|
### 迁移要求
|
||||||
|
|
||||||
|
使用新系统需要:
|
||||||
|
1. ✅ 前端必须支持WebSocket
|
||||||
|
2. ✅ 后端必须运行新的WebSocket适配器
|
||||||
|
3. ✅ 清除旧的WebChat相关配置
|
||||||
|
|
||||||
|
## 优势对比
|
||||||
|
|
||||||
|
| 特性 | 旧WebChat (SSE) | 新WebSocket |
|
||||||
|
|------|----------------|-------------|
|
||||||
|
| 双向通信 | ❌ 单向(服务器→客户端) | ✅ 双向 |
|
||||||
|
| 主动推送 | ❌ 不支持 | ✅ 支持 |
|
||||||
|
| 连接管理 | ❌ 无状态 | ✅ 有状态,完整生命周期 |
|
||||||
|
| 流式输出 | ✅ 支持 | ✅ 支持(更优) |
|
||||||
|
| 心跳机制 | ❌ 无 | ✅ 30秒心跳 |
|
||||||
|
| 自动重连 | ❌ 无 | ✅ 最多5次 |
|
||||||
|
| 多连接 | ⚠️ 难以管理 | ✅ 完整支持 |
|
||||||
|
| 连接状态 | ❌ 不可见 | ✅ 实时显示 |
|
||||||
|
| 广播功能 | ❌ 不支持 | ✅ 支持 |
|
||||||
|
|
||||||
|
## 测试方式
|
||||||
|
|
||||||
|
### 1. Python测试客户端
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 单连接测试
|
||||||
|
python test_websocket_client.py <pipeline_uuid>
|
||||||
|
|
||||||
|
# 指定会话类型
|
||||||
|
python test_websocket_client.py <pipeline_uuid> --session-type group
|
||||||
|
|
||||||
|
# 多连接并发测试(5个连接)
|
||||||
|
python test_websocket_client.py <pipeline_uuid> --multi 5
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 前端测试
|
||||||
|
|
||||||
|
1. 启动LangBot服务器
|
||||||
|
2. 访问前端界面
|
||||||
|
3. 打开流水线调试对话框
|
||||||
|
4. 观察连接状态指示器(左下角圆点)
|
||||||
|
5. 发送消息测试流式响应
|
||||||
|
|
||||||
|
### 3. 浏览器控制台测试
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const ws = new WebSocket('ws://localhost:8000/api/v1/pipelines/<uuid>/ws/connect?session_type=person');
|
||||||
|
|
||||||
|
ws.onopen = () => {
|
||||||
|
console.log('已连接');
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: 'message',
|
||||||
|
message: [{type: 'Plain', text: '你好'}]
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
console.log('收到:', JSON.parse(event.data));
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### Q: 为什么完全删除旧代码而不保留兼容性?
|
||||||
|
A: 根据需求,不需要考虑任何对老版本的兼容性,彻底迁移可以避免代码冗余和维护负担。
|
||||||
|
|
||||||
|
### Q: 流式输出如何工作?
|
||||||
|
A:
|
||||||
|
1. 后端通过`reply_message_chunk`发送消息块
|
||||||
|
2. 消息块放入队列
|
||||||
|
3. WebSocket连接从队列取出并发送
|
||||||
|
4. 前端实时更新UI
|
||||||
|
5. `is_final=true`表示最后一块
|
||||||
|
|
||||||
|
### Q: 如何确保连接不断开?
|
||||||
|
A:
|
||||||
|
1. 客户端每30秒发送心跳(ping)
|
||||||
|
2. 服务器响应pong
|
||||||
|
3. 连接断开时自动重连(最多5次)
|
||||||
|
|
||||||
|
### Q: 如何实现后端主动推送?
|
||||||
|
A:
|
||||||
|
1. 调用 `/api/v1/pipelines/<uuid>/ws/broadcast` API
|
||||||
|
2. 消息会被推送到该流水线的所有连接
|
||||||
|
3. 前端通过`onBroadcast`回调接收
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
✅ **完成的工作**
|
||||||
|
- 完全移除旧的WebChat/SSE系统
|
||||||
|
- 实现完整的WebSocket双向通信系统
|
||||||
|
- 支持流式输出
|
||||||
|
- 支持多连接并发
|
||||||
|
- 实现自动重连和心跳机制
|
||||||
|
- 提供完整的测试工具和文档
|
||||||
|
|
||||||
|
✅ **核心特性**
|
||||||
|
- 双向实时通信
|
||||||
|
- 流式消息支持
|
||||||
|
- 多连接管理
|
||||||
|
- 自动重连
|
||||||
|
- 心跳保活
|
||||||
|
- 连接状态可视化
|
||||||
|
- 广播消息
|
||||||
|
|
||||||
|
✅ **技术亮点**
|
||||||
|
- 异步架构(asyncio)
|
||||||
|
- 线程安全的连接管理
|
||||||
|
- 独立的消息队列
|
||||||
|
- 完整的错误处理
|
||||||
|
- 模块化设计
|
||||||
|
|
||||||
|
🎉 系统已完全迁移到WebSocket,无任何旧代码遗留!
|
||||||
117
docs/PYPI_INSTALLATION.md
Normal file
117
docs/PYPI_INSTALLATION.md
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
# LangBot PyPI Package Installation
|
||||||
|
|
||||||
|
## Quick Start with uvx
|
||||||
|
|
||||||
|
The easiest way to run LangBot is using `uvx` (recommended for quick testing):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uvx langbot
|
||||||
|
```
|
||||||
|
|
||||||
|
This will automatically download and run the latest version of LangBot.
|
||||||
|
|
||||||
|
## Install with pip/uv
|
||||||
|
|
||||||
|
You can also install LangBot as a regular Python package:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Using pip
|
||||||
|
pip install langbot
|
||||||
|
|
||||||
|
# Using uv
|
||||||
|
uv pip install langbot
|
||||||
|
```
|
||||||
|
|
||||||
|
Then run it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
langbot
|
||||||
|
```
|
||||||
|
|
||||||
|
Or using Python module syntax:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m langbot
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installation with Frontend
|
||||||
|
|
||||||
|
When published to PyPI, the LangBot package includes the pre-built frontend files. You don't need to build the frontend separately.
|
||||||
|
|
||||||
|
## Data Directory
|
||||||
|
|
||||||
|
When running LangBot as a package, it will create a `data/` directory in your current working directory to store configuration, logs, and other runtime data. You can run LangBot from any directory, and it will set up its data directory there.
|
||||||
|
|
||||||
|
## Command Line Options
|
||||||
|
|
||||||
|
LangBot supports the following command line options:
|
||||||
|
|
||||||
|
- `--standalone-runtime`: Use standalone plugin runtime
|
||||||
|
- `--debug`: Enable debug mode
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
langbot --debug
|
||||||
|
```
|
||||||
|
|
||||||
|
## Comparison with Other Installation Methods
|
||||||
|
|
||||||
|
### PyPI Package (uvx/pip)
|
||||||
|
- **Pros**: Easy to install and update, no need to clone repository or build frontend
|
||||||
|
- **Cons**: Less flexible for development/customization
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
- **Pros**: Isolated environment, easy deployment
|
||||||
|
- **Cons**: Requires Docker
|
||||||
|
|
||||||
|
### Manual Source Installation
|
||||||
|
- **Pros**: Full control, easy to customize and develop
|
||||||
|
- **Cons**: Requires building frontend, managing dependencies manually
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
If you want to contribute or customize LangBot, you should still use the manual installation method by cloning the repository:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/langbot-app/LangBot
|
||||||
|
cd LangBot
|
||||||
|
uv sync
|
||||||
|
cd web
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
cd ..
|
||||||
|
uv run main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Updating
|
||||||
|
|
||||||
|
To update to the latest version:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# With pip
|
||||||
|
pip install --upgrade langbot
|
||||||
|
|
||||||
|
# With uv
|
||||||
|
uv pip install --upgrade langbot
|
||||||
|
|
||||||
|
# With uvx (automatically uses latest)
|
||||||
|
uvx langbot
|
||||||
|
```
|
||||||
|
|
||||||
|
## System Requirements
|
||||||
|
|
||||||
|
- Python 3.10.1 or higher
|
||||||
|
- Operating System: Linux, macOS, or Windows
|
||||||
|
|
||||||
|
## Differences from Source Installation
|
||||||
|
|
||||||
|
When running LangBot from the PyPI package (via uvx or pip), there are a few behavioral differences compared to running from source:
|
||||||
|
|
||||||
|
1. **Version Check**: The package version does not prompt for user input when the Python version is incompatible. It simply prints an error message and exits. This makes it compatible with non-interactive environments like containers and CI/CD.
|
||||||
|
|
||||||
|
2. **Working Directory**: The package version does not require being run from the LangBot project root. You can run `langbot` from any directory, and it will create a `data/` directory in your current working directory.
|
||||||
|
|
||||||
|
3. **Frontend Files**: The frontend is pre-built and included in the package, so you don't need to run `npm build` separately.
|
||||||
|
|
||||||
|
These differences are intentional to make the package more user-friendly and suitable for various deployment scenarios.
|
||||||
259
docs/SEEKDB_INTEGRATION.md
Normal file
259
docs/SEEKDB_INTEGRATION.md
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
# SeekDB Vector Database Integration
|
||||||
|
|
||||||
|
This document describes how to use OceanBase SeekDB as the vector database backend for LangBot's knowledge base feature.
|
||||||
|
|
||||||
|
## What is SeekDB?
|
||||||
|
|
||||||
|
**OceanBase SeekDB** is an AI-native search database that unifies relational, vector, text, JSON and GIS in a single engine, enabling hybrid search and in-database AI workflows. It's developed by OceanBase and released under Apache 2.0 license.
|
||||||
|
|
||||||
|
### Key Features
|
||||||
|
|
||||||
|
- **Hybrid Search**: Combine vector search, full-text search and relational query in a single statement
|
||||||
|
- **Multi-Model Support**: Support relational, vector, text, JSON and GIS in a single engine
|
||||||
|
- **Lightweight**: Requires as little as 1 CPU core and 2 GB of memory
|
||||||
|
- **Multiple Deployment Modes**: Supports both embedded mode and client/server mode
|
||||||
|
- **MySQL Compatible**: Powered by OceanBase engine with full ACID compliance and MySQL compatibility
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
SeekDB support is automatically included when you install LangBot. The required dependency `pyseekdb` is listed in `pyproject.toml`.
|
||||||
|
|
||||||
|
If you need to install it manually:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install pyseekdb
|
||||||
|
```
|
||||||
|
|
||||||
|
## ⚠️ Platform Compatibility
|
||||||
|
|
||||||
|
### Embedded Mode
|
||||||
|
|
||||||
|
| Platform | Status | Notes |
|
||||||
|
|----------|--------|-------|
|
||||||
|
| Linux | ✅ Supported | Full embedded mode support via `pylibseekdb` |
|
||||||
|
| macOS | ❌ Not Supported | `pylibseekdb` is Linux-only; use server mode instead |
|
||||||
|
| Windows | ❌ Not Supported | `pylibseekdb` is Linux-only; use server mode instead |
|
||||||
|
|
||||||
|
**Important**: Embedded mode requires the `pylibseekdb` library, which is only available on Linux. If you're on macOS or Windows, you must use server mode.
|
||||||
|
|
||||||
|
### Server Mode (Docker)
|
||||||
|
|
||||||
|
| Platform | Status | Notes |
|
||||||
|
|----------|--------|-------|
|
||||||
|
| Linux | ✅ Supported | Full Docker support |
|
||||||
|
| macOS | ⚠️ Known Issue | Docker container initialization failure - [See Issue #36](https://github.com/oceanbase/seekdb/issues/36) |
|
||||||
|
| Windows | ⚠️ Untested | Should work but not yet tested |
|
||||||
|
|
||||||
|
**macOS Users**: Currently, SeekDB Docker containers have an initialization issue on macOS ([oceanbase/seekdb#36](https://github.com/oceanbase/seekdb/issues/36)). Until this is resolved, we recommend:
|
||||||
|
- Using ChromaDB or Qdrant as alternatives
|
||||||
|
- Connecting to a remote SeekDB server on Linux if available
|
||||||
|
|
||||||
|
### Server Mode (Remote Connection)
|
||||||
|
|
||||||
|
| Platform | Status | Notes |
|
||||||
|
|----------|--------|-------|
|
||||||
|
| All Platforms | ✅ Supported | Connect to SeekDB running on a remote Linux server |
|
||||||
|
|
||||||
|
**Recommendation for macOS/Windows users**: Deploy SeekDB on a Linux server and connect via server mode configuration.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Embedded Mode (Recommended for Development)
|
||||||
|
|
||||||
|
Embedded mode runs SeekDB directly within the LangBot process, storing data locally. This is the simplest setup and requires no external services.
|
||||||
|
|
||||||
|
Edit your `config.yaml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
vdb:
|
||||||
|
use: seekdb
|
||||||
|
seekdb:
|
||||||
|
mode: embedded
|
||||||
|
path: './data/seekdb' # Path to store SeekDB data
|
||||||
|
database: 'langbot' # Database name
|
||||||
|
```
|
||||||
|
|
||||||
|
### Server Mode (For Production)
|
||||||
|
|
||||||
|
Server mode connects to a remote SeekDB server or OceanBase server. This is recommended for production deployments.
|
||||||
|
|
||||||
|
#### SeekDB Server
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
vdb:
|
||||||
|
use: seekdb
|
||||||
|
seekdb:
|
||||||
|
mode: server
|
||||||
|
host: 'localhost'
|
||||||
|
port: 2881
|
||||||
|
database: 'langbot'
|
||||||
|
user: 'root'
|
||||||
|
password: '' # Can also use SEEKDB_PASSWORD env var
|
||||||
|
```
|
||||||
|
|
||||||
|
#### OceanBase Server
|
||||||
|
|
||||||
|
If you're using OceanBase with seekdb capabilities:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
vdb:
|
||||||
|
use: seekdb
|
||||||
|
seekdb:
|
||||||
|
mode: server
|
||||||
|
host: 'localhost'
|
||||||
|
port: 2881
|
||||||
|
tenant: 'sys' # OceanBase tenant name
|
||||||
|
database: 'langbot'
|
||||||
|
user: 'root'
|
||||||
|
password: ''
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration Parameters
|
||||||
|
|
||||||
|
| Parameter | Required | Default | Description |
|
||||||
|
|-----------|----------|--------------|-------------|
|
||||||
|
| `mode` | No | `embedded` | Deployment mode: `embedded` or `server` |
|
||||||
|
| `path` | No | `./data/seekdb` | Data directory for embedded mode |
|
||||||
|
| `database` | No | `langbot` | Database name |
|
||||||
|
| `host` | No | `localhost` | Server host (server mode only) |
|
||||||
|
| `port` | No | `2881` | Server port (server mode only) |
|
||||||
|
| `user` | No | `root` | Username (server mode only) |
|
||||||
|
| `password` | No | `''` | Password (server mode only) |
|
||||||
|
| `tenant` | No | None | OceanBase tenant (optional, server mode only) |
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Once configured, SeekDB will be used automatically for all knowledge base operations in LangBot:
|
||||||
|
|
||||||
|
1. **Creating Knowledge Bases**: Vectors will be stored in SeekDB collections
|
||||||
|
2. **Adding Documents**: Document embeddings will be indexed in SeekDB
|
||||||
|
3. **Searching**: Vector similarity search will use SeekDB's efficient indexing
|
||||||
|
4. **Deleting**: Document removal will delete vectors from SeekDB
|
||||||
|
|
||||||
|
No code changes are required - just update your configuration!
|
||||||
|
|
||||||
|
## Architecture Details
|
||||||
|
|
||||||
|
### Implementation
|
||||||
|
|
||||||
|
The SeekDB adapter is implemented in `src/langbot/pkg/vector/vdbs/seekdb.py` and follows the same `VectorDatabase` interface as Chroma and Qdrant adapters.
|
||||||
|
|
||||||
|
Key methods:
|
||||||
|
- `add_embeddings()`: Add vectors with metadata to a collection
|
||||||
|
- `search()`: Perform vector similarity search
|
||||||
|
- `delete_by_file_id()`: Delete vectors by file ID metadata
|
||||||
|
- `get_or_create_collection()`: Manage collections
|
||||||
|
- `delete_collection()`: Remove entire collections
|
||||||
|
|
||||||
|
### Vector Storage
|
||||||
|
|
||||||
|
- Collections are created with HNSW (Hierarchical Navigable Small World) index
|
||||||
|
- Default distance metric: Cosine similarity
|
||||||
|
- Default vector dimension: 384 (adjusts automatically based on embeddings)
|
||||||
|
- Metadata is stored alongside vectors for filtering
|
||||||
|
|
||||||
|
## Advantages Over Other Vector Databases
|
||||||
|
|
||||||
|
### vs. ChromaDB
|
||||||
|
- ✅ Better MySQL compatibility
|
||||||
|
- ✅ Hybrid search capabilities (vector + full-text + SQL)
|
||||||
|
- ✅ Production-grade distributed mode support
|
||||||
|
- ✅ Lightweight embedded mode
|
||||||
|
|
||||||
|
### vs. Qdrant
|
||||||
|
- ✅ SQL query support
|
||||||
|
- ✅ MySQL ecosystem integration
|
||||||
|
- ✅ Simpler deployment (no Docker required for embedded mode)
|
||||||
|
- ✅ Multi-model data support (not just vectors)
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Import Error
|
||||||
|
|
||||||
|
If you see: `ImportError: pyseekdb is not installed`
|
||||||
|
|
||||||
|
Solution:
|
||||||
|
```bash
|
||||||
|
pip install pyseekdb
|
||||||
|
```
|
||||||
|
|
||||||
|
### Embedded Mode Error on macOS/Windows
|
||||||
|
|
||||||
|
**Error**:
|
||||||
|
```
|
||||||
|
RuntimeError: Embedded Client is not available because pylibseekdb is not available.
|
||||||
|
Please install pylibseekdb (Linux only) or use RemoteServerClient (host/port) instead.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cause**: `pylibseekdb` is only available on Linux platforms.
|
||||||
|
|
||||||
|
**Solution**: Use server mode instead:
|
||||||
|
1. Deploy SeekDB on a Linux server or VM
|
||||||
|
2. Configure LangBot to use server mode:
|
||||||
|
```yaml
|
||||||
|
vdb:
|
||||||
|
use: seekdb
|
||||||
|
seekdb:
|
||||||
|
mode: server
|
||||||
|
host: 'your-seekdb-server-ip'
|
||||||
|
port: 2881
|
||||||
|
database: 'langbot'
|
||||||
|
user: 'root'
|
||||||
|
password: ''
|
||||||
|
```
|
||||||
|
|
||||||
|
**Alternative**: Use ChromaDB or Qdrant, which work on all platforms:
|
||||||
|
```yaml
|
||||||
|
vdb:
|
||||||
|
use: chroma # or qdrant
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker Container Fails on macOS
|
||||||
|
|
||||||
|
**Symptoms**:
|
||||||
|
```bash
|
||||||
|
docker run -d -p 2881:2881 oceanbase/seekdb:latest
|
||||||
|
# Container exits immediately with code 30
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error in logs**:
|
||||||
|
```
|
||||||
|
[ERROR] Code: Agent.SeekDB.Not.Exists
|
||||||
|
Message: initialize failed: init agent failed: SeekDB not exists in current directory.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cause**: This is a known issue with SeekDB Docker containers on macOS. See [oceanbase/seekdb#36](https://github.com/oceanbase/seekdb/issues/36).
|
||||||
|
|
||||||
|
**Status**: Under investigation by OceanBase team.
|
||||||
|
|
||||||
|
**Workaround Options**:
|
||||||
|
1. **Use alternatives**: ChromaDB or Qdrant work perfectly on macOS
|
||||||
|
2. **Remote server**: Deploy SeekDB on a Linux server and connect remotely
|
||||||
|
3. **Wait for fix**: Monitor the GitHub issue for updates
|
||||||
|
|
||||||
|
### Connection Error (Server Mode)
|
||||||
|
|
||||||
|
If SeekDB server is not reachable, check:
|
||||||
|
1. Server is running: `ps aux | grep observer`
|
||||||
|
2. Port is accessible: `nc -zv localhost 2881`
|
||||||
|
3. Credentials are correct in config
|
||||||
|
4. Firewall allows connections on port 2881
|
||||||
|
|
||||||
|
### Performance Issues
|
||||||
|
|
||||||
|
For large datasets:
|
||||||
|
- Use server mode instead of embedded mode
|
||||||
|
- Ensure adequate memory allocation
|
||||||
|
- Consider using OceanBase distributed mode for very large scale
|
||||||
|
- Adjust HNSW index parameters if needed
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- SeekDB GitHub: https://github.com/oceanbase/seekdb
|
||||||
|
- pyseekdb SDK: https://github.com/oceanbase/pyseekdb
|
||||||
|
- OceanBase Documentation: https://oceanbase.ai
|
||||||
|
- LangBot Documentation: https://docs.langbot.app
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
SeekDB is licensed under Apache License 2.0.
|
||||||
394
docs/WEBSOCKET_README.md
Normal file
394
docs/WEBSOCKET_README.md
Normal file
@@ -0,0 +1,394 @@
|
|||||||
|
# LangBot WebSocket 双向通信系统
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
这是一个内置在 LangBot 中的完整 IM (即时通讯) 系统,支持:
|
||||||
|
|
||||||
|
- ✅ WebSocket 双向实时通信
|
||||||
|
- ✅ 多个客户端并发连接
|
||||||
|
- ✅ 前端到后端的消息发送
|
||||||
|
- ✅ 后端到前端的主动推送
|
||||||
|
- ✅ 流式响应支持
|
||||||
|
- ✅ 连接管理和会话隔离
|
||||||
|
- ✅ 心跳机制
|
||||||
|
- ✅ 广播消息功能
|
||||||
|
|
||||||
|
## 架构设计
|
||||||
|
|
||||||
|
### 核心组件
|
||||||
|
|
||||||
|
1. **WebSocketConnectionManager** (`websocket_manager.py`)
|
||||||
|
- 管理所有活跃的 WebSocket 连接
|
||||||
|
- 支持按流水线、会话类型查询连接
|
||||||
|
- 提供广播和单播功能
|
||||||
|
- 线程安全的并发访问控制
|
||||||
|
|
||||||
|
2. **WebSocketAdapter** (`websocket_adapter.py`)
|
||||||
|
- 实现平台适配器接口
|
||||||
|
- 处理消息的接收和发送
|
||||||
|
- 支持流式输出
|
||||||
|
- 管理消息历史
|
||||||
|
|
||||||
|
3. **WebSocketChatRouterGroup** (`websocket_chat.py`)
|
||||||
|
- WebSocket 路由控制器
|
||||||
|
- 处理连接建立、消息收发
|
||||||
|
- 实现心跳机制
|
||||||
|
- 提供 REST API 接口
|
||||||
|
|
||||||
|
## API 接口
|
||||||
|
|
||||||
|
### WebSocket 连接
|
||||||
|
|
||||||
|
#### 建立连接
|
||||||
|
|
||||||
|
```
|
||||||
|
ws://localhost:8000/api/v1/pipelines/<pipeline_uuid>/ws/connect?session_type=<person|group>
|
||||||
|
```
|
||||||
|
|
||||||
|
**参数:**
|
||||||
|
- `pipeline_uuid`: 流水线 UUID (必需)
|
||||||
|
- `session_type`: 会话类型,可选 `person` 或 `group` (默认: `person`)
|
||||||
|
|
||||||
|
**连接成功响应:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "connected",
|
||||||
|
"connection_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"pipeline_uuid": "your-pipeline-uuid",
|
||||||
|
"session_type": "person",
|
||||||
|
"timestamp": "2025-01-28T12:00:00"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 消息格式
|
||||||
|
|
||||||
|
#### 客户端发送消息
|
||||||
|
|
||||||
|
**发送聊天消息:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "message",
|
||||||
|
"message": [
|
||||||
|
{
|
||||||
|
"type": "Plain",
|
||||||
|
"text": "你好,这是一条测试消息"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**发送心跳:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "ping"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**主动断开连接:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "disconnect"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 服务器响应消息
|
||||||
|
|
||||||
|
**聊天响应 (流式):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "response",
|
||||||
|
"data": {
|
||||||
|
"id": 1,
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "这是机器人的回复",
|
||||||
|
"message_chain": [...],
|
||||||
|
"timestamp": "2025-01-28T12:00:00",
|
||||||
|
"is_final": false,
|
||||||
|
"connection_id": "..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**心跳响应:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "pong",
|
||||||
|
"timestamp": "2025-01-28T12:00:00"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**广播消息:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "broadcast",
|
||||||
|
"message": "这是一条广播消息",
|
||||||
|
"timestamp": "2025-01-28T12:00:00"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**错误消息:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "error",
|
||||||
|
"message": "错误描述"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### REST API 接口
|
||||||
|
|
||||||
|
#### 1. 获取消息历史
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/pipelines/<pipeline_uuid>/ws/messages/<session_type>
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"msg": "ok",
|
||||||
|
"data": {
|
||||||
|
"messages": [...]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. 重置会话
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/v1/pipelines/<pipeline_uuid>/ws/reset/<session_type>
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"msg": "ok",
|
||||||
|
"data": {
|
||||||
|
"message": "Session reset successfully"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. 获取连接统计
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/pipelines/<pipeline_uuid>/ws/connections
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"msg": "ok",
|
||||||
|
"data": {
|
||||||
|
"stats": {
|
||||||
|
"total_connections": 5,
|
||||||
|
"pipelines": 2,
|
||||||
|
"connections_by_pipeline": {
|
||||||
|
"pipeline-1": 3,
|
||||||
|
"pipeline-2": 2
|
||||||
|
},
|
||||||
|
"connections_by_session_type": {
|
||||||
|
"person": 4,
|
||||||
|
"group": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"connections": [
|
||||||
|
{
|
||||||
|
"connection_id": "...",
|
||||||
|
"session_type": "person",
|
||||||
|
"created_at": "2025-01-28T12:00:00",
|
||||||
|
"last_active": "2025-01-28T12:05:00",
|
||||||
|
"is_active": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. 广播消息 (后端主动推送)
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/v1/pipelines/<pipeline_uuid>/ws/broadcast
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"message": "这是一条广播消息"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"msg": "ok",
|
||||||
|
"data": {
|
||||||
|
"message": "Broadcast sent successfully"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用示例
|
||||||
|
|
||||||
|
### Python 客户端示例
|
||||||
|
|
||||||
|
使用提供的测试客户端:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 安装依赖
|
||||||
|
pip install websockets
|
||||||
|
|
||||||
|
# 单个连接测试
|
||||||
|
python test_websocket_client.py <pipeline_uuid>
|
||||||
|
|
||||||
|
# 指定会话类型
|
||||||
|
python test_websocket_client.py <pipeline_uuid> --session-type group
|
||||||
|
|
||||||
|
# 多连接并发测试
|
||||||
|
python test_websocket_client.py <pipeline_uuid> --multi 5
|
||||||
|
```
|
||||||
|
|
||||||
|
### JavaScript 客户端示例
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 建立 WebSocket 连接
|
||||||
|
const ws = new WebSocket('ws://localhost:8000/api/v1/pipelines/your-pipeline-uuid/ws/connect?session_type=person');
|
||||||
|
|
||||||
|
// 连接建立
|
||||||
|
ws.onopen = () => {
|
||||||
|
console.log('WebSocket 连接已建立');
|
||||||
|
|
||||||
|
// 发送消息
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: 'message',
|
||||||
|
message: [
|
||||||
|
{
|
||||||
|
type: 'Plain',
|
||||||
|
text: '你好'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 接收消息
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
|
||||||
|
if (data.type === 'connected') {
|
||||||
|
console.log('连接成功:', data.connection_id);
|
||||||
|
} else if (data.type === 'response') {
|
||||||
|
console.log('机器人回复:', data.data.content);
|
||||||
|
if (data.data.is_final) {
|
||||||
|
console.log('响应完成');
|
||||||
|
}
|
||||||
|
} else if (data.type === 'broadcast') {
|
||||||
|
console.log('收到广播:', data.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 连接关闭
|
||||||
|
ws.onclose = () => {
|
||||||
|
console.log('WebSocket 连接已关闭');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 错误处理
|
||||||
|
ws.onerror = (error) => {
|
||||||
|
console.error('WebSocket 错误:', error);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 发送心跳
|
||||||
|
setInterval(() => {
|
||||||
|
if (ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send(JSON.stringify({ type: 'ping' }));
|
||||||
|
}
|
||||||
|
}, 30000); // 每 30 秒发送一次心跳
|
||||||
|
```
|
||||||
|
|
||||||
|
## 特性说明
|
||||||
|
|
||||||
|
### 1. 多连接支持
|
||||||
|
|
||||||
|
系统支持同时建立多个 WebSocket 连接,每个连接都有唯一的 `connection_id`。连接按照流水线和会话类型进行分组管理。
|
||||||
|
|
||||||
|
### 2. 双向通信
|
||||||
|
|
||||||
|
- **前端 → 后端**: 客户端可以主动发送消息给服务器
|
||||||
|
- **后端 → 前端**: 服务器可以通过广播 API 主动推送消息给客户端
|
||||||
|
|
||||||
|
### 3. 流式响应
|
||||||
|
|
||||||
|
支持流式输出,机器人的响应会分块发送,客户端可以实时显示部分响应内容。
|
||||||
|
|
||||||
|
### 4. 会话隔离
|
||||||
|
|
||||||
|
支持 `person` 和 `group` 两种会话类型,不同类型的会话消息历史互不影响。
|
||||||
|
|
||||||
|
### 5. 连接管理
|
||||||
|
|
||||||
|
- 自动追踪连接状态
|
||||||
|
- 记录最后活跃时间
|
||||||
|
- 支持连接统计查询
|
||||||
|
- 连接断开时自动清理资源
|
||||||
|
|
||||||
|
### 6. 心跳机制
|
||||||
|
|
||||||
|
客户端可以定期发送 `ping` 消息,服务器会响应 `pong`,用于保持连接活跃和检测连接状态。
|
||||||
|
|
||||||
|
## 架构优势
|
||||||
|
|
||||||
|
1. **高并发**: 使用 asyncio 异步架构,支持大量并发连接
|
||||||
|
2. **可扩展**: 模块化设计,易于扩展新功能
|
||||||
|
3. **线程安全**: 连接管理器使用锁机制保证并发安全
|
||||||
|
4. **消息队列**: 每个连接独立的发送队列,避免消息混乱
|
||||||
|
5. **灵活路由**: 支持按流水线、会话类型灵活路由消息
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **认证**: 当前 WebSocket 连接不需要认证,生产环境建议添加认证机制
|
||||||
|
2. **心跳**: 建议客户端实现心跳机制,避免连接超时
|
||||||
|
3. **重连**: 客户端应实现断线重连逻辑
|
||||||
|
4. **消息大小**: 注意控制单条消息大小,避免内存溢出
|
||||||
|
5. **连接数限制**: 生产环境建议设置最大连接数限制
|
||||||
|
|
||||||
|
## 故障排查
|
||||||
|
|
||||||
|
### 连接失败
|
||||||
|
|
||||||
|
1. 检查流水线 UUID 是否正确
|
||||||
|
2. 检查服务器是否正常运行
|
||||||
|
3. 检查防火墙设置
|
||||||
|
|
||||||
|
### 消息发送失败
|
||||||
|
|
||||||
|
1. 检查消息格式是否正确
|
||||||
|
2. 检查连接是否仍然活跃
|
||||||
|
3. 查看服务器日志获取详细错误信息
|
||||||
|
|
||||||
|
### 性能问题
|
||||||
|
|
||||||
|
1. 检查并发连接数是否过多
|
||||||
|
2. 检查消息处理速度
|
||||||
|
3. 考虑使用连接池或负载均衡
|
||||||
|
|
||||||
|
## 开发调试
|
||||||
|
|
||||||
|
启用详细日志:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import logging
|
||||||
|
logging.getLogger('langbot.pkg.platform.sources.websocket_adapter').setLevel(logging.DEBUG)
|
||||||
|
logging.getLogger('langbot.pkg.platform.sources.websocket_manager').setLevel(logging.DEBUG)
|
||||||
|
logging.getLogger('langbot.pkg.api.http.controller.groups.pipelines.websocket_chat').setLevel(logging.DEBUG)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 后续改进建议
|
||||||
|
|
||||||
|
1. 添加用户认证和授权机制
|
||||||
|
2. 实现消息持久化
|
||||||
|
3. 添加消息加密
|
||||||
|
4. 实现更丰富的消息类型 (图片、文件等)
|
||||||
|
5. 添加消息已读/未读状态
|
||||||
|
6. 实现群组聊天功能
|
||||||
|
7. 添加在线状态显示
|
||||||
|
8. 实现消息撤回功能
|
||||||
1944
docs/service-api-openapi.json
Normal file
1944
docs/service-api-openapi.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,45 +0,0 @@
|
|||||||
from v1 import client # type: ignore
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
import os
|
|
||||||
import json
|
|
||||||
|
|
||||||
|
|
||||||
class TestDifyClient:
|
|
||||||
async def test_chat_messages(self):
|
|
||||||
cln = client.AsyncDifyServiceClient(api_key=os.getenv('DIFY_API_KEY'), base_url=os.getenv('DIFY_BASE_URL'))
|
|
||||||
|
|
||||||
async for chunk in cln.chat_messages(inputs={}, query='调用工具查看现在几点?', user='test'):
|
|
||||||
print(json.dumps(chunk, ensure_ascii=False, indent=4))
|
|
||||||
|
|
||||||
async def test_upload_file(self):
|
|
||||||
cln = client.AsyncDifyServiceClient(api_key=os.getenv('DIFY_API_KEY'), base_url=os.getenv('DIFY_BASE_URL'))
|
|
||||||
|
|
||||||
file_bytes = open('img.png', 'rb').read()
|
|
||||||
|
|
||||||
print(type(file_bytes))
|
|
||||||
|
|
||||||
file = ('img2.png', file_bytes, 'image/png')
|
|
||||||
|
|
||||||
resp = await cln.upload_file(file=file, user='test')
|
|
||||||
print(json.dumps(resp, ensure_ascii=False, indent=4))
|
|
||||||
|
|
||||||
async def test_workflow_run(self):
|
|
||||||
cln = client.AsyncDifyServiceClient(api_key=os.getenv('DIFY_API_KEY'), base_url=os.getenv('DIFY_BASE_URL'))
|
|
||||||
|
|
||||||
# resp = await cln.workflow_run(inputs={}, user="test")
|
|
||||||
# # print(json.dumps(resp, ensure_ascii=False, indent=4))
|
|
||||||
# print(resp)
|
|
||||||
chunks = []
|
|
||||||
|
|
||||||
ignored_events = ['text_chunk']
|
|
||||||
async for chunk in cln.workflow_run(inputs={}, user='test'):
|
|
||||||
if chunk['event'] in ignored_events:
|
|
||||||
continue
|
|
||||||
chunks.append(chunk)
|
|
||||||
print(json.dumps(chunks, ensure_ascii=False, indent=4))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
asyncio.run(TestDifyClient().test_chat_messages())
|
|
||||||
@@ -1,263 +0,0 @@
|
|||||||
import time
|
|
||||||
from quart import request
|
|
||||||
import httpx
|
|
||||||
from quart import Quart
|
|
||||||
from typing import Callable, Dict, Any
|
|
||||||
import langbot_plugin.api.entities.builtin.platform.events as platform_events
|
|
||||||
from .qqofficialevent import QQOfficialEvent
|
|
||||||
import json
|
|
||||||
import traceback
|
|
||||||
from cryptography.hazmat.primitives.asymmetric import ed25519
|
|
||||||
|
|
||||||
|
|
||||||
def handle_validation(body: dict, bot_secret: str):
|
|
||||||
# bot正确的secert是32位的,此处仅为了适配演示demo
|
|
||||||
while len(bot_secret) < 32:
|
|
||||||
bot_secret = bot_secret * 2
|
|
||||||
bot_secret = bot_secret[:32]
|
|
||||||
# 实际使用场景中以上三行内容可清除
|
|
||||||
|
|
||||||
seed_bytes = bot_secret.encode()
|
|
||||||
|
|
||||||
signing_key = ed25519.Ed25519PrivateKey.from_private_bytes(seed_bytes)
|
|
||||||
|
|
||||||
msg = body['d']['event_ts'] + body['d']['plain_token']
|
|
||||||
msg_bytes = msg.encode()
|
|
||||||
|
|
||||||
signature = signing_key.sign(msg_bytes)
|
|
||||||
|
|
||||||
signature_hex = signature.hex()
|
|
||||||
|
|
||||||
response = {'plain_token': body['d']['plain_token'], 'signature': signature_hex}
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
class QQOfficialClient:
|
|
||||||
def __init__(self, secret: str, token: str, app_id: str, logger: None):
|
|
||||||
self.app = Quart(__name__)
|
|
||||||
self.app.add_url_rule(
|
|
||||||
'/callback/command',
|
|
||||||
'handle_callback',
|
|
||||||
self.handle_callback_request,
|
|
||||||
methods=['GET', 'POST'],
|
|
||||||
)
|
|
||||||
self.secret = secret
|
|
||||||
self.token = token
|
|
||||||
self.app_id = app_id
|
|
||||||
self._message_handlers = {}
|
|
||||||
self.base_url = 'https://api.sgroup.qq.com'
|
|
||||||
self.access_token = ''
|
|
||||||
self.access_token_expiry_time = None
|
|
||||||
self.logger = logger
|
|
||||||
|
|
||||||
async def check_access_token(self):
|
|
||||||
"""检查access_token是否存在"""
|
|
||||||
if not self.access_token or await self.is_token_expired():
|
|
||||||
return False
|
|
||||||
return bool(self.access_token and self.access_token.strip())
|
|
||||||
|
|
||||||
async def get_access_token(self):
|
|
||||||
"""获取access_token"""
|
|
||||||
url = 'https://bots.qq.com/app/getAppAccessToken'
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
params = {
|
|
||||||
'appId': self.app_id,
|
|
||||||
'clientSecret': self.secret,
|
|
||||||
}
|
|
||||||
headers = {
|
|
||||||
'content-type': 'application/json',
|
|
||||||
}
|
|
||||||
try:
|
|
||||||
response = await client.post(url, json=params, headers=headers)
|
|
||||||
if response.status_code == 200:
|
|
||||||
response_data = response.json()
|
|
||||||
access_token = response_data.get('access_token')
|
|
||||||
expires_in = int(response_data.get('expires_in', 7200))
|
|
||||||
self.access_token_expiry_time = time.time() + expires_in - 60
|
|
||||||
if access_token:
|
|
||||||
self.access_token = access_token
|
|
||||||
except Exception as e:
|
|
||||||
await self.logger.error(f'获取access_token失败: {response_data}')
|
|
||||||
raise Exception(f'获取access_token失败: {e}')
|
|
||||||
|
|
||||||
async def handle_callback_request(self):
|
|
||||||
"""处理回调请求"""
|
|
||||||
try:
|
|
||||||
# 读取请求数据
|
|
||||||
body = await request.get_data()
|
|
||||||
payload = json.loads(body)
|
|
||||||
|
|
||||||
# 验证是否为回调验证请求
|
|
||||||
if payload.get('op') == 13:
|
|
||||||
# 生成签名
|
|
||||||
response = handle_validation(payload, self.secret)
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
if payload.get('op') == 0:
|
|
||||||
message_data = await self.get_message(payload)
|
|
||||||
if message_data:
|
|
||||||
event = QQOfficialEvent.from_payload(message_data)
|
|
||||||
await self._handle_message(event)
|
|
||||||
|
|
||||||
return {'code': 0, 'message': 'success'}
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
await self.logger.error(f'Error in handle_callback_request: {traceback.format_exc()}')
|
|
||||||
return {'error': str(e)}, 400
|
|
||||||
|
|
||||||
async def run_task(self, host: str, port: int, *args, **kwargs):
|
|
||||||
"""启动 Quart 应用"""
|
|
||||||
await self.app.run_task(host=host, port=port, *args, **kwargs)
|
|
||||||
|
|
||||||
def on_message(self, msg_type: str):
|
|
||||||
"""注册消息类型处理器"""
|
|
||||||
|
|
||||||
def decorator(func: Callable[[platform_events.Event], None]):
|
|
||||||
if msg_type not in self._message_handlers:
|
|
||||||
self._message_handlers[msg_type] = []
|
|
||||||
self._message_handlers[msg_type].append(func)
|
|
||||||
return func
|
|
||||||
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
async def _handle_message(self, event: QQOfficialEvent):
|
|
||||||
"""处理消息事件"""
|
|
||||||
msg_type = event.t
|
|
||||||
if msg_type in self._message_handlers:
|
|
||||||
for handler in self._message_handlers[msg_type]:
|
|
||||||
await handler(event)
|
|
||||||
|
|
||||||
async def get_message(self, msg: dict) -> Dict[str, Any]:
|
|
||||||
"""获取消息"""
|
|
||||||
message_data = {
|
|
||||||
't': msg.get('t', {}),
|
|
||||||
'user_openid': msg.get('d', {}).get('author', {}).get('user_openid', {}),
|
|
||||||
'timestamp': msg.get('d', {}).get('timestamp', {}),
|
|
||||||
'd_author_id': msg.get('d', {}).get('author', {}).get('id', {}),
|
|
||||||
'content': msg.get('d', {}).get('content', {}),
|
|
||||||
'd_id': msg.get('d', {}).get('id', {}),
|
|
||||||
'id': msg.get('id', {}),
|
|
||||||
'channel_id': msg.get('d', {}).get('channel_id', {}),
|
|
||||||
'username': msg.get('d', {}).get('author', {}).get('username', {}),
|
|
||||||
'guild_id': msg.get('d', {}).get('guild_id', {}),
|
|
||||||
'member_openid': msg.get('d', {}).get('author', {}).get('openid', {}),
|
|
||||||
'group_openid': msg.get('d', {}).get('group_openid', {}),
|
|
||||||
}
|
|
||||||
attachments = msg.get('d', {}).get('attachments', [])
|
|
||||||
image_attachments = [attachment['url'] for attachment in attachments if await self.is_image(attachment)]
|
|
||||||
image_attachments_type = [
|
|
||||||
attachment['content_type'] for attachment in attachments if await self.is_image(attachment)
|
|
||||||
]
|
|
||||||
if image_attachments:
|
|
||||||
message_data['image_attachments'] = image_attachments[0]
|
|
||||||
message_data['content_type'] = image_attachments_type[0]
|
|
||||||
else:
|
|
||||||
message_data['image_attachments'] = None
|
|
||||||
|
|
||||||
return message_data
|
|
||||||
|
|
||||||
async def is_image(self, attachment: dict) -> bool:
|
|
||||||
"""判断是否为图片附件"""
|
|
||||||
content_type = attachment.get('content_type', '')
|
|
||||||
return content_type.startswith('image/')
|
|
||||||
|
|
||||||
async def send_private_text_msg(self, user_openid: str, content: str, msg_id: str):
|
|
||||||
"""发送私聊消息"""
|
|
||||||
if not await self.check_access_token():
|
|
||||||
await self.get_access_token()
|
|
||||||
|
|
||||||
url = self.base_url + '/v2/users/' + user_openid + '/messages'
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
headers = {
|
|
||||||
'Authorization': f'QQBot {self.access_token}',
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
}
|
|
||||||
data = {
|
|
||||||
'content': content,
|
|
||||||
'msg_type': 0,
|
|
||||||
'msg_id': msg_id,
|
|
||||||
}
|
|
||||||
response = await client.post(url, headers=headers, json=data)
|
|
||||||
response_data = response.json()
|
|
||||||
if response.status_code == 200:
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
await self.logger.error(f'发送私聊消息失败: {response_data}')
|
|
||||||
raise ValueError(response)
|
|
||||||
|
|
||||||
async def send_group_text_msg(self, group_openid: str, content: str, msg_id: str):
|
|
||||||
"""发送群聊消息"""
|
|
||||||
if not await self.check_access_token():
|
|
||||||
await self.get_access_token()
|
|
||||||
|
|
||||||
url = self.base_url + '/v2/groups/' + group_openid + '/messages'
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
headers = {
|
|
||||||
'Authorization': f'QQBot {self.access_token}',
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
}
|
|
||||||
data = {
|
|
||||||
'content': content,
|
|
||||||
'msg_type': 0,
|
|
||||||
'msg_id': msg_id,
|
|
||||||
}
|
|
||||||
response = await client.post(url, headers=headers, json=data)
|
|
||||||
if response.status_code == 200:
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
await self.logger.error(f'发送群聊消息失败:{response.json()}')
|
|
||||||
raise Exception(response.read().decode())
|
|
||||||
|
|
||||||
async def send_channle_group_text_msg(self, channel_id: str, content: str, msg_id: str):
|
|
||||||
"""发送频道群聊消息"""
|
|
||||||
if not await self.check_access_token():
|
|
||||||
await self.get_access_token()
|
|
||||||
|
|
||||||
url = self.base_url + '/channels/' + channel_id + '/messages'
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
headers = {
|
|
||||||
'Authorization': f'QQBot {self.access_token}',
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
}
|
|
||||||
params = {
|
|
||||||
'content': content,
|
|
||||||
'msg_type': 0,
|
|
||||||
'msg_id': msg_id,
|
|
||||||
}
|
|
||||||
response = await client.post(url, headers=headers, json=params)
|
|
||||||
if response.status_code == 200:
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
await self.logger.error(f'发送频道群聊消息失败: {response.json()}')
|
|
||||||
raise Exception(response)
|
|
||||||
|
|
||||||
async def send_channle_private_text_msg(self, guild_id: str, content: str, msg_id: str):
|
|
||||||
"""发送频道私聊消息"""
|
|
||||||
if not await self.check_access_token():
|
|
||||||
await self.get_access_token()
|
|
||||||
|
|
||||||
url = self.base_url + '/dms/' + guild_id + '/messages'
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
headers = {
|
|
||||||
'Authorization': f'QQBot {self.access_token}',
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
}
|
|
||||||
params = {
|
|
||||||
'content': content,
|
|
||||||
'msg_type': 0,
|
|
||||||
'msg_id': msg_id,
|
|
||||||
}
|
|
||||||
response = await client.post(url, headers=headers, json=params)
|
|
||||||
if response.status_code == 200:
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
await self.logger.error(f'发送频道私聊消息失败: {response.json()}')
|
|
||||||
raise Exception(response)
|
|
||||||
|
|
||||||
async def is_token_expired(self):
|
|
||||||
"""检查token是否过期"""
|
|
||||||
if self.access_token_expiry_time is None:
|
|
||||||
return True
|
|
||||||
return time.time() > self.access_token_expiry_time
|
|
||||||
@@ -1,290 +0,0 @@
|
|||||||
import json
|
|
||||||
import time
|
|
||||||
import uuid
|
|
||||||
import xml.etree.ElementTree as ET
|
|
||||||
from urllib.parse import unquote
|
|
||||||
import hashlib
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
import httpx
|
|
||||||
from libs.wecom_ai_bot_api.WXBizMsgCrypt3 import WXBizMsgCrypt
|
|
||||||
from quart import Quart, request, Response, jsonify
|
|
||||||
import langbot_plugin.api.entities.builtin.platform.message as platform_message
|
|
||||||
import asyncio
|
|
||||||
from libs.wecom_ai_bot_api import wecombotevent
|
|
||||||
from typing import Callable
|
|
||||||
import base64
|
|
||||||
from Crypto.Cipher import AES
|
|
||||||
from pkg.platform.logger import EventLogger
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class WecomBotClient:
|
|
||||||
def __init__(self,Token:str,EnCodingAESKey:str,Corpid:str,logger:EventLogger):
|
|
||||||
self.Token=Token
|
|
||||||
self.EnCodingAESKey=EnCodingAESKey
|
|
||||||
self.Corpid=Corpid
|
|
||||||
self.ReceiveId = ''
|
|
||||||
self.app = Quart(__name__)
|
|
||||||
self.app.add_url_rule(
|
|
||||||
'/callback/command',
|
|
||||||
'handle_callback',
|
|
||||||
self.handle_callback_request,
|
|
||||||
methods=['POST','GET']
|
|
||||||
)
|
|
||||||
self._message_handlers = {
|
|
||||||
'example': [],
|
|
||||||
}
|
|
||||||
self.user_stream_map = {}
|
|
||||||
self.logger = logger
|
|
||||||
self.generated_content = {}
|
|
||||||
self.msg_id_map = {}
|
|
||||||
|
|
||||||
async def sha1_signature(token: str, timestamp: str, nonce: str, encrypt: str) -> str:
|
|
||||||
raw = "".join(sorted([token, timestamp, nonce, encrypt]))
|
|
||||||
return hashlib.sha1(raw.encode("utf-8")).hexdigest()
|
|
||||||
|
|
||||||
async def handle_callback_request(self):
|
|
||||||
try:
|
|
||||||
self.wxcpt=WXBizMsgCrypt(self.Token,self.EnCodingAESKey,'')
|
|
||||||
|
|
||||||
if request.method == "GET":
|
|
||||||
|
|
||||||
msg_signature = unquote(request.args.get("msg_signature", ""))
|
|
||||||
timestamp = unquote(request.args.get("timestamp", ""))
|
|
||||||
nonce = unquote(request.args.get("nonce", ""))
|
|
||||||
echostr = unquote(request.args.get("echostr", ""))
|
|
||||||
|
|
||||||
if not all([msg_signature, timestamp, nonce, echostr]):
|
|
||||||
await self.logger.error("请求参数缺失")
|
|
||||||
return Response("缺少参数", status=400)
|
|
||||||
|
|
||||||
ret, decrypted_str = self.wxcpt.VerifyURL(msg_signature, timestamp, nonce, echostr)
|
|
||||||
if ret != 0:
|
|
||||||
|
|
||||||
await self.logger.error("验证URL失败")
|
|
||||||
return Response("验证失败", status=403)
|
|
||||||
|
|
||||||
return Response(decrypted_str, mimetype="text/plain")
|
|
||||||
|
|
||||||
elif request.method == "POST":
|
|
||||||
msg_signature = unquote(request.args.get("msg_signature", ""))
|
|
||||||
timestamp = unquote(request.args.get("timestamp", ""))
|
|
||||||
nonce = unquote(request.args.get("nonce", ""))
|
|
||||||
|
|
||||||
try:
|
|
||||||
timeout = 3
|
|
||||||
interval = 0.1
|
|
||||||
start_time = time.monotonic()
|
|
||||||
encrypted_json = await request.get_json()
|
|
||||||
encrypted_msg = encrypted_json.get("encrypt", "")
|
|
||||||
if not encrypted_msg:
|
|
||||||
await self.logger.error("请求体中缺少 'encrypt' 字段")
|
|
||||||
|
|
||||||
xml_post_data = f"<xml><Encrypt><![CDATA[{encrypted_msg}]]></Encrypt></xml>"
|
|
||||||
ret, decrypted_xml = self.wxcpt.DecryptMsg(xml_post_data, msg_signature, timestamp, nonce)
|
|
||||||
if ret != 0:
|
|
||||||
await self.logger.error("解密失败")
|
|
||||||
|
|
||||||
|
|
||||||
msg_json = json.loads(decrypted_xml)
|
|
||||||
|
|
||||||
from_user_id = msg_json.get("from", {}).get("userid")
|
|
||||||
chatid = msg_json.get("chatid", "")
|
|
||||||
|
|
||||||
message_data = await self.get_message(msg_json)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if message_data:
|
|
||||||
try:
|
|
||||||
event = wecombotevent.WecomBotEvent(message_data)
|
|
||||||
if event:
|
|
||||||
await self._handle_message(event)
|
|
||||||
except Exception as e:
|
|
||||||
await self.logger.error(traceback.format_exc())
|
|
||||||
print(traceback.format_exc())
|
|
||||||
|
|
||||||
start_time = time.time()
|
|
||||||
try:
|
|
||||||
if msg_json.get('chattype','') == 'single':
|
|
||||||
if from_user_id in self.user_stream_map:
|
|
||||||
stream_id = self.user_stream_map[from_user_id]
|
|
||||||
else:
|
|
||||||
stream_id =str(uuid.uuid4())
|
|
||||||
self.user_stream_map[from_user_id] = stream_id
|
|
||||||
|
|
||||||
|
|
||||||
else:
|
|
||||||
|
|
||||||
if chatid in self.user_stream_map:
|
|
||||||
stream_id = self.user_stream_map[chatid]
|
|
||||||
else:
|
|
||||||
stream_id = str(uuid.uuid4())
|
|
||||||
self.user_stream_map[chatid] = stream_id
|
|
||||||
except Exception as e:
|
|
||||||
await self.logger.error(traceback.format_exc())
|
|
||||||
print(traceback.format_exc())
|
|
||||||
while True:
|
|
||||||
content = self.generated_content.pop(msg_json['msgid'],None)
|
|
||||||
if content:
|
|
||||||
reply_plain = {
|
|
||||||
"msgtype": "stream",
|
|
||||||
"stream": {
|
|
||||||
"id": stream_id,
|
|
||||||
"finish": True,
|
|
||||||
"content": content
|
|
||||||
}
|
|
||||||
}
|
|
||||||
reply_plain_str = json.dumps(reply_plain, ensure_ascii=False)
|
|
||||||
|
|
||||||
reply_timestamp = str(int(time.time()))
|
|
||||||
ret, encrypt_text = self.wxcpt.EncryptMsg(reply_plain_str, nonce, reply_timestamp)
|
|
||||||
if ret != 0:
|
|
||||||
|
|
||||||
await self.logger.error("加密失败"+str(ret))
|
|
||||||
|
|
||||||
|
|
||||||
root = ET.fromstring(encrypt_text)
|
|
||||||
encrypt = root.find("Encrypt").text
|
|
||||||
resp = {
|
|
||||||
"encrypt": encrypt,
|
|
||||||
}
|
|
||||||
return jsonify(resp), 200
|
|
||||||
|
|
||||||
if time.time() - start_time > timeout:
|
|
||||||
break
|
|
||||||
|
|
||||||
await asyncio.sleep(interval)
|
|
||||||
|
|
||||||
if self.msg_id_map.get(message_data['msgid'], 1) == 3:
|
|
||||||
await self.logger.error('请求失效:暂不支持智能机器人超过7秒的请求,如有需求,请联系 LangBot 团队。')
|
|
||||||
return ''
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
await self.logger.error(traceback.format_exc())
|
|
||||||
print(traceback.format_exc())
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
await self.logger.error(traceback.format_exc())
|
|
||||||
print(traceback.format_exc())
|
|
||||||
|
|
||||||
|
|
||||||
async def get_message(self,msg_json):
|
|
||||||
message_data = {}
|
|
||||||
|
|
||||||
if msg_json.get('chattype','') == 'single':
|
|
||||||
message_data['type'] = 'single'
|
|
||||||
elif msg_json.get('chattype','') == 'group':
|
|
||||||
message_data['type'] = 'group'
|
|
||||||
|
|
||||||
if msg_json.get('msgtype') == 'text':
|
|
||||||
message_data['content'] = msg_json.get('text',{}).get('content')
|
|
||||||
elif msg_json.get('msgtype') == 'image':
|
|
||||||
picurl = msg_json.get('image', {}).get('url','')
|
|
||||||
base64 = await self.download_url_to_base64(picurl,self.EnCodingAESKey)
|
|
||||||
message_data['picurl'] = base64
|
|
||||||
elif msg_json.get('msgtype') == 'mixed':
|
|
||||||
items = msg_json.get('mixed', {}).get('msg_item', [])
|
|
||||||
texts = []
|
|
||||||
picurl = None
|
|
||||||
for item in items:
|
|
||||||
if item.get('msgtype') == 'text':
|
|
||||||
texts.append(item.get('text', {}).get('content', ''))
|
|
||||||
elif item.get('msgtype') == 'image' and picurl is None:
|
|
||||||
picurl = item.get('image', {}).get('url')
|
|
||||||
|
|
||||||
if texts:
|
|
||||||
message_data['content'] = "".join(texts) # 拼接所有 text
|
|
||||||
if picurl:
|
|
||||||
base64 = await self.download_url_to_base64(picurl,self.EnCodingAESKey)
|
|
||||||
message_data['picurl'] = base64 # 只保留第一个 image
|
|
||||||
|
|
||||||
message_data['userid'] = msg_json.get('from', {}).get('userid', '')
|
|
||||||
message_data['msgid'] = msg_json.get('msgid', '')
|
|
||||||
|
|
||||||
if msg_json.get('aibotid'):
|
|
||||||
message_data['aibotid'] = msg_json.get('aibotid', '')
|
|
||||||
|
|
||||||
return message_data
|
|
||||||
|
|
||||||
async def _handle_message(self, event: wecombotevent.WecomBotEvent):
|
|
||||||
"""
|
|
||||||
处理消息事件。
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
message_id = event.message_id
|
|
||||||
if message_id in self.msg_id_map.keys():
|
|
||||||
self.msg_id_map[message_id] += 1
|
|
||||||
return
|
|
||||||
self.msg_id_map[message_id] = 1
|
|
||||||
msg_type = event.type
|
|
||||||
if msg_type in self._message_handlers:
|
|
||||||
for handler in self._message_handlers[msg_type]:
|
|
||||||
await handler(event)
|
|
||||||
except Exception:
|
|
||||||
print(traceback.format_exc())
|
|
||||||
|
|
||||||
async def set_message(self, msg_id: str, content: str):
|
|
||||||
self.generated_content[msg_id] = content
|
|
||||||
|
|
||||||
def on_message(self, msg_type: str):
|
|
||||||
def decorator(func: Callable[[wecombotevent.WecomBotEvent], None]):
|
|
||||||
if msg_type not in self._message_handlers:
|
|
||||||
self._message_handlers[msg_type] = []
|
|
||||||
self._message_handlers[msg_type].append(func)
|
|
||||||
return func
|
|
||||||
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
|
|
||||||
async def download_url_to_base64(self, download_url, encoding_aes_key):
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
response = await client.get(download_url)
|
|
||||||
if response.status_code != 200:
|
|
||||||
await self.logger.error(f'failed to get file: {response.text}')
|
|
||||||
return None
|
|
||||||
|
|
||||||
encrypted_bytes = response.content
|
|
||||||
|
|
||||||
|
|
||||||
aes_key = base64.b64decode(encoding_aes_key + "=") # base64 补齐
|
|
||||||
iv = aes_key[:16]
|
|
||||||
|
|
||||||
|
|
||||||
cipher = AES.new(aes_key, AES.MODE_CBC, iv)
|
|
||||||
decrypted = cipher.decrypt(encrypted_bytes)
|
|
||||||
|
|
||||||
|
|
||||||
pad_len = decrypted[-1]
|
|
||||||
decrypted = decrypted[:-pad_len]
|
|
||||||
|
|
||||||
|
|
||||||
if decrypted.startswith(b"\xff\xd8"): # JPEG
|
|
||||||
mime_type = "image/jpeg"
|
|
||||||
elif decrypted.startswith(b"\x89PNG"): # PNG
|
|
||||||
mime_type = "image/png"
|
|
||||||
elif decrypted.startswith((b"GIF87a", b"GIF89a")): # GIF
|
|
||||||
mime_type = "image/gif"
|
|
||||||
elif decrypted.startswith(b"BM"): # BMP
|
|
||||||
mime_type = "image/bmp"
|
|
||||||
elif decrypted.startswith(b"II*\x00") or decrypted.startswith(b"MM\x00*"): # TIFF
|
|
||||||
mime_type = "image/tiff"
|
|
||||||
else:
|
|
||||||
mime_type = "application/octet-stream"
|
|
||||||
|
|
||||||
# 转 base64
|
|
||||||
base64_str = base64.b64encode(decrypted).decode("utf-8")
|
|
||||||
return f"data:{mime_type};base64,{base64_str}"
|
|
||||||
|
|
||||||
|
|
||||||
async def run_task(self, host: str, port: int, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
启动 Quart 应用。
|
|
||||||
"""
|
|
||||||
await self.app.run_task(host=host, port=port, *args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
from typing import Dict, Any, Optional
|
|
||||||
|
|
||||||
|
|
||||||
class WecomBotEvent(dict):
|
|
||||||
@staticmethod
|
|
||||||
def from_payload(payload: Dict[str, Any]) -> Optional['WecomBotEvent']:
|
|
||||||
try:
|
|
||||||
event = WecomBotEvent(payload)
|
|
||||||
return event
|
|
||||||
except KeyError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def type(self) -> str:
|
|
||||||
"""
|
|
||||||
事件类型
|
|
||||||
"""
|
|
||||||
return self.get('type', '')
|
|
||||||
|
|
||||||
@property
|
|
||||||
def userid(self) -> str:
|
|
||||||
"""
|
|
||||||
用户id
|
|
||||||
"""
|
|
||||||
return self.get('from', {}).get('userid', '')
|
|
||||||
|
|
||||||
@property
|
|
||||||
def content(self) -> str:
|
|
||||||
"""
|
|
||||||
内容
|
|
||||||
"""
|
|
||||||
return self.get('content', '')
|
|
||||||
|
|
||||||
@property
|
|
||||||
def picurl(self) -> str:
|
|
||||||
"""
|
|
||||||
图片url
|
|
||||||
"""
|
|
||||||
return self.get('picurl', '')
|
|
||||||
|
|
||||||
@property
|
|
||||||
def chatid(self) -> str:
|
|
||||||
"""
|
|
||||||
群组id
|
|
||||||
"""
|
|
||||||
return self.get('chatid', {})
|
|
||||||
|
|
||||||
@property
|
|
||||||
def message_id(self) -> str:
|
|
||||||
"""
|
|
||||||
消息id
|
|
||||||
"""
|
|
||||||
return self.get('msgid', '')
|
|
||||||
|
|
||||||
@property
|
|
||||||
def ai_bot_id(self) -> str:
|
|
||||||
"""
|
|
||||||
AI Bot ID
|
|
||||||
"""
|
|
||||||
return self.get('aibotid', '')
|
|
||||||
118
main.py
118
main.py
@@ -1,117 +1,3 @@
|
|||||||
import asyncio
|
import langbot.__main__
|
||||||
import argparse
|
|
||||||
# LangBot 终端启动入口
|
|
||||||
# 在此层级解决依赖项检查。
|
|
||||||
# LangBot/main.py
|
|
||||||
|
|
||||||
asciiart = r"""
|
langbot.__main__.main()
|
||||||
_ ___ _
|
|
||||||
| | __ _ _ _ __ _| _ ) ___| |_
|
|
||||||
| |__/ _` | ' \/ _` | _ \/ _ \ _|
|
|
||||||
|____\__,_|_||_\__, |___/\___/\__|
|
|
||||||
|___/
|
|
||||||
|
|
||||||
⭐️ Open Source 开源地址: https://github.com/langbot-app/LangBot
|
|
||||||
📖 Documentation 文档地址: https://docs.langbot.app
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
async def main_entry(loop: asyncio.AbstractEventLoop):
|
|
||||||
parser = argparse.ArgumentParser(description='LangBot')
|
|
||||||
parser.add_argument(
|
|
||||||
'--standalone-runtime',
|
|
||||||
action='store_true',
|
|
||||||
help='Use standalone plugin runtime / 使用独立插件运行时',
|
|
||||||
default=False,
|
|
||||||
)
|
|
||||||
parser.add_argument('--debug', action='store_true', help='Debug mode / 调试模式', default=False)
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
if args.standalone_runtime:
|
|
||||||
from pkg.utils import platform
|
|
||||||
|
|
||||||
platform.standalone_runtime = True
|
|
||||||
|
|
||||||
if args.debug:
|
|
||||||
from pkg.utils import constants
|
|
||||||
|
|
||||||
constants.debug_mode = True
|
|
||||||
|
|
||||||
print(asciiart)
|
|
||||||
|
|
||||||
import sys
|
|
||||||
|
|
||||||
# 检查依赖
|
|
||||||
|
|
||||||
from pkg.core.bootutils import deps
|
|
||||||
|
|
||||||
missing_deps = await deps.check_deps()
|
|
||||||
|
|
||||||
if missing_deps:
|
|
||||||
print('以下依赖包未安装,将自动安装,请完成后重启程序:')
|
|
||||||
print(
|
|
||||||
'These dependencies are missing, they will be installed automatically, please restart the program after completion:'
|
|
||||||
)
|
|
||||||
for dep in missing_deps:
|
|
||||||
print('-', dep)
|
|
||||||
await deps.install_deps(missing_deps)
|
|
||||||
print('已自动安装缺失的依赖包,请重启程序。')
|
|
||||||
print('The missing dependencies have been installed automatically, please restart the program.')
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
# # 检查pydantic版本,如果没有 pydantic.v1,则把 pydantic 映射为 v1
|
|
||||||
# import pydantic.version
|
|
||||||
|
|
||||||
# if pydantic.version.VERSION < '2.0':
|
|
||||||
# import pydantic
|
|
||||||
|
|
||||||
# sys.modules['pydantic.v1'] = pydantic
|
|
||||||
|
|
||||||
# 检查配置文件
|
|
||||||
|
|
||||||
from pkg.core.bootutils import files
|
|
||||||
|
|
||||||
generated_files = await files.generate_files()
|
|
||||||
|
|
||||||
if generated_files:
|
|
||||||
print('以下文件不存在,已自动生成:')
|
|
||||||
print('Following files do not exist and have been automatically generated:')
|
|
||||||
for file in generated_files:
|
|
||||||
print('-', file)
|
|
||||||
|
|
||||||
from pkg.core import boot
|
|
||||||
|
|
||||||
await boot.main(loop)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
# 必须大于 3.10.1
|
|
||||||
if sys.version_info < (3, 10, 1):
|
|
||||||
print('需要 Python 3.10.1 及以上版本,当前 Python 版本为:', sys.version)
|
|
||||||
input('按任意键退出...')
|
|
||||||
print('Your Python version is not supported. Please exit the program by pressing any key.')
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
# Check if the current directory is the LangBot project root directory
|
|
||||||
invalid_pwd = False
|
|
||||||
|
|
||||||
if not os.path.exists('main.py'):
|
|
||||||
invalid_pwd = True
|
|
||||||
else:
|
|
||||||
with open('main.py', 'r', encoding='utf-8') as f:
|
|
||||||
content = f.read()
|
|
||||||
if 'LangBot/main.py' not in content:
|
|
||||||
invalid_pwd = True
|
|
||||||
if invalid_pwd:
|
|
||||||
print('请在 LangBot 项目根目录下以命令形式运行此程序。')
|
|
||||||
input('按任意键退出...')
|
|
||||||
print('Please run this program in the LangBot project root directory in command form.')
|
|
||||||
print('Press any key to exit...')
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
loop = asyncio.new_event_loop()
|
|
||||||
|
|
||||||
loop.run_until_complete(main_entry(loop))
|
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import quart
|
|
||||||
import mimetypes
|
|
||||||
import uuid
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
import quart.datastructures
|
|
||||||
|
|
||||||
from .. import group
|
|
||||||
|
|
||||||
|
|
||||||
@group.group_class('files', '/api/v1/files')
|
|
||||||
class FilesRouterGroup(group.RouterGroup):
|
|
||||||
async def initialize(self) -> None:
|
|
||||||
@self.route('/image/<image_key>', methods=['GET'], auth_type=group.AuthType.NONE)
|
|
||||||
async def _(image_key: str) -> quart.Response:
|
|
||||||
if '/' in image_key or '\\' in image_key:
|
|
||||||
return quart.Response(status=404)
|
|
||||||
|
|
||||||
if not await self.ap.storage_mgr.storage_provider.exists(image_key):
|
|
||||||
return quart.Response(status=404)
|
|
||||||
|
|
||||||
image_bytes = await self.ap.storage_mgr.storage_provider.load(image_key)
|
|
||||||
mime_type = mimetypes.guess_type(image_key)[0]
|
|
||||||
if mime_type is None:
|
|
||||||
mime_type = 'image/jpeg'
|
|
||||||
|
|
||||||
return quart.Response(image_bytes, mimetype=mime_type)
|
|
||||||
|
|
||||||
@self.route('/documents', methods=['POST'], auth_type=group.AuthType.USER_TOKEN)
|
|
||||||
async def _() -> quart.Response:
|
|
||||||
request = quart.request
|
|
||||||
# get file bytes from 'file'
|
|
||||||
file = (await request.files)['file']
|
|
||||||
assert isinstance(file, quart.datastructures.FileStorage)
|
|
||||||
|
|
||||||
file_bytes = await asyncio.to_thread(file.stream.read)
|
|
||||||
extension = file.filename.split('.')[-1]
|
|
||||||
file_name = file.filename.split('.')[0]
|
|
||||||
|
|
||||||
# check if file name contains '/' or '\'
|
|
||||||
if '/' in file_name or '\\' in file_name:
|
|
||||||
return self.fail(400, 'File name contains invalid characters')
|
|
||||||
|
|
||||||
file_key = file_name + '_' + str(uuid.uuid4())[:8] + '.' + extension
|
|
||||||
# save file to storage
|
|
||||||
await self.ap.storage_mgr.storage_provider.save(file_key, file_bytes)
|
|
||||||
return self.success(
|
|
||||||
data={
|
|
||||||
'file_id': file_key,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import quart
|
|
||||||
|
|
||||||
from ... import group
|
|
||||||
|
|
||||||
|
|
||||||
@group.group_class('pipelines', '/api/v1/pipelines')
|
|
||||||
class PipelinesRouterGroup(group.RouterGroup):
|
|
||||||
async def initialize(self) -> None:
|
|
||||||
@self.route('', methods=['GET', 'POST'])
|
|
||||||
async def _() -> str:
|
|
||||||
if quart.request.method == 'GET':
|
|
||||||
sort_by = quart.request.args.get('sort_by', 'created_at')
|
|
||||||
sort_order = quart.request.args.get('sort_order', 'DESC')
|
|
||||||
return self.success(
|
|
||||||
data={'pipelines': await self.ap.pipeline_service.get_pipelines(sort_by, sort_order)}
|
|
||||||
)
|
|
||||||
elif quart.request.method == 'POST':
|
|
||||||
json_data = await quart.request.json
|
|
||||||
|
|
||||||
pipeline_uuid = await self.ap.pipeline_service.create_pipeline(json_data)
|
|
||||||
|
|
||||||
return self.success(data={'uuid': pipeline_uuid})
|
|
||||||
|
|
||||||
@self.route('/_/metadata', methods=['GET'])
|
|
||||||
async def _() -> str:
|
|
||||||
return self.success(data={'configs': await self.ap.pipeline_service.get_pipeline_metadata()})
|
|
||||||
|
|
||||||
@self.route('/<pipeline_uuid>', methods=['GET', 'PUT', 'DELETE'])
|
|
||||||
async def _(pipeline_uuid: str) -> str:
|
|
||||||
if quart.request.method == 'GET':
|
|
||||||
pipeline = await self.ap.pipeline_service.get_pipeline(pipeline_uuid)
|
|
||||||
|
|
||||||
if pipeline is None:
|
|
||||||
return self.http_status(404, -1, 'pipeline not found')
|
|
||||||
|
|
||||||
return self.success(data={'pipeline': pipeline})
|
|
||||||
elif quart.request.method == 'PUT':
|
|
||||||
json_data = await quart.request.json
|
|
||||||
|
|
||||||
await self.ap.pipeline_service.update_pipeline(pipeline_uuid, json_data)
|
|
||||||
|
|
||||||
return self.success()
|
|
||||||
elif quart.request.method == 'DELETE':
|
|
||||||
await self.ap.pipeline_service.delete_pipeline(pipeline_uuid)
|
|
||||||
|
|
||||||
return self.success()
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
import json
|
|
||||||
|
|
||||||
import quart
|
|
||||||
|
|
||||||
from ... import group
|
|
||||||
|
|
||||||
|
|
||||||
@group.group_class('webchat', '/api/v1/pipelines/<pipeline_uuid>/chat')
|
|
||||||
class WebChatDebugRouterGroup(group.RouterGroup):
|
|
||||||
async def initialize(self) -> None:
|
|
||||||
@self.route('/send', methods=['POST'])
|
|
||||||
async def send_message(pipeline_uuid: str) -> str:
|
|
||||||
"""Send a message to the pipeline for debugging"""
|
|
||||||
|
|
||||||
async def stream_generator(generator):
|
|
||||||
yield 'data: {"type": "start"}\n\n'
|
|
||||||
async for message in generator:
|
|
||||||
yield f'data: {json.dumps({"message": message})}\n\n'
|
|
||||||
yield 'data: {"type": "end"}\n\n'
|
|
||||||
|
|
||||||
try:
|
|
||||||
data = await quart.request.get_json()
|
|
||||||
session_type = data.get('session_type', 'person')
|
|
||||||
message_chain_obj = data.get('message', [])
|
|
||||||
is_stream = data.get('is_stream', False)
|
|
||||||
|
|
||||||
if not message_chain_obj:
|
|
||||||
return self.http_status(400, -1, 'message is required')
|
|
||||||
|
|
||||||
if session_type not in ['person', 'group']:
|
|
||||||
return self.http_status(400, -1, 'session_type must be person or group')
|
|
||||||
|
|
||||||
webchat_adapter = self.ap.platform_mgr.webchat_proxy_bot.adapter
|
|
||||||
|
|
||||||
if not webchat_adapter:
|
|
||||||
return self.http_status(404, -1, 'WebChat adapter not found')
|
|
||||||
|
|
||||||
if is_stream:
|
|
||||||
generator = webchat_adapter.send_webchat_message(
|
|
||||||
pipeline_uuid, session_type, message_chain_obj, is_stream
|
|
||||||
)
|
|
||||||
# 设置正确的响应头
|
|
||||||
headers = {
|
|
||||||
'Content-Type': 'text/event-stream',
|
|
||||||
'Transfer-Encoding': 'chunked',
|
|
||||||
'Cache-Control': 'no-cache',
|
|
||||||
'Connection': 'keep-alive',
|
|
||||||
}
|
|
||||||
return quart.Response(stream_generator(generator), mimetype='text/event-stream', headers=headers)
|
|
||||||
|
|
||||||
else: # non-stream
|
|
||||||
result = None
|
|
||||||
async for message in webchat_adapter.send_webchat_message(
|
|
||||||
pipeline_uuid, session_type, message_chain_obj
|
|
||||||
):
|
|
||||||
result = message
|
|
||||||
if result is not None:
|
|
||||||
return self.success(
|
|
||||||
data={
|
|
||||||
'message': result,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return self.http_status(400, -1, 'message is required')
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
return self.http_status(500, -1, f'Internal server error: {str(e)}')
|
|
||||||
|
|
||||||
@self.route('/messages/<session_type>', methods=['GET'])
|
|
||||||
async def get_messages(pipeline_uuid: str, session_type: str) -> str:
|
|
||||||
"""Get the message history of the pipeline for debugging"""
|
|
||||||
try:
|
|
||||||
if session_type not in ['person', 'group']:
|
|
||||||
return self.http_status(400, -1, 'session_type must be person or group')
|
|
||||||
|
|
||||||
webchat_adapter = self.ap.platform_mgr.webchat_proxy_bot.adapter
|
|
||||||
|
|
||||||
if not webchat_adapter:
|
|
||||||
return self.http_status(404, -1, 'WebChat adapter not found')
|
|
||||||
|
|
||||||
messages = webchat_adapter.get_webchat_messages(pipeline_uuid, session_type)
|
|
||||||
|
|
||||||
return self.success(data={'messages': messages})
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
return self.http_status(500, -1, f'Internal server error: {str(e)}')
|
|
||||||
|
|
||||||
@self.route('/reset/<session_type>', methods=['POST'])
|
|
||||||
async def reset_session(session_type: str) -> str:
|
|
||||||
"""Reset the debug session"""
|
|
||||||
try:
|
|
||||||
if session_type not in ['person', 'group']:
|
|
||||||
return self.http_status(400, -1, 'session_type must be person or group')
|
|
||||||
|
|
||||||
webchat_adapter = None
|
|
||||||
for bot in self.ap.platform_mgr.bots:
|
|
||||||
if hasattr(bot.adapter, '__class__') and bot.adapter.__class__.__name__ == 'WebChatAdapter':
|
|
||||||
webchat_adapter = bot.adapter
|
|
||||||
break
|
|
||||||
|
|
||||||
if not webchat_adapter:
|
|
||||||
return self.http_status(404, -1, 'WebChat adapter not found')
|
|
||||||
|
|
||||||
webchat_adapter.reset_debug_session(session_type)
|
|
||||||
|
|
||||||
return self.success(data={'message': 'Session reset successfully'})
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
return self.http_status(500, -1, f'Internal server error: {str(e)}')
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import quart
|
|
||||||
|
|
||||||
from ... import group
|
|
||||||
|
|
||||||
|
|
||||||
@group.group_class('adapters', '/api/v1/platform/adapters')
|
|
||||||
class AdaptersRouterGroup(group.RouterGroup):
|
|
||||||
async def initialize(self) -> None:
|
|
||||||
@self.route('', methods=['GET'])
|
|
||||||
async def _() -> str:
|
|
||||||
return self.success(data={'adapters': self.ap.platform_mgr.get_available_adapters_info()})
|
|
||||||
|
|
||||||
@self.route('/<adapter_name>', methods=['GET'])
|
|
||||||
async def _(adapter_name: str) -> str:
|
|
||||||
adapter_info = self.ap.platform_mgr.get_available_adapter_info_by_name(adapter_name)
|
|
||||||
|
|
||||||
if adapter_info is None:
|
|
||||||
return self.http_status(404, -1, 'adapter not found')
|
|
||||||
|
|
||||||
return self.success(data={'adapter': adapter_info})
|
|
||||||
|
|
||||||
@self.route('/<adapter_name>/icon', methods=['GET'], auth_type=group.AuthType.NONE)
|
|
||||||
async def _(adapter_name: str) -> quart.Response:
|
|
||||||
adapter_manifest = self.ap.platform_mgr.get_available_adapter_manifest_by_name(adapter_name)
|
|
||||||
|
|
||||||
if adapter_manifest is None:
|
|
||||||
return self.http_status(404, -1, 'adapter not found')
|
|
||||||
|
|
||||||
icon_path = adapter_manifest.icon_rel_path
|
|
||||||
|
|
||||||
if icon_path is None:
|
|
||||||
return self.http_status(404, -1, 'icon not found')
|
|
||||||
|
|
||||||
return await quart.send_file(icon_path)
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
import quart
|
|
||||||
|
|
||||||
from ... import group
|
|
||||||
|
|
||||||
|
|
||||||
@group.group_class('bots', '/api/v1/platform/bots')
|
|
||||||
class BotsRouterGroup(group.RouterGroup):
|
|
||||||
async def initialize(self) -> None:
|
|
||||||
@self.route('', methods=['GET', 'POST'])
|
|
||||||
async def _() -> str:
|
|
||||||
if quart.request.method == 'GET':
|
|
||||||
return self.success(data={'bots': await self.ap.bot_service.get_bots()})
|
|
||||||
elif quart.request.method == 'POST':
|
|
||||||
json_data = await quart.request.json
|
|
||||||
bot_uuid = await self.ap.bot_service.create_bot(json_data)
|
|
||||||
return self.success(data={'uuid': bot_uuid})
|
|
||||||
|
|
||||||
@self.route('/<bot_uuid>', methods=['GET', 'PUT', 'DELETE'])
|
|
||||||
async def _(bot_uuid: str) -> str:
|
|
||||||
if quart.request.method == 'GET':
|
|
||||||
bot = await self.ap.bot_service.get_bot(bot_uuid)
|
|
||||||
if bot is None:
|
|
||||||
return self.http_status(404, -1, 'bot not found')
|
|
||||||
return self.success(data={'bot': bot})
|
|
||||||
elif quart.request.method == 'PUT':
|
|
||||||
json_data = await quart.request.json
|
|
||||||
await self.ap.bot_service.update_bot(bot_uuid, json_data)
|
|
||||||
return self.success()
|
|
||||||
elif quart.request.method == 'DELETE':
|
|
||||||
await self.ap.bot_service.delete_bot(bot_uuid)
|
|
||||||
return self.success()
|
|
||||||
|
|
||||||
@self.route('/<bot_uuid>/logs', methods=['POST'])
|
|
||||||
async def _(bot_uuid: str) -> str:
|
|
||||||
json_data = await quart.request.json
|
|
||||||
from_index = json_data.get('from_index', -1)
|
|
||||||
max_count = json_data.get('max_count', 10)
|
|
||||||
logs, total_count = await self.ap.bot_service.list_event_logs(bot_uuid, from_index, max_count)
|
|
||||||
return self.success(
|
|
||||||
data={
|
|
||||||
'logs': logs,
|
|
||||||
'total_count': total_count,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import base64
|
|
||||||
import quart
|
|
||||||
|
|
||||||
from .....core import taskmgr
|
|
||||||
from .. import group
|
|
||||||
from langbot_plugin.runtime.plugin.mgr import PluginInstallSource
|
|
||||||
|
|
||||||
|
|
||||||
@group.group_class('plugins', '/api/v1/plugins')
|
|
||||||
class PluginsRouterGroup(group.RouterGroup):
|
|
||||||
async def initialize(self) -> None:
|
|
||||||
@self.route('', methods=['GET'], auth_type=group.AuthType.USER_TOKEN)
|
|
||||||
async def _() -> str:
|
|
||||||
plugins = await self.ap.plugin_connector.list_plugins()
|
|
||||||
|
|
||||||
return self.success(data={'plugins': plugins})
|
|
||||||
|
|
||||||
@self.route(
|
|
||||||
'/<author>/<plugin_name>/upgrade',
|
|
||||||
methods=['POST'],
|
|
||||||
auth_type=group.AuthType.USER_TOKEN,
|
|
||||||
)
|
|
||||||
async def _(author: str, plugin_name: str) -> str:
|
|
||||||
ctx = taskmgr.TaskContext.new()
|
|
||||||
wrapper = self.ap.task_mgr.create_user_task(
|
|
||||||
self.ap.plugin_connector.upgrade_plugin(author, plugin_name, task_context=ctx),
|
|
||||||
kind='plugin-operation',
|
|
||||||
name=f'plugin-upgrade-{plugin_name}',
|
|
||||||
label=f'Upgrading plugin {plugin_name}',
|
|
||||||
context=ctx,
|
|
||||||
)
|
|
||||||
return self.success(data={'task_id': wrapper.id})
|
|
||||||
|
|
||||||
@self.route(
|
|
||||||
'/<author>/<plugin_name>',
|
|
||||||
methods=['GET', 'DELETE'],
|
|
||||||
auth_type=group.AuthType.USER_TOKEN,
|
|
||||||
)
|
|
||||||
async def _(author: str, plugin_name: str) -> str:
|
|
||||||
if quart.request.method == 'GET':
|
|
||||||
plugin = await self.ap.plugin_connector.get_plugin_info(author, plugin_name)
|
|
||||||
if plugin is None:
|
|
||||||
return self.http_status(404, -1, 'plugin not found')
|
|
||||||
return self.success(data={'plugin': plugin})
|
|
||||||
elif quart.request.method == 'DELETE':
|
|
||||||
ctx = taskmgr.TaskContext.new()
|
|
||||||
wrapper = self.ap.task_mgr.create_user_task(
|
|
||||||
self.ap.plugin_connector.delete_plugin(author, plugin_name, task_context=ctx),
|
|
||||||
kind='plugin-operation',
|
|
||||||
name=f'plugin-remove-{plugin_name}',
|
|
||||||
label=f'Removing plugin {plugin_name}',
|
|
||||||
context=ctx,
|
|
||||||
)
|
|
||||||
|
|
||||||
return self.success(data={'task_id': wrapper.id})
|
|
||||||
|
|
||||||
@self.route(
|
|
||||||
'/<author>/<plugin_name>/config',
|
|
||||||
methods=['GET', 'PUT'],
|
|
||||||
auth_type=group.AuthType.USER_TOKEN,
|
|
||||||
)
|
|
||||||
async def _(author: str, plugin_name: str) -> quart.Response:
|
|
||||||
plugin = await self.ap.plugin_connector.get_plugin_info(author, plugin_name)
|
|
||||||
if plugin is None:
|
|
||||||
return self.http_status(404, -1, 'plugin not found')
|
|
||||||
|
|
||||||
if quart.request.method == 'GET':
|
|
||||||
return self.success(data={'config': plugin['plugin_config']})
|
|
||||||
elif quart.request.method == 'PUT':
|
|
||||||
data = await quart.request.json
|
|
||||||
|
|
||||||
await self.ap.plugin_connector.set_plugin_config(author, plugin_name, data)
|
|
||||||
|
|
||||||
return self.success(data={})
|
|
||||||
|
|
||||||
@self.route(
|
|
||||||
'/<author>/<plugin_name>/icon',
|
|
||||||
methods=['GET'],
|
|
||||||
auth_type=group.AuthType.NONE,
|
|
||||||
)
|
|
||||||
async def _(author: str, plugin_name: str) -> quart.Response:
|
|
||||||
icon_data = await self.ap.plugin_connector.get_plugin_icon(author, plugin_name)
|
|
||||||
icon_base64 = icon_data['plugin_icon_base64']
|
|
||||||
mime_type = icon_data['mime_type']
|
|
||||||
|
|
||||||
icon_data = base64.b64decode(icon_base64)
|
|
||||||
|
|
||||||
return quart.Response(icon_data, mimetype=mime_type)
|
|
||||||
|
|
||||||
@self.route('/install/github', methods=['POST'], auth_type=group.AuthType.USER_TOKEN)
|
|
||||||
async def _() -> str:
|
|
||||||
data = await quart.request.json
|
|
||||||
|
|
||||||
ctx = taskmgr.TaskContext.new()
|
|
||||||
short_source_str = data['source'][-8:]
|
|
||||||
wrapper = self.ap.task_mgr.create_user_task(
|
|
||||||
self.ap.plugin_mgr.install_plugin(data['source'], task_context=ctx),
|
|
||||||
kind='plugin-operation',
|
|
||||||
name='plugin-install-github',
|
|
||||||
label=f'Installing plugin from github ...{short_source_str}',
|
|
||||||
context=ctx,
|
|
||||||
)
|
|
||||||
|
|
||||||
return self.success(data={'task_id': wrapper.id})
|
|
||||||
|
|
||||||
@self.route('/install/marketplace', methods=['POST'], auth_type=group.AuthType.USER_TOKEN)
|
|
||||||
async def _() -> str:
|
|
||||||
data = await quart.request.json
|
|
||||||
|
|
||||||
ctx = taskmgr.TaskContext.new()
|
|
||||||
wrapper = self.ap.task_mgr.create_user_task(
|
|
||||||
self.ap.plugin_connector.install_plugin(PluginInstallSource.MARKETPLACE, data, task_context=ctx),
|
|
||||||
kind='plugin-operation',
|
|
||||||
name='plugin-install-marketplace',
|
|
||||||
label=f'Installing plugin from marketplace ...{data}',
|
|
||||||
context=ctx,
|
|
||||||
)
|
|
||||||
|
|
||||||
return self.success(data={'task_id': wrapper.id})
|
|
||||||
|
|
||||||
@self.route('/install/local', methods=['POST'], auth_type=group.AuthType.USER_TOKEN)
|
|
||||||
async def _() -> str:
|
|
||||||
file = (await quart.request.files).get('file')
|
|
||||||
if file is None:
|
|
||||||
return self.http_status(400, -1, 'file is required')
|
|
||||||
|
|
||||||
file_bytes = file.read()
|
|
||||||
|
|
||||||
data = {
|
|
||||||
'plugin_file': file_bytes,
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx = taskmgr.TaskContext.new()
|
|
||||||
wrapper = self.ap.task_mgr.create_user_task(
|
|
||||||
self.ap.plugin_connector.install_plugin(PluginInstallSource.LOCAL, data, task_context=ctx),
|
|
||||||
kind='plugin-operation',
|
|
||||||
name='plugin-install-local',
|
|
||||||
label=f'Installing plugin from local ...{file.filename}',
|
|
||||||
context=ctx,
|
|
||||||
)
|
|
||||||
|
|
||||||
return self.success(data={'task_id': wrapper.id})
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
import quart
|
|
||||||
|
|
||||||
from ... import group
|
|
||||||
|
|
||||||
|
|
||||||
@group.group_class('models/llm', '/api/v1/provider/models/llm')
|
|
||||||
class LLMModelsRouterGroup(group.RouterGroup):
|
|
||||||
async def initialize(self) -> None:
|
|
||||||
@self.route('', methods=['GET', 'POST'])
|
|
||||||
async def _() -> str:
|
|
||||||
if quart.request.method == 'GET':
|
|
||||||
return self.success(data={'models': await self.ap.llm_model_service.get_llm_models()})
|
|
||||||
elif quart.request.method == 'POST':
|
|
||||||
json_data = await quart.request.json
|
|
||||||
|
|
||||||
model_uuid = await self.ap.llm_model_service.create_llm_model(json_data)
|
|
||||||
|
|
||||||
return self.success(data={'uuid': model_uuid})
|
|
||||||
|
|
||||||
@self.route('/<model_uuid>', methods=['GET', 'PUT', 'DELETE'])
|
|
||||||
async def _(model_uuid: str) -> str:
|
|
||||||
if quart.request.method == 'GET':
|
|
||||||
model = await self.ap.llm_model_service.get_llm_model(model_uuid)
|
|
||||||
|
|
||||||
if model is None:
|
|
||||||
return self.http_status(404, -1, 'model not found')
|
|
||||||
|
|
||||||
return self.success(data={'model': model})
|
|
||||||
elif quart.request.method == 'PUT':
|
|
||||||
json_data = await quart.request.json
|
|
||||||
|
|
||||||
await self.ap.llm_model_service.update_llm_model(model_uuid, json_data)
|
|
||||||
|
|
||||||
return self.success()
|
|
||||||
elif quart.request.method == 'DELETE':
|
|
||||||
await self.ap.llm_model_service.delete_llm_model(model_uuid)
|
|
||||||
|
|
||||||
return self.success()
|
|
||||||
|
|
||||||
@self.route('/<model_uuid>/test', methods=['POST'])
|
|
||||||
async def _(model_uuid: str) -> str:
|
|
||||||
json_data = await quart.request.json
|
|
||||||
|
|
||||||
await self.ap.llm_model_service.test_llm_model(model_uuid, json_data)
|
|
||||||
|
|
||||||
return self.success()
|
|
||||||
|
|
||||||
|
|
||||||
@group.group_class('models/embedding', '/api/v1/provider/models/embedding')
|
|
||||||
class EmbeddingModelsRouterGroup(group.RouterGroup):
|
|
||||||
async def initialize(self) -> None:
|
|
||||||
@self.route('', methods=['GET', 'POST'])
|
|
||||||
async def _() -> str:
|
|
||||||
if quart.request.method == 'GET':
|
|
||||||
return self.success(data={'models': await self.ap.embedding_models_service.get_embedding_models()})
|
|
||||||
elif quart.request.method == 'POST':
|
|
||||||
json_data = await quart.request.json
|
|
||||||
|
|
||||||
model_uuid = await self.ap.embedding_models_service.create_embedding_model(json_data)
|
|
||||||
|
|
||||||
return self.success(data={'uuid': model_uuid})
|
|
||||||
|
|
||||||
@self.route('/<model_uuid>', methods=['GET', 'PUT', 'DELETE'])
|
|
||||||
async def _(model_uuid: str) -> str:
|
|
||||||
if quart.request.method == 'GET':
|
|
||||||
model = await self.ap.embedding_models_service.get_embedding_model(model_uuid)
|
|
||||||
|
|
||||||
if model is None:
|
|
||||||
return self.http_status(404, -1, 'model not found')
|
|
||||||
|
|
||||||
return self.success(data={'model': model})
|
|
||||||
elif quart.request.method == 'PUT':
|
|
||||||
json_data = await quart.request.json
|
|
||||||
|
|
||||||
await self.ap.embedding_models_service.update_embedding_model(model_uuid, json_data)
|
|
||||||
|
|
||||||
return self.success()
|
|
||||||
elif quart.request.method == 'DELETE':
|
|
||||||
await self.ap.embedding_models_service.delete_embedding_model(model_uuid)
|
|
||||||
|
|
||||||
return self.success()
|
|
||||||
|
|
||||||
@self.route('/<model_uuid>/test', methods=['POST'])
|
|
||||||
async def _(model_uuid: str) -> str:
|
|
||||||
json_data = await quart.request.json
|
|
||||||
|
|
||||||
await self.ap.embedding_models_service.test_embedding_model(model_uuid, json_data)
|
|
||||||
|
|
||||||
return self.success()
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
import quart
|
|
||||||
|
|
||||||
from .. import group
|
|
||||||
from .....utils import constants
|
|
||||||
|
|
||||||
|
|
||||||
@group.group_class('system', '/api/v1/system')
|
|
||||||
class SystemRouterGroup(group.RouterGroup):
|
|
||||||
async def initialize(self) -> None:
|
|
||||||
@self.route('/info', methods=['GET'], auth_type=group.AuthType.NONE)
|
|
||||||
async def _() -> str:
|
|
||||||
return self.success(
|
|
||||||
data={
|
|
||||||
'version': constants.semantic_version,
|
|
||||||
'debug': constants.debug_mode,
|
|
||||||
'enabled_platform_count': len(self.ap.platform_mgr.get_running_adapters()),
|
|
||||||
'enable_marketplace': self.ap.instance_config.data.get('plugin', {}).get(
|
|
||||||
'enable_marketplace', True
|
|
||||||
),
|
|
||||||
'cloud_service_url': (
|
|
||||||
self.ap.instance_config.data.get('plugin', {}).get(
|
|
||||||
'cloud_service_url', 'https://space.langbot.app'
|
|
||||||
)
|
|
||||||
if 'cloud_service_url' in self.ap.instance_config.data.get('plugin', {})
|
|
||||||
else 'https://space.langbot.app'
|
|
||||||
),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
@self.route('/tasks', methods=['GET'], auth_type=group.AuthType.USER_TOKEN)
|
|
||||||
async def _() -> str:
|
|
||||||
task_type = quart.request.args.get('type')
|
|
||||||
|
|
||||||
if task_type == '':
|
|
||||||
task_type = None
|
|
||||||
|
|
||||||
return self.success(data=self.ap.task_mgr.get_tasks_dict(task_type))
|
|
||||||
|
|
||||||
@self.route('/tasks/<task_id>', methods=['GET'], auth_type=group.AuthType.USER_TOKEN)
|
|
||||||
async def _(task_id: str) -> str:
|
|
||||||
task = self.ap.task_mgr.get_task_by_id(int(task_id))
|
|
||||||
|
|
||||||
if task is None:
|
|
||||||
return self.http_status(404, 404, 'Task not found')
|
|
||||||
|
|
||||||
return self.success(data=task.to_dict())
|
|
||||||
|
|
||||||
@self.route('/debug/exec', methods=['POST'], auth_type=group.AuthType.USER_TOKEN)
|
|
||||||
async def _() -> str:
|
|
||||||
if not constants.debug_mode:
|
|
||||||
return self.http_status(403, 403, 'Forbidden')
|
|
||||||
|
|
||||||
py_code = await quart.request.data
|
|
||||||
|
|
||||||
ap = self.ap
|
|
||||||
|
|
||||||
return self.success(data=exec(py_code, {'ap': ap}))
|
|
||||||
|
|
||||||
@self.route('/debug/tools/call', methods=['POST'], auth_type=group.AuthType.USER_TOKEN)
|
|
||||||
async def _() -> str:
|
|
||||||
if not constants.debug_mode:
|
|
||||||
return self.http_status(403, 403, 'Forbidden')
|
|
||||||
|
|
||||||
data = await quart.request.json
|
|
||||||
|
|
||||||
return self.success(
|
|
||||||
data=await self.ap.tool_mgr.execute_func_call(data['tool_name'], data['tool_parameters'])
|
|
||||||
)
|
|
||||||
|
|
||||||
@self.route(
|
|
||||||
'/debug/plugin/action',
|
|
||||||
methods=['POST'],
|
|
||||||
auth_type=group.AuthType.USER_TOKEN,
|
|
||||||
)
|
|
||||||
async def _() -> str:
|
|
||||||
if not constants.debug_mode:
|
|
||||||
return self.http_status(403, 403, 'Forbidden')
|
|
||||||
|
|
||||||
data = await quart.request.json
|
|
||||||
|
|
||||||
class AnoymousAction:
|
|
||||||
value = 'anonymous_action'
|
|
||||||
|
|
||||||
def __init__(self, value: str):
|
|
||||||
self.value = value
|
|
||||||
|
|
||||||
resp = await self.ap.plugin_connector.handler.call_action(
|
|
||||||
AnoymousAction(data['action']),
|
|
||||||
data['data'],
|
|
||||||
timeout=data.get('timeout', 10),
|
|
||||||
)
|
|
||||||
|
|
||||||
return self.success(data=resp)
|
|
||||||
|
|
||||||
@self.route(
|
|
||||||
'/status/plugin-system',
|
|
||||||
methods=['GET'],
|
|
||||||
auth_type=group.AuthType.USER_TOKEN,
|
|
||||||
)
|
|
||||||
async def _() -> str:
|
|
||||||
plugin_connector_error = 'ok'
|
|
||||||
is_connected = True
|
|
||||||
|
|
||||||
try:
|
|
||||||
await self.ap.plugin_connector.ping_plugin_runtime()
|
|
||||||
except Exception as e:
|
|
||||||
plugin_connector_error = str(e)
|
|
||||||
is_connected = False
|
|
||||||
|
|
||||||
return self.success(
|
|
||||||
data={
|
|
||||||
'is_enable': self.ap.plugin_connector.is_enable_plugin,
|
|
||||||
'is_connected': is_connected,
|
|
||||||
'plugin_connector_error': plugin_connector_error,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
import quart
|
|
||||||
import argon2
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
from .. import group
|
|
||||||
|
|
||||||
|
|
||||||
@group.group_class('user', '/api/v1/user')
|
|
||||||
class UserRouterGroup(group.RouterGroup):
|
|
||||||
async def initialize(self) -> None:
|
|
||||||
@self.route('/init', methods=['GET', 'POST'], auth_type=group.AuthType.NONE)
|
|
||||||
async def _() -> str:
|
|
||||||
if quart.request.method == 'GET':
|
|
||||||
return self.success(data={'initialized': await self.ap.user_service.is_initialized()})
|
|
||||||
|
|
||||||
if await self.ap.user_service.is_initialized():
|
|
||||||
return self.fail(1, 'System already initialized')
|
|
||||||
|
|
||||||
json_data = await quart.request.json
|
|
||||||
|
|
||||||
user_email = json_data['user']
|
|
||||||
password = json_data['password']
|
|
||||||
|
|
||||||
await self.ap.user_service.create_user(user_email, password)
|
|
||||||
|
|
||||||
return self.success()
|
|
||||||
|
|
||||||
@self.route('/auth', methods=['POST'], auth_type=group.AuthType.NONE)
|
|
||||||
async def _() -> str:
|
|
||||||
json_data = await quart.request.json
|
|
||||||
|
|
||||||
try:
|
|
||||||
token = await self.ap.user_service.authenticate(json_data['user'], json_data['password'])
|
|
||||||
except argon2.exceptions.VerifyMismatchError:
|
|
||||||
return self.fail(1, 'Invalid username or password')
|
|
||||||
|
|
||||||
return self.success(data={'token': token})
|
|
||||||
|
|
||||||
@self.route('/check-token', methods=['GET'], auth_type=group.AuthType.USER_TOKEN)
|
|
||||||
async def _(user_email: str) -> str:
|
|
||||||
token = await self.ap.user_service.generate_jwt_token(user_email)
|
|
||||||
|
|
||||||
return self.success(data={'token': token})
|
|
||||||
|
|
||||||
@self.route('/reset-password', methods=['POST'], auth_type=group.AuthType.NONE)
|
|
||||||
async def _() -> str:
|
|
||||||
json_data = await quart.request.json
|
|
||||||
|
|
||||||
user_email = json_data['user']
|
|
||||||
recovery_key = json_data['recovery_key']
|
|
||||||
new_password = json_data['new_password']
|
|
||||||
|
|
||||||
# hard sleep 3s for security
|
|
||||||
await asyncio.sleep(3)
|
|
||||||
|
|
||||||
if not await self.ap.user_service.is_initialized():
|
|
||||||
return self.http_status(400, -1, 'System not initialized')
|
|
||||||
|
|
||||||
user_obj = await self.ap.user_service.get_user_by_email(user_email)
|
|
||||||
|
|
||||||
if user_obj is None:
|
|
||||||
return self.http_status(400, -1, 'User not found')
|
|
||||||
|
|
||||||
if recovery_key != self.ap.instance_config.data['system']['recovery_key']:
|
|
||||||
return self.http_status(403, -1, 'Invalid recovery key')
|
|
||||||
|
|
||||||
await self.ap.user_service.reset_password(user_email, new_password)
|
|
||||||
|
|
||||||
return self.success(data={'user': user_email})
|
|
||||||
|
|
||||||
@self.route('/change-password', methods=['POST'], auth_type=group.AuthType.USER_TOKEN)
|
|
||||||
async def _(user_email: str) -> str:
|
|
||||||
json_data = await quart.request.json
|
|
||||||
|
|
||||||
current_password = json_data['current_password']
|
|
||||||
new_password = json_data['new_password']
|
|
||||||
|
|
||||||
try:
|
|
||||||
await self.ap.user_service.change_password(user_email, current_password, new_password)
|
|
||||||
except argon2.exceptions.VerifyMismatchError:
|
|
||||||
return self.http_status(400, -1, 'Current password is incorrect')
|
|
||||||
except ValueError as e:
|
|
||||||
return self.http_status(400, -1, str(e))
|
|
||||||
|
|
||||||
return self.success(data={'user': user_email})
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import uuid
|
|
||||||
import sqlalchemy
|
|
||||||
|
|
||||||
from ....core import app
|
|
||||||
from ....entity.persistence import rag as persistence_rag
|
|
||||||
|
|
||||||
|
|
||||||
class KnowledgeService:
|
|
||||||
"""知识库服务"""
|
|
||||||
|
|
||||||
ap: app.Application
|
|
||||||
|
|
||||||
def __init__(self, ap: app.Application) -> None:
|
|
||||||
self.ap = ap
|
|
||||||
|
|
||||||
async def get_knowledge_bases(self) -> list[dict]:
|
|
||||||
"""获取所有知识库"""
|
|
||||||
result = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_rag.KnowledgeBase))
|
|
||||||
knowledge_bases = result.all()
|
|
||||||
return [
|
|
||||||
self.ap.persistence_mgr.serialize_model(persistence_rag.KnowledgeBase, knowledge_base)
|
|
||||||
for knowledge_base in knowledge_bases
|
|
||||||
]
|
|
||||||
|
|
||||||
async def get_knowledge_base(self, kb_uuid: str) -> dict | None:
|
|
||||||
"""获取知识库"""
|
|
||||||
result = await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.select(persistence_rag.KnowledgeBase).where(persistence_rag.KnowledgeBase.uuid == kb_uuid)
|
|
||||||
)
|
|
||||||
knowledge_base = result.first()
|
|
||||||
if knowledge_base is None:
|
|
||||||
return None
|
|
||||||
return self.ap.persistence_mgr.serialize_model(persistence_rag.KnowledgeBase, knowledge_base)
|
|
||||||
|
|
||||||
async def create_knowledge_base(self, kb_data: dict) -> str:
|
|
||||||
"""创建知识库"""
|
|
||||||
kb_data['uuid'] = str(uuid.uuid4())
|
|
||||||
await self.ap.persistence_mgr.execute_async(sqlalchemy.insert(persistence_rag.KnowledgeBase).values(kb_data))
|
|
||||||
|
|
||||||
kb = await self.get_knowledge_base(kb_data['uuid'])
|
|
||||||
|
|
||||||
await self.ap.rag_mgr.load_knowledge_base(kb)
|
|
||||||
|
|
||||||
return kb_data['uuid']
|
|
||||||
|
|
||||||
async def update_knowledge_base(self, kb_uuid: str, kb_data: dict) -> None:
|
|
||||||
"""更新知识库"""
|
|
||||||
if 'uuid' in kb_data:
|
|
||||||
del kb_data['uuid']
|
|
||||||
|
|
||||||
if 'embedding_model_uuid' in kb_data:
|
|
||||||
del kb_data['embedding_model_uuid']
|
|
||||||
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.update(persistence_rag.KnowledgeBase)
|
|
||||||
.values(kb_data)
|
|
||||||
.where(persistence_rag.KnowledgeBase.uuid == kb_uuid)
|
|
||||||
)
|
|
||||||
await self.ap.rag_mgr.remove_knowledge_base_from_runtime(kb_uuid)
|
|
||||||
|
|
||||||
kb = await self.get_knowledge_base(kb_uuid)
|
|
||||||
|
|
||||||
await self.ap.rag_mgr.load_knowledge_base(kb)
|
|
||||||
|
|
||||||
async def store_file(self, kb_uuid: str, file_id: str) -> int:
|
|
||||||
"""存储文件"""
|
|
||||||
# await self.ap.persistence_mgr.execute_async(sqlalchemy.insert(persistence_rag.File).values(kb_id=kb_uuid, file_id=file_id))
|
|
||||||
# await self.ap.rag_mgr.store_file(file_id)
|
|
||||||
runtime_kb = await self.ap.rag_mgr.get_knowledge_base_by_uuid(kb_uuid)
|
|
||||||
if runtime_kb is None:
|
|
||||||
raise Exception('Knowledge base not found')
|
|
||||||
return await runtime_kb.store_file(file_id)
|
|
||||||
|
|
||||||
async def retrieve_knowledge_base(self, kb_uuid: str, query: str) -> list[dict]:
|
|
||||||
"""检索知识库"""
|
|
||||||
runtime_kb = await self.ap.rag_mgr.get_knowledge_base_by_uuid(kb_uuid)
|
|
||||||
if runtime_kb is None:
|
|
||||||
raise Exception('Knowledge base not found')
|
|
||||||
return [
|
|
||||||
result.model_dump() for result in await runtime_kb.retrieve(query, runtime_kb.knowledge_base_entity.top_k)
|
|
||||||
]
|
|
||||||
|
|
||||||
async def get_files_by_knowledge_base(self, kb_uuid: str) -> list[dict]:
|
|
||||||
"""获取知识库文件"""
|
|
||||||
result = await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.select(persistence_rag.File).where(persistence_rag.File.kb_id == kb_uuid)
|
|
||||||
)
|
|
||||||
files = result.all()
|
|
||||||
return [self.ap.persistence_mgr.serialize_model(persistence_rag.File, file) for file in files]
|
|
||||||
|
|
||||||
async def delete_file(self, kb_uuid: str, file_id: str) -> None:
|
|
||||||
"""删除文件"""
|
|
||||||
runtime_kb = await self.ap.rag_mgr.get_knowledge_base_by_uuid(kb_uuid)
|
|
||||||
if runtime_kb is None:
|
|
||||||
raise Exception('Knowledge base not found')
|
|
||||||
await runtime_kb.delete_file(file_id)
|
|
||||||
|
|
||||||
async def delete_knowledge_base(self, kb_uuid: str) -> None:
|
|
||||||
"""删除知识库"""
|
|
||||||
await self.ap.rag_mgr.delete_knowledge_base(kb_uuid)
|
|
||||||
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.delete(persistence_rag.KnowledgeBase).where(persistence_rag.KnowledgeBase.uuid == kb_uuid)
|
|
||||||
)
|
|
||||||
|
|
||||||
# delete files
|
|
||||||
files = await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.select(persistence_rag.File).where(persistence_rag.File.kb_id == kb_uuid)
|
|
||||||
)
|
|
||||||
for file in files:
|
|
||||||
# delete chunks
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.delete(persistence_rag.Chunk).where(persistence_rag.Chunk.file_id == file.uuid)
|
|
||||||
)
|
|
||||||
# delete file
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.delete(persistence_rag.File).where(persistence_rag.File.uuid == file.uuid)
|
|
||||||
)
|
|
||||||
@@ -1,199 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import uuid
|
|
||||||
import sqlalchemy
|
|
||||||
|
|
||||||
from ....core import app
|
|
||||||
from ....entity.persistence import model as persistence_model
|
|
||||||
from ....entity.persistence import pipeline as persistence_pipeline
|
|
||||||
from ....provider.modelmgr import requester as model_requester
|
|
||||||
from langbot_plugin.api.entities.builtin.provider import message as provider_message
|
|
||||||
|
|
||||||
|
|
||||||
class LLMModelsService:
|
|
||||||
ap: app.Application
|
|
||||||
|
|
||||||
def __init__(self, ap: app.Application) -> None:
|
|
||||||
self.ap = ap
|
|
||||||
|
|
||||||
async def get_llm_models(self, include_secret: bool = True) -> list[dict]:
|
|
||||||
result = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_model.LLMModel))
|
|
||||||
|
|
||||||
models = result.all()
|
|
||||||
|
|
||||||
masked_columns = []
|
|
||||||
if not include_secret:
|
|
||||||
masked_columns = ['api_keys']
|
|
||||||
|
|
||||||
return [
|
|
||||||
self.ap.persistence_mgr.serialize_model(persistence_model.LLMModel, model, masked_columns)
|
|
||||||
for model in models
|
|
||||||
]
|
|
||||||
|
|
||||||
async def create_llm_model(self, model_data: dict) -> str:
|
|
||||||
model_data['uuid'] = str(uuid.uuid4())
|
|
||||||
|
|
||||||
await self.ap.persistence_mgr.execute_async(sqlalchemy.insert(persistence_model.LLMModel).values(**model_data))
|
|
||||||
|
|
||||||
llm_model = await self.get_llm_model(model_data['uuid'])
|
|
||||||
|
|
||||||
await self.ap.model_mgr.load_llm_model(llm_model)
|
|
||||||
|
|
||||||
# check if default pipeline has no model bound
|
|
||||||
result = await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.select(persistence_pipeline.LegacyPipeline).where(
|
|
||||||
persistence_pipeline.LegacyPipeline.is_default == True
|
|
||||||
)
|
|
||||||
)
|
|
||||||
pipeline = result.first()
|
|
||||||
if pipeline is not None and pipeline.config['ai']['local-agent']['model'] == '':
|
|
||||||
pipeline_config = pipeline.config
|
|
||||||
pipeline_config['ai']['local-agent']['model'] = model_data['uuid']
|
|
||||||
pipeline_data = {'config': pipeline_config}
|
|
||||||
await self.ap.pipeline_service.update_pipeline(pipeline.uuid, pipeline_data)
|
|
||||||
|
|
||||||
return model_data['uuid']
|
|
||||||
|
|
||||||
async def get_llm_model(self, model_uuid: str) -> dict | None:
|
|
||||||
result = await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.select(persistence_model.LLMModel).where(persistence_model.LLMModel.uuid == model_uuid)
|
|
||||||
)
|
|
||||||
|
|
||||||
model = result.first()
|
|
||||||
|
|
||||||
if model is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return self.ap.persistence_mgr.serialize_model(persistence_model.LLMModel, model)
|
|
||||||
|
|
||||||
async def update_llm_model(self, model_uuid: str, model_data: dict) -> None:
|
|
||||||
if 'uuid' in model_data:
|
|
||||||
del model_data['uuid']
|
|
||||||
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.update(persistence_model.LLMModel)
|
|
||||||
.where(persistence_model.LLMModel.uuid == model_uuid)
|
|
||||||
.values(**model_data)
|
|
||||||
)
|
|
||||||
|
|
||||||
await self.ap.model_mgr.remove_llm_model(model_uuid)
|
|
||||||
|
|
||||||
llm_model = await self.get_llm_model(model_uuid)
|
|
||||||
|
|
||||||
await self.ap.model_mgr.load_llm_model(llm_model)
|
|
||||||
|
|
||||||
async def delete_llm_model(self, model_uuid: str) -> None:
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.delete(persistence_model.LLMModel).where(persistence_model.LLMModel.uuid == model_uuid)
|
|
||||||
)
|
|
||||||
|
|
||||||
await self.ap.model_mgr.remove_llm_model(model_uuid)
|
|
||||||
|
|
||||||
async def test_llm_model(self, model_uuid: str, model_data: dict) -> None:
|
|
||||||
runtime_llm_model: model_requester.RuntimeLLMModel | None = None
|
|
||||||
|
|
||||||
if model_uuid != '_':
|
|
||||||
for model in self.ap.model_mgr.llm_models:
|
|
||||||
if model.model_entity.uuid == model_uuid:
|
|
||||||
runtime_llm_model = model
|
|
||||||
break
|
|
||||||
|
|
||||||
if runtime_llm_model is None:
|
|
||||||
raise Exception('model not found')
|
|
||||||
|
|
||||||
else:
|
|
||||||
runtime_llm_model = await self.ap.model_mgr.init_runtime_llm_model(model_data)
|
|
||||||
|
|
||||||
await runtime_llm_model.requester.invoke_llm(
|
|
||||||
query=None,
|
|
||||||
model=runtime_llm_model,
|
|
||||||
messages=[provider_message.Message(role='user', content='Hello, world!')],
|
|
||||||
funcs=[],
|
|
||||||
extra_args=model_data.get('extra_args', {}),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class EmbeddingModelsService:
|
|
||||||
ap: app.Application
|
|
||||||
|
|
||||||
def __init__(self, ap: app.Application) -> None:
|
|
||||||
self.ap = ap
|
|
||||||
|
|
||||||
async def get_embedding_models(self) -> list[dict]:
|
|
||||||
result = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_model.EmbeddingModel))
|
|
||||||
|
|
||||||
models = result.all()
|
|
||||||
return [self.ap.persistence_mgr.serialize_model(persistence_model.EmbeddingModel, model) for model in models]
|
|
||||||
|
|
||||||
async def create_embedding_model(self, model_data: dict) -> str:
|
|
||||||
model_data['uuid'] = str(uuid.uuid4())
|
|
||||||
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.insert(persistence_model.EmbeddingModel).values(**model_data)
|
|
||||||
)
|
|
||||||
|
|
||||||
embedding_model = await self.get_embedding_model(model_data['uuid'])
|
|
||||||
|
|
||||||
await self.ap.model_mgr.load_embedding_model(embedding_model)
|
|
||||||
|
|
||||||
return model_data['uuid']
|
|
||||||
|
|
||||||
async def get_embedding_model(self, model_uuid: str) -> dict | None:
|
|
||||||
result = await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.select(persistence_model.EmbeddingModel).where(
|
|
||||||
persistence_model.EmbeddingModel.uuid == model_uuid
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
model = result.first()
|
|
||||||
|
|
||||||
if model is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return self.ap.persistence_mgr.serialize_model(persistence_model.EmbeddingModel, model)
|
|
||||||
|
|
||||||
async def update_embedding_model(self, model_uuid: str, model_data: dict) -> None:
|
|
||||||
if 'uuid' in model_data:
|
|
||||||
del model_data['uuid']
|
|
||||||
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.update(persistence_model.EmbeddingModel)
|
|
||||||
.where(persistence_model.EmbeddingModel.uuid == model_uuid)
|
|
||||||
.values(**model_data)
|
|
||||||
)
|
|
||||||
|
|
||||||
await self.ap.model_mgr.remove_embedding_model(model_uuid)
|
|
||||||
|
|
||||||
embedding_model = await self.get_embedding_model(model_uuid)
|
|
||||||
|
|
||||||
await self.ap.model_mgr.load_embedding_model(embedding_model)
|
|
||||||
|
|
||||||
async def delete_embedding_model(self, model_uuid: str) -> None:
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.delete(persistence_model.EmbeddingModel).where(
|
|
||||||
persistence_model.EmbeddingModel.uuid == model_uuid
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
await self.ap.model_mgr.remove_embedding_model(model_uuid)
|
|
||||||
|
|
||||||
async def test_embedding_model(self, model_uuid: str, model_data: dict) -> None:
|
|
||||||
runtime_embedding_model: model_requester.RuntimeEmbeddingModel | None = None
|
|
||||||
|
|
||||||
if model_uuid != '_':
|
|
||||||
for model in self.ap.model_mgr.embedding_models:
|
|
||||||
if model.model_entity.uuid == model_uuid:
|
|
||||||
runtime_embedding_model = model
|
|
||||||
break
|
|
||||||
|
|
||||||
if runtime_embedding_model is None:
|
|
||||||
raise Exception('model not found')
|
|
||||||
|
|
||||||
else:
|
|
||||||
runtime_embedding_model = await self.ap.model_mgr.init_runtime_embedding_model(model_data)
|
|
||||||
|
|
||||||
await runtime_embedding_model.requester.invoke_embedding(
|
|
||||||
model=runtime_embedding_model,
|
|
||||||
input_text=['Hello, world!'],
|
|
||||||
extra_args={},
|
|
||||||
)
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import uuid
|
|
||||||
import json
|
|
||||||
import sqlalchemy
|
|
||||||
|
|
||||||
from ....core import app
|
|
||||||
from ....entity.persistence import pipeline as persistence_pipeline
|
|
||||||
|
|
||||||
|
|
||||||
default_stage_order = [
|
|
||||||
'GroupRespondRuleCheckStage', # 群响应规则检查
|
|
||||||
'BanSessionCheckStage', # 封禁会话检查
|
|
||||||
'PreContentFilterStage', # 内容过滤前置阶段
|
|
||||||
'PreProcessor', # 预处理器
|
|
||||||
'ConversationMessageTruncator', # 会话消息截断器
|
|
||||||
'RequireRateLimitOccupancy', # 请求速率限制占用
|
|
||||||
'MessageProcessor', # 处理器
|
|
||||||
'ReleaseRateLimitOccupancy', # 释放速率限制占用
|
|
||||||
'PostContentFilterStage', # 内容过滤后置阶段
|
|
||||||
'ResponseWrapper', # 响应包装器
|
|
||||||
'LongTextProcessStage', # 长文本处理
|
|
||||||
'SendResponseBackStage', # 发送响应
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class PipelineService:
|
|
||||||
ap: app.Application
|
|
||||||
|
|
||||||
def __init__(self, ap: app.Application) -> None:
|
|
||||||
self.ap = ap
|
|
||||||
|
|
||||||
async def get_pipeline_metadata(self) -> dict:
|
|
||||||
return [
|
|
||||||
self.ap.pipeline_config_meta_trigger.data,
|
|
||||||
self.ap.pipeline_config_meta_safety.data,
|
|
||||||
self.ap.pipeline_config_meta_ai.data,
|
|
||||||
self.ap.pipeline_config_meta_output.data,
|
|
||||||
]
|
|
||||||
|
|
||||||
async def get_pipelines(self, sort_by: str = 'created_at', sort_order: str = 'DESC') -> list[dict]:
|
|
||||||
query = sqlalchemy.select(persistence_pipeline.LegacyPipeline)
|
|
||||||
|
|
||||||
if sort_by == 'created_at':
|
|
||||||
if sort_order == 'DESC':
|
|
||||||
query = query.order_by(persistence_pipeline.LegacyPipeline.created_at.desc())
|
|
||||||
else:
|
|
||||||
query = query.order_by(persistence_pipeline.LegacyPipeline.created_at.asc())
|
|
||||||
elif sort_by == 'updated_at':
|
|
||||||
if sort_order == 'DESC':
|
|
||||||
query = query.order_by(persistence_pipeline.LegacyPipeline.updated_at.desc())
|
|
||||||
else:
|
|
||||||
query = query.order_by(persistence_pipeline.LegacyPipeline.updated_at.asc())
|
|
||||||
|
|
||||||
result = await self.ap.persistence_mgr.execute_async(query)
|
|
||||||
pipelines = result.all()
|
|
||||||
return [
|
|
||||||
self.ap.persistence_mgr.serialize_model(persistence_pipeline.LegacyPipeline, pipeline)
|
|
||||||
for pipeline in pipelines
|
|
||||||
]
|
|
||||||
|
|
||||||
async def get_pipeline(self, pipeline_uuid: str) -> dict | None:
|
|
||||||
result = await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.select(persistence_pipeline.LegacyPipeline).where(
|
|
||||||
persistence_pipeline.LegacyPipeline.uuid == pipeline_uuid
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
pipeline = result.first()
|
|
||||||
|
|
||||||
if pipeline is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return self.ap.persistence_mgr.serialize_model(persistence_pipeline.LegacyPipeline, pipeline)
|
|
||||||
|
|
||||||
async def create_pipeline(self, pipeline_data: dict, default: bool = False) -> str:
|
|
||||||
pipeline_data['uuid'] = str(uuid.uuid4())
|
|
||||||
pipeline_data['for_version'] = self.ap.ver_mgr.get_current_version()
|
|
||||||
pipeline_data['stages'] = default_stage_order.copy()
|
|
||||||
pipeline_data['is_default'] = default
|
|
||||||
pipeline_data['config'] = json.load(open('templates/default-pipeline-config.json', 'r', encoding='utf-8'))
|
|
||||||
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.insert(persistence_pipeline.LegacyPipeline).values(**pipeline_data)
|
|
||||||
)
|
|
||||||
|
|
||||||
pipeline = await self.get_pipeline(pipeline_data['uuid'])
|
|
||||||
|
|
||||||
await self.ap.pipeline_mgr.load_pipeline(pipeline)
|
|
||||||
|
|
||||||
return pipeline_data['uuid']
|
|
||||||
|
|
||||||
async def update_pipeline(self, pipeline_uuid: str, pipeline_data: dict) -> None:
|
|
||||||
if 'uuid' in pipeline_data:
|
|
||||||
del pipeline_data['uuid']
|
|
||||||
if 'for_version' in pipeline_data:
|
|
||||||
del pipeline_data['for_version']
|
|
||||||
if 'stages' in pipeline_data:
|
|
||||||
del pipeline_data['stages']
|
|
||||||
if 'is_default' in pipeline_data:
|
|
||||||
del pipeline_data['is_default']
|
|
||||||
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.update(persistence_pipeline.LegacyPipeline)
|
|
||||||
.where(persistence_pipeline.LegacyPipeline.uuid == pipeline_uuid)
|
|
||||||
.values(**pipeline_data)
|
|
||||||
)
|
|
||||||
|
|
||||||
pipeline = await self.get_pipeline(pipeline_uuid)
|
|
||||||
|
|
||||||
if 'name' in pipeline_data:
|
|
||||||
from ....entity.persistence import bot as persistence_bot
|
|
||||||
|
|
||||||
result = await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.select(persistence_bot.Bot).where(persistence_bot.Bot.use_pipeline_uuid == pipeline_uuid)
|
|
||||||
)
|
|
||||||
|
|
||||||
bots = result.all()
|
|
||||||
|
|
||||||
for bot in bots:
|
|
||||||
bot_data = {'use_pipeline_name': pipeline_data['name']}
|
|
||||||
await self.ap.bot_service.update_bot(bot.uuid, bot_data)
|
|
||||||
|
|
||||||
await self.ap.pipeline_mgr.remove_pipeline(pipeline_uuid)
|
|
||||||
await self.ap.pipeline_mgr.load_pipeline(pipeline)
|
|
||||||
|
|
||||||
# update all conversation that use this pipeline
|
|
||||||
for session in self.ap.sess_mgr.session_list:
|
|
||||||
if session.using_conversation is not None and session.using_conversation.pipeline_uuid == pipeline_uuid:
|
|
||||||
session.using_conversation = None
|
|
||||||
|
|
||||||
async def delete_pipeline(self, pipeline_uuid: str) -> None:
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.delete(persistence_pipeline.LegacyPipeline).where(
|
|
||||||
persistence_pipeline.LegacyPipeline.uuid == pipeline_uuid
|
|
||||||
)
|
|
||||||
)
|
|
||||||
await self.ap.pipeline_mgr.remove_pipeline(pipeline_uuid)
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import sqlalchemy
|
|
||||||
import argon2
|
|
||||||
import jwt
|
|
||||||
import datetime
|
|
||||||
|
|
||||||
from ....core import app
|
|
||||||
from ....entity.persistence import user
|
|
||||||
from ....utils import constants
|
|
||||||
|
|
||||||
|
|
||||||
class UserService:
|
|
||||||
ap: app.Application
|
|
||||||
|
|
||||||
def __init__(self, ap: app.Application) -> None:
|
|
||||||
self.ap = ap
|
|
||||||
|
|
||||||
async def is_initialized(self) -> bool:
|
|
||||||
result = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(user.User).limit(1))
|
|
||||||
|
|
||||||
result_list = result.all()
|
|
||||||
return result_list is not None and len(result_list) > 0
|
|
||||||
|
|
||||||
async def create_user(self, user_email: str, password: str) -> None:
|
|
||||||
ph = argon2.PasswordHasher()
|
|
||||||
|
|
||||||
hashed_password = ph.hash(password)
|
|
||||||
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.insert(user.User).values(user=user_email, password=hashed_password)
|
|
||||||
)
|
|
||||||
|
|
||||||
async def get_user_by_email(self, user_email: str) -> user.User | None:
|
|
||||||
result = await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.select(user.User).where(user.User.user == user_email)
|
|
||||||
)
|
|
||||||
|
|
||||||
result_list = result.all()
|
|
||||||
return result_list[0] if result_list is not None and len(result_list) > 0 else None
|
|
||||||
|
|
||||||
async def authenticate(self, user_email: str, password: str) -> str | None:
|
|
||||||
result = await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.select(user.User).where(user.User.user == user_email)
|
|
||||||
)
|
|
||||||
|
|
||||||
result_list = result.all()
|
|
||||||
|
|
||||||
if result_list is None or len(result_list) == 0:
|
|
||||||
raise ValueError('用户不存在')
|
|
||||||
|
|
||||||
user_obj = result_list[0]
|
|
||||||
|
|
||||||
ph = argon2.PasswordHasher()
|
|
||||||
|
|
||||||
ph.verify(user_obj.password, password)
|
|
||||||
|
|
||||||
return await self.generate_jwt_token(user_email)
|
|
||||||
|
|
||||||
async def generate_jwt_token(self, user_email: str) -> str:
|
|
||||||
jwt_secret = self.ap.instance_config.data['system']['jwt']['secret']
|
|
||||||
jwt_expire = self.ap.instance_config.data['system']['jwt']['expire']
|
|
||||||
|
|
||||||
payload = {
|
|
||||||
'user': user_email,
|
|
||||||
'iss': 'LangBot-' + constants.edition,
|
|
||||||
'exp': datetime.datetime.now() + datetime.timedelta(seconds=jwt_expire),
|
|
||||||
}
|
|
||||||
|
|
||||||
return jwt.encode(payload, jwt_secret, algorithm='HS256')
|
|
||||||
|
|
||||||
async def verify_jwt_token(self, token: str) -> str:
|
|
||||||
jwt_secret = self.ap.instance_config.data['system']['jwt']['secret']
|
|
||||||
|
|
||||||
return jwt.decode(token, jwt_secret, algorithms=['HS256'])['user']
|
|
||||||
|
|
||||||
async def reset_password(self, user_email: str, new_password: str) -> None:
|
|
||||||
ph = argon2.PasswordHasher()
|
|
||||||
|
|
||||||
hashed_password = ph.hash(new_password)
|
|
||||||
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.update(user.User).where(user.User.user == user_email).values(password=hashed_password)
|
|
||||||
)
|
|
||||||
|
|
||||||
async def change_password(self, user_email: str, current_password: str, new_password: str) -> None:
|
|
||||||
ph = argon2.PasswordHasher()
|
|
||||||
|
|
||||||
user_obj = await self.get_user_by_email(user_email)
|
|
||||||
if user_obj is None:
|
|
||||||
raise ValueError('User not found')
|
|
||||||
|
|
||||||
ph.verify(user_obj.password, current_password)
|
|
||||||
|
|
||||||
hashed_password = ph.hash(new_password)
|
|
||||||
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.update(user.User).where(user.User.user == user_email).values(password=hashed_password)
|
|
||||||
)
|
|
||||||
199
pkg/core/app.py
199
pkg/core/app.py
@@ -1,199 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import logging
|
|
||||||
import asyncio
|
|
||||||
import traceback
|
|
||||||
import os
|
|
||||||
|
|
||||||
from ..platform import botmgr as im_mgr
|
|
||||||
from ..provider.session import sessionmgr as llm_session_mgr
|
|
||||||
from ..provider.modelmgr import modelmgr as llm_model_mgr
|
|
||||||
from ..provider.tools import toolmgr as llm_tool_mgr
|
|
||||||
from ..config import manager as config_mgr
|
|
||||||
from ..command import cmdmgr
|
|
||||||
from ..plugin import connector as plugin_connector
|
|
||||||
from ..pipeline import pool
|
|
||||||
from ..pipeline import controller, pipelinemgr
|
|
||||||
from ..utils import version as version_mgr, proxy as proxy_mgr, announce as announce_mgr
|
|
||||||
from ..persistence import mgr as persistencemgr
|
|
||||||
from ..api.http.controller import main as http_controller
|
|
||||||
from ..api.http.service import user as user_service
|
|
||||||
from ..api.http.service import model as model_service
|
|
||||||
from ..api.http.service import pipeline as pipeline_service
|
|
||||||
from ..api.http.service import bot as bot_service
|
|
||||||
from ..api.http.service import knowledge as knowledge_service
|
|
||||||
from ..discover import engine as discover_engine
|
|
||||||
from ..storage import mgr as storagemgr
|
|
||||||
from ..utils import logcache
|
|
||||||
from . import taskmgr
|
|
||||||
from . import entities as core_entities
|
|
||||||
from ..rag.knowledge import kbmgr as rag_mgr
|
|
||||||
from ..vector import mgr as vectordb_mgr
|
|
||||||
|
|
||||||
|
|
||||||
class Application:
|
|
||||||
"""Runtime application object and context"""
|
|
||||||
|
|
||||||
event_loop: asyncio.AbstractEventLoop = None
|
|
||||||
|
|
||||||
# asyncio_tasks: list[asyncio.Task] = []
|
|
||||||
task_mgr: taskmgr.AsyncTaskManager = None
|
|
||||||
|
|
||||||
discover: discover_engine.ComponentDiscoveryEngine = None
|
|
||||||
|
|
||||||
platform_mgr: im_mgr.PlatformManager = None
|
|
||||||
|
|
||||||
cmd_mgr: cmdmgr.CommandManager = None
|
|
||||||
|
|
||||||
sess_mgr: llm_session_mgr.SessionManager = None
|
|
||||||
|
|
||||||
model_mgr: llm_model_mgr.ModelManager = None
|
|
||||||
|
|
||||||
rag_mgr: rag_mgr.RAGManager = None
|
|
||||||
|
|
||||||
# TODO move to pipeline
|
|
||||||
tool_mgr: llm_tool_mgr.ToolManager = None
|
|
||||||
|
|
||||||
# ======= Config manager =======
|
|
||||||
|
|
||||||
command_cfg: config_mgr.ConfigManager = None # deprecated
|
|
||||||
|
|
||||||
pipeline_cfg: config_mgr.ConfigManager = None # deprecated
|
|
||||||
|
|
||||||
platform_cfg: config_mgr.ConfigManager = None # deprecated
|
|
||||||
|
|
||||||
provider_cfg: config_mgr.ConfigManager = None # deprecated
|
|
||||||
|
|
||||||
system_cfg: config_mgr.ConfigManager = None # deprecated
|
|
||||||
|
|
||||||
instance_config: config_mgr.ConfigManager = None
|
|
||||||
|
|
||||||
# ======= Metadata config manager =======
|
|
||||||
|
|
||||||
sensitive_meta: config_mgr.ConfigManager = None
|
|
||||||
|
|
||||||
pipeline_config_meta_trigger: config_mgr.ConfigManager = None
|
|
||||||
pipeline_config_meta_safety: config_mgr.ConfigManager = None
|
|
||||||
pipeline_config_meta_ai: config_mgr.ConfigManager = None
|
|
||||||
pipeline_config_meta_output: config_mgr.ConfigManager = None
|
|
||||||
|
|
||||||
# =========================
|
|
||||||
|
|
||||||
plugin_connector: plugin_connector.PluginRuntimeConnector = None
|
|
||||||
|
|
||||||
query_pool: pool.QueryPool = None
|
|
||||||
|
|
||||||
ctrl: controller.Controller = None
|
|
||||||
|
|
||||||
pipeline_mgr: pipelinemgr.PipelineManager = None
|
|
||||||
|
|
||||||
ver_mgr: version_mgr.VersionManager = None
|
|
||||||
|
|
||||||
ann_mgr: announce_mgr.AnnouncementManager = None
|
|
||||||
|
|
||||||
proxy_mgr: proxy_mgr.ProxyManager = None
|
|
||||||
|
|
||||||
logger: logging.Logger = None
|
|
||||||
|
|
||||||
persistence_mgr: persistencemgr.PersistenceManager = None
|
|
||||||
|
|
||||||
vector_db_mgr: vectordb_mgr.VectorDBManager = None
|
|
||||||
|
|
||||||
http_ctrl: http_controller.HTTPController = None
|
|
||||||
|
|
||||||
log_cache: logcache.LogCache = None
|
|
||||||
|
|
||||||
storage_mgr: storagemgr.StorageMgr = None
|
|
||||||
|
|
||||||
# ========= HTTP Services =========
|
|
||||||
|
|
||||||
user_service: user_service.UserService = None
|
|
||||||
|
|
||||||
llm_model_service: model_service.LLMModelsService = None
|
|
||||||
|
|
||||||
embedding_models_service: model_service.EmbeddingModelsService = None
|
|
||||||
|
|
||||||
pipeline_service: pipeline_service.PipelineService = None
|
|
||||||
|
|
||||||
bot_service: bot_service.BotService = None
|
|
||||||
|
|
||||||
knowledge_service: knowledge_service.KnowledgeService = None
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def initialize(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def run(self):
|
|
||||||
try:
|
|
||||||
await self.plugin_connector.initialize_plugins()
|
|
||||||
|
|
||||||
# 后续可能会允许动态重启其他任务
|
|
||||||
# 故为了防止程序在非 Ctrl-C 情况下退出,这里创建一个不会结束的协程
|
|
||||||
async def never_ending():
|
|
||||||
while True:
|
|
||||||
await asyncio.sleep(1)
|
|
||||||
|
|
||||||
self.task_mgr.create_task(
|
|
||||||
self.platform_mgr.run(),
|
|
||||||
name='platform-manager',
|
|
||||||
scopes=[
|
|
||||||
core_entities.LifecycleControlScope.APPLICATION,
|
|
||||||
core_entities.LifecycleControlScope.PLATFORM,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
self.task_mgr.create_task(
|
|
||||||
self.ctrl.run(),
|
|
||||||
name='query-controller',
|
|
||||||
scopes=[core_entities.LifecycleControlScope.APPLICATION],
|
|
||||||
)
|
|
||||||
self.task_mgr.create_task(
|
|
||||||
self.http_ctrl.run(),
|
|
||||||
name='http-api-controller',
|
|
||||||
scopes=[core_entities.LifecycleControlScope.APPLICATION],
|
|
||||||
)
|
|
||||||
|
|
||||||
self.task_mgr.create_task(
|
|
||||||
never_ending(),
|
|
||||||
name='never-ending-task',
|
|
||||||
scopes=[core_entities.LifecycleControlScope.APPLICATION],
|
|
||||||
)
|
|
||||||
|
|
||||||
await self.print_web_access_info()
|
|
||||||
await self.task_mgr.wait_all()
|
|
||||||
except asyncio.CancelledError:
|
|
||||||
pass
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.error(f'Application runtime fatal exception: {e}')
|
|
||||||
self.logger.debug(f'Traceback: {traceback.format_exc()}')
|
|
||||||
|
|
||||||
def dispose(self):
|
|
||||||
self.plugin_connector.dispose()
|
|
||||||
|
|
||||||
async def print_web_access_info(self):
|
|
||||||
"""Print access webui tips"""
|
|
||||||
|
|
||||||
if not os.path.exists(os.path.join('.', 'web/out')):
|
|
||||||
self.logger.warning('WebUI 文件缺失,请根据文档部署:https://docs.langbot.app/zh')
|
|
||||||
self.logger.warning(
|
|
||||||
'WebUI files are missing, please deploy according to the documentation: https://docs.langbot.app/en'
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
host_ip = '127.0.0.1'
|
|
||||||
|
|
||||||
port = self.instance_config.data['api']['port']
|
|
||||||
|
|
||||||
tips = f"""
|
|
||||||
=======================================
|
|
||||||
✨ Access WebUI / 访问管理面板
|
|
||||||
|
|
||||||
🏠 Local Address: http://{host_ip}:{port}/
|
|
||||||
🌐 Public Address: http://<Your Public IP>:{port}/
|
|
||||||
|
|
||||||
📌 Running this program in a container? Please ensure that the {port} port is exposed
|
|
||||||
=======================================
|
|
||||||
""".strip()
|
|
||||||
for line in tips.split('\n'):
|
|
||||||
self.logger.info(line)
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
from .. import stage, app
|
|
||||||
from ..bootutils import config
|
|
||||||
|
|
||||||
|
|
||||||
@stage.stage_class('LoadConfigStage')
|
|
||||||
class LoadConfigStage(stage.BootingStage):
|
|
||||||
"""Load config file stage"""
|
|
||||||
|
|
||||||
async def run(self, ap: app.Application):
|
|
||||||
"""Load config file"""
|
|
||||||
|
|
||||||
# ======= deprecated =======
|
|
||||||
if os.path.exists('data/config/command.json'):
|
|
||||||
ap.command_cfg = await config.load_json_config(
|
|
||||||
'data/config/command.json',
|
|
||||||
'templates/legacy/command.json',
|
|
||||||
completion=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
if os.path.exists('data/config/pipeline.json'):
|
|
||||||
ap.pipeline_cfg = await config.load_json_config(
|
|
||||||
'data/config/pipeline.json',
|
|
||||||
'templates/legacy/pipeline.json',
|
|
||||||
completion=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
if os.path.exists('data/config/platform.json'):
|
|
||||||
ap.platform_cfg = await config.load_json_config(
|
|
||||||
'data/config/platform.json',
|
|
||||||
'templates/legacy/platform.json',
|
|
||||||
completion=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
if os.path.exists('data/config/provider.json'):
|
|
||||||
ap.provider_cfg = await config.load_json_config(
|
|
||||||
'data/config/provider.json',
|
|
||||||
'templates/legacy/provider.json',
|
|
||||||
completion=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
if os.path.exists('data/config/system.json'):
|
|
||||||
ap.system_cfg = await config.load_json_config(
|
|
||||||
'data/config/system.json',
|
|
||||||
'templates/legacy/system.json',
|
|
||||||
completion=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
# ======= deprecated =======
|
|
||||||
|
|
||||||
ap.instance_config = await config.load_yaml_config(
|
|
||||||
'data/config.yaml', 'templates/config.yaml', completion=False
|
|
||||||
)
|
|
||||||
await ap.instance_config.dump_config()
|
|
||||||
|
|
||||||
ap.sensitive_meta = await config.load_json_config(
|
|
||||||
'data/metadata/sensitive-words.json',
|
|
||||||
'templates/metadata/sensitive-words.json',
|
|
||||||
)
|
|
||||||
await ap.sensitive_meta.dump_config()
|
|
||||||
|
|
||||||
ap.pipeline_config_meta_trigger = await config.load_yaml_config(
|
|
||||||
'templates/metadata/pipeline/trigger.yaml',
|
|
||||||
'templates/metadata/pipeline/trigger.yaml',
|
|
||||||
)
|
|
||||||
ap.pipeline_config_meta_safety = await config.load_yaml_config(
|
|
||||||
'templates/metadata/pipeline/safety.yaml',
|
|
||||||
'templates/metadata/pipeline/safety.yaml',
|
|
||||||
)
|
|
||||||
ap.pipeline_config_meta_ai = await config.load_yaml_config(
|
|
||||||
'templates/metadata/pipeline/ai.yaml', 'templates/metadata/pipeline/ai.yaml'
|
|
||||||
)
|
|
||||||
ap.pipeline_config_meta_output = await config.load_yaml_config(
|
|
||||||
'templates/metadata/pipeline/output.yaml',
|
|
||||||
'templates/metadata/pipeline/output.yaml',
|
|
||||||
)
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import sqlalchemy
|
|
||||||
|
|
||||||
from .base import Base
|
|
||||||
|
|
||||||
|
|
||||||
class LLMModel(Base):
|
|
||||||
"""LLM model"""
|
|
||||||
|
|
||||||
__tablename__ = 'llm_models'
|
|
||||||
|
|
||||||
uuid = sqlalchemy.Column(sqlalchemy.String(255), primary_key=True, unique=True)
|
|
||||||
name = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
|
|
||||||
description = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
|
|
||||||
requester = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
|
|
||||||
requester_config = sqlalchemy.Column(sqlalchemy.JSON, nullable=False, default={})
|
|
||||||
api_keys = sqlalchemy.Column(sqlalchemy.JSON, nullable=False)
|
|
||||||
abilities = sqlalchemy.Column(sqlalchemy.JSON, nullable=False, default=[])
|
|
||||||
extra_args = sqlalchemy.Column(sqlalchemy.JSON, nullable=False, default={})
|
|
||||||
created_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False, server_default=sqlalchemy.func.now())
|
|
||||||
updated_at = sqlalchemy.Column(
|
|
||||||
sqlalchemy.DateTime,
|
|
||||||
nullable=False,
|
|
||||||
server_default=sqlalchemy.func.now(),
|
|
||||||
onupdate=sqlalchemy.func.now(),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class EmbeddingModel(Base):
|
|
||||||
"""Embedding 模型"""
|
|
||||||
|
|
||||||
__tablename__ = 'embedding_models'
|
|
||||||
|
|
||||||
uuid = sqlalchemy.Column(sqlalchemy.String(255), primary_key=True, unique=True)
|
|
||||||
name = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
|
|
||||||
description = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
|
|
||||||
requester = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
|
|
||||||
requester_config = sqlalchemy.Column(sqlalchemy.JSON, nullable=False, default={})
|
|
||||||
api_keys = sqlalchemy.Column(sqlalchemy.JSON, nullable=False)
|
|
||||||
extra_args = sqlalchemy.Column(sqlalchemy.JSON, nullable=False, default={})
|
|
||||||
created_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False, server_default=sqlalchemy.func.now())
|
|
||||||
updated_at = sqlalchemy.Column(
|
|
||||||
sqlalchemy.DateTime,
|
|
||||||
nullable=False,
|
|
||||||
server_default=sqlalchemy.func.now(),
|
|
||||||
onupdate=sqlalchemy.func.now(),
|
|
||||||
)
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import sqlalchemy
|
|
||||||
|
|
||||||
from .base import Base
|
|
||||||
|
|
||||||
|
|
||||||
class User(Base):
|
|
||||||
__tablename__ = 'users'
|
|
||||||
|
|
||||||
id = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True)
|
|
||||||
user = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
|
|
||||||
password = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
|
|
||||||
created_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False, server_default=sqlalchemy.func.now())
|
|
||||||
updated_at = sqlalchemy.Column(
|
|
||||||
sqlalchemy.DateTime,
|
|
||||||
nullable=False,
|
|
||||||
server_default=sqlalchemy.func.now(),
|
|
||||||
onupdate=sqlalchemy.func.now(),
|
|
||||||
)
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import pydantic
|
|
||||||
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
|
|
||||||
class RetrieveResultEntry(pydantic.BaseModel):
|
|
||||||
id: str
|
|
||||||
|
|
||||||
metadata: dict[str, Any]
|
|
||||||
|
|
||||||
distance: float
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
from .. import migration
|
|
||||||
|
|
||||||
import sqlalchemy
|
|
||||||
|
|
||||||
from ...entity.persistence import pipeline as persistence_pipeline
|
|
||||||
|
|
||||||
|
|
||||||
@migration.migration_class(2)
|
|
||||||
class DBMigrateCombineQuoteMsgConfig(migration.DBMigration):
|
|
||||||
"""Combine quote message config"""
|
|
||||||
|
|
||||||
async def upgrade(self):
|
|
||||||
"""Upgrade"""
|
|
||||||
# read all pipelines
|
|
||||||
pipelines = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_pipeline.LegacyPipeline))
|
|
||||||
|
|
||||||
for pipeline in pipelines:
|
|
||||||
serialized_pipeline = self.ap.persistence_mgr.serialize_model(persistence_pipeline.LegacyPipeline, pipeline)
|
|
||||||
|
|
||||||
config = serialized_pipeline['config']
|
|
||||||
|
|
||||||
if 'misc' not in config['trigger']:
|
|
||||||
config['trigger']['misc'] = {}
|
|
||||||
|
|
||||||
if 'combine-quote-message' not in config['trigger']['misc']:
|
|
||||||
config['trigger']['misc']['combine-quote-message'] = False
|
|
||||||
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.update(persistence_pipeline.LegacyPipeline)
|
|
||||||
.where(persistence_pipeline.LegacyPipeline.uuid == serialized_pipeline['uuid'])
|
|
||||||
.values(
|
|
||||||
{
|
|
||||||
'config': config,
|
|
||||||
'for_version': self.ap.ver_mgr.get_current_version(),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
async def downgrade(self):
|
|
||||||
"""Downgrade"""
|
|
||||||
pass
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
from .. import migration
|
|
||||||
|
|
||||||
import sqlalchemy
|
|
||||||
|
|
||||||
from ...entity.persistence import pipeline as persistence_pipeline
|
|
||||||
|
|
||||||
|
|
||||||
@migration.migration_class(3)
|
|
||||||
class DBMigrateN8nConfig(migration.DBMigration):
|
|
||||||
"""N8n config"""
|
|
||||||
|
|
||||||
async def upgrade(self):
|
|
||||||
"""Upgrade"""
|
|
||||||
# read all pipelines
|
|
||||||
pipelines = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_pipeline.LegacyPipeline))
|
|
||||||
|
|
||||||
for pipeline in pipelines:
|
|
||||||
serialized_pipeline = self.ap.persistence_mgr.serialize_model(persistence_pipeline.LegacyPipeline, pipeline)
|
|
||||||
|
|
||||||
config = serialized_pipeline['config']
|
|
||||||
|
|
||||||
if 'n8n-service-api' not in config['ai']:
|
|
||||||
config['ai']['n8n-service-api'] = {
|
|
||||||
'webhook-url': 'http://your-n8n-webhook-url',
|
|
||||||
'auth-type': 'none',
|
|
||||||
'basic-username': '',
|
|
||||||
'basic-password': '',
|
|
||||||
'jwt-secret': '',
|
|
||||||
'jwt-algorithm': 'HS256',
|
|
||||||
'header-name': '',
|
|
||||||
'header-value': '',
|
|
||||||
'timeout': 120,
|
|
||||||
'output-key': 'response',
|
|
||||||
}
|
|
||||||
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.update(persistence_pipeline.LegacyPipeline)
|
|
||||||
.where(persistence_pipeline.LegacyPipeline.uuid == serialized_pipeline['uuid'])
|
|
||||||
.values(
|
|
||||||
{
|
|
||||||
'config': config,
|
|
||||||
'for_version': self.ap.ver_mgr.get_current_version(),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
async def downgrade(self):
|
|
||||||
"""Downgrade"""
|
|
||||||
pass
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
from .. import migration
|
|
||||||
|
|
||||||
import sqlalchemy
|
|
||||||
|
|
||||||
from ...entity.persistence import pipeline as persistence_pipeline
|
|
||||||
|
|
||||||
|
|
||||||
@migration.migration_class(4)
|
|
||||||
class DBMigrateRAGKBUUID(migration.DBMigration):
|
|
||||||
"""RAG知识库UUID"""
|
|
||||||
|
|
||||||
async def upgrade(self):
|
|
||||||
"""升级"""
|
|
||||||
# read all pipelines
|
|
||||||
pipelines = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_pipeline.LegacyPipeline))
|
|
||||||
|
|
||||||
for pipeline in pipelines:
|
|
||||||
serialized_pipeline = self.ap.persistence_mgr.serialize_model(persistence_pipeline.LegacyPipeline, pipeline)
|
|
||||||
|
|
||||||
config = serialized_pipeline['config']
|
|
||||||
|
|
||||||
if 'knowledge-base' not in config['ai']['local-agent']:
|
|
||||||
config['ai']['local-agent']['knowledge-base'] = ''
|
|
||||||
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.update(persistence_pipeline.LegacyPipeline)
|
|
||||||
.where(persistence_pipeline.LegacyPipeline.uuid == serialized_pipeline['uuid'])
|
|
||||||
.values(
|
|
||||||
{
|
|
||||||
'config': config,
|
|
||||||
'for_version': self.ap.ver_mgr.get_current_version(),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
async def downgrade(self):
|
|
||||||
"""降级"""
|
|
||||||
pass
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
from .. import migration
|
|
||||||
|
|
||||||
import sqlalchemy
|
|
||||||
|
|
||||||
from ...entity.persistence import pipeline as persistence_pipeline
|
|
||||||
|
|
||||||
|
|
||||||
@migration.migration_class(5)
|
|
||||||
class DBMigratePipelineRemoveCotConfig(migration.DBMigration):
|
|
||||||
"""Pipeline remove cot config"""
|
|
||||||
|
|
||||||
async def upgrade(self):
|
|
||||||
"""Upgrade"""
|
|
||||||
# read all pipelines
|
|
||||||
pipelines = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_pipeline.LegacyPipeline))
|
|
||||||
|
|
||||||
for pipeline in pipelines:
|
|
||||||
serialized_pipeline = self.ap.persistence_mgr.serialize_model(persistence_pipeline.LegacyPipeline, pipeline)
|
|
||||||
|
|
||||||
config = serialized_pipeline['config']
|
|
||||||
|
|
||||||
if 'remove-think' not in config['output']['misc']:
|
|
||||||
config['output']['misc']['remove-think'] = False
|
|
||||||
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.update(persistence_pipeline.LegacyPipeline)
|
|
||||||
.where(persistence_pipeline.LegacyPipeline.uuid == serialized_pipeline['uuid'])
|
|
||||||
.values(
|
|
||||||
{
|
|
||||||
'config': config,
|
|
||||||
'for_version': self.ap.ver_mgr.get_current_version(),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
async def downgrade(self):
|
|
||||||
"""Downgrade"""
|
|
||||||
pass
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
from .. import migration
|
|
||||||
|
|
||||||
import sqlalchemy
|
|
||||||
|
|
||||||
from ...entity.persistence import pipeline as persistence_pipeline
|
|
||||||
|
|
||||||
|
|
||||||
@migration.migration_class(6)
|
|
||||||
class DBMigrateLangflowApiConfig(migration.DBMigration):
|
|
||||||
"""Langflow API config"""
|
|
||||||
|
|
||||||
async def upgrade(self):
|
|
||||||
"""Upgrade"""
|
|
||||||
# read all pipelines
|
|
||||||
pipelines = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_pipeline.LegacyPipeline))
|
|
||||||
|
|
||||||
for pipeline in pipelines:
|
|
||||||
serialized_pipeline = self.ap.persistence_mgr.serialize_model(persistence_pipeline.LegacyPipeline, pipeline)
|
|
||||||
|
|
||||||
config = serialized_pipeline['config']
|
|
||||||
|
|
||||||
if 'langflow-api' not in config['ai']:
|
|
||||||
config['ai']['langflow-api'] = {
|
|
||||||
'base-url': 'http://localhost:7860',
|
|
||||||
'api-key': 'your-api-key',
|
|
||||||
'flow-id': 'your-flow-id',
|
|
||||||
'input-type': 'chat',
|
|
||||||
'output-type': 'chat',
|
|
||||||
'tweaks': '{}',
|
|
||||||
}
|
|
||||||
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.update(persistence_pipeline.LegacyPipeline)
|
|
||||||
.where(persistence_pipeline.LegacyPipeline.uuid == serialized_pipeline['uuid'])
|
|
||||||
.values(
|
|
||||||
{
|
|
||||||
'config': config,
|
|
||||||
'for_version': self.ap.ver_mgr.get_current_version(),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
async def downgrade(self):
|
|
||||||
"""Downgrade"""
|
|
||||||
pass
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import aiohttp
|
|
||||||
|
|
||||||
from .. import entities
|
|
||||||
from .. import filter as filter_model
|
|
||||||
import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query
|
|
||||||
|
|
||||||
BAIDU_EXAMINE_URL = 'https://aip.baidubce.com/rest/2.0/solution/v1/text_censor/v2/user_defined?access_token={}'
|
|
||||||
BAIDU_EXAMINE_TOKEN_URL = 'https://aip.baidubce.com/oauth/2.0/token'
|
|
||||||
|
|
||||||
|
|
||||||
@filter_model.filter_class('baidu-cloud-examine')
|
|
||||||
class BaiduCloudExamine(filter_model.ContentFilter):
|
|
||||||
"""百度云内容审核"""
|
|
||||||
|
|
||||||
async def _get_token(self) -> str:
|
|
||||||
async with aiohttp.ClientSession() as session:
|
|
||||||
async with session.post(
|
|
||||||
BAIDU_EXAMINE_TOKEN_URL,
|
|
||||||
params={
|
|
||||||
'grant_type': 'client_credentials',
|
|
||||||
'client_id': self.ap.pipeline_cfg.data['baidu-cloud-examine']['api-key'],
|
|
||||||
'client_secret': self.ap.pipeline_cfg.data['baidu-cloud-examine']['api-secret'],
|
|
||||||
},
|
|
||||||
) as resp:
|
|
||||||
return (await resp.json())['access_token']
|
|
||||||
|
|
||||||
async def process(self, query: pipeline_query.Query, message: str) -> entities.FilterResult:
|
|
||||||
async with aiohttp.ClientSession() as session:
|
|
||||||
async with session.post(
|
|
||||||
BAIDU_EXAMINE_URL.format(await self._get_token()),
|
|
||||||
headers={
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
|
||||||
'Accept': 'application/json',
|
|
||||||
},
|
|
||||||
data=f'text={message}'.encode('utf-8'),
|
|
||||||
) as resp:
|
|
||||||
result = await resp.json()
|
|
||||||
|
|
||||||
if 'error_code' in result:
|
|
||||||
return entities.FilterResult(
|
|
||||||
level=entities.ResultLevel.BLOCK,
|
|
||||||
replacement=message,
|
|
||||||
user_notice='',
|
|
||||||
console_notice=f'百度云判定出错,错误信息:{result["error_msg"]}',
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
conclusion = result['conclusion']
|
|
||||||
|
|
||||||
if conclusion in ('合规'):
|
|
||||||
return entities.FilterResult(
|
|
||||||
level=entities.ResultLevel.PASS,
|
|
||||||
replacement=message,
|
|
||||||
user_notice='',
|
|
||||||
console_notice=f'百度云判定结果:{conclusion}',
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return entities.FilterResult(
|
|
||||||
level=entities.ResultLevel.BLOCK,
|
|
||||||
replacement=message,
|
|
||||||
user_notice='消息中存在不合适的内容, 请修改',
|
|
||||||
console_notice=f'百度云判定结果:{conclusion}',
|
|
||||||
)
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import datetime
|
|
||||||
|
|
||||||
from .. import stage, entities
|
|
||||||
from langbot_plugin.api.entities.builtin.provider import message as provider_message
|
|
||||||
import langbot_plugin.api.entities.events as events
|
|
||||||
import langbot_plugin.api.entities.builtin.platform.message as platform_message
|
|
||||||
import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query
|
|
||||||
|
|
||||||
|
|
||||||
@stage.stage_class('PreProcessor')
|
|
||||||
class PreProcessor(stage.PipelineStage):
|
|
||||||
"""Request pre-processing stage
|
|
||||||
|
|
||||||
Check out session, prompt, context, model, and content functions.
|
|
||||||
|
|
||||||
Rewrite:
|
|
||||||
- session
|
|
||||||
- prompt
|
|
||||||
- messages
|
|
||||||
- user_message
|
|
||||||
- use_model
|
|
||||||
- use_funcs
|
|
||||||
"""
|
|
||||||
|
|
||||||
async def process(
|
|
||||||
self,
|
|
||||||
query: pipeline_query.Query,
|
|
||||||
stage_inst_name: str,
|
|
||||||
) -> entities.StageProcessResult:
|
|
||||||
"""Process"""
|
|
||||||
selected_runner = query.pipeline_config['ai']['runner']['runner']
|
|
||||||
|
|
||||||
session = await self.ap.sess_mgr.get_session(query)
|
|
||||||
|
|
||||||
# When not local-agent, llm_model is None
|
|
||||||
llm_model = (
|
|
||||||
await self.ap.model_mgr.get_model_by_uuid(query.pipeline_config['ai']['local-agent']['model'])
|
|
||||||
if selected_runner == 'local-agent'
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
|
|
||||||
conversation = await self.ap.sess_mgr.get_conversation(
|
|
||||||
query,
|
|
||||||
session,
|
|
||||||
query.pipeline_config['ai']['local-agent']['prompt'],
|
|
||||||
query.pipeline_uuid,
|
|
||||||
query.bot_uuid,
|
|
||||||
)
|
|
||||||
|
|
||||||
# 设置query
|
|
||||||
query.session = session
|
|
||||||
query.prompt = conversation.prompt.copy()
|
|
||||||
query.messages = conversation.messages.copy()
|
|
||||||
|
|
||||||
if selected_runner == 'local-agent':
|
|
||||||
query.use_funcs = []
|
|
||||||
query.use_llm_model_uuid = llm_model.model_entity.uuid
|
|
||||||
|
|
||||||
if llm_model.model_entity.abilities.__contains__('func_call'):
|
|
||||||
query.use_funcs = await self.ap.tool_mgr.get_all_tools()
|
|
||||||
|
|
||||||
variables = {
|
|
||||||
'session_id': f'{query.session.launcher_type.value}_{query.session.launcher_id}',
|
|
||||||
'conversation_id': conversation.uuid,
|
|
||||||
'msg_create_time': (
|
|
||||||
int(query.message_event.time) if query.message_event.time else int(datetime.datetime.now().timestamp())
|
|
||||||
),
|
|
||||||
}
|
|
||||||
query.variables.update(variables)
|
|
||||||
|
|
||||||
# Check if this model supports vision, if not, remove all images
|
|
||||||
# TODO this checking should be performed in runner, and in this stage, the image should be reserved
|
|
||||||
if selected_runner == 'local-agent' and not llm_model.model_entity.abilities.__contains__('vision'):
|
|
||||||
for msg in query.messages:
|
|
||||||
if isinstance(msg.content, list):
|
|
||||||
for me in msg.content:
|
|
||||||
if me.type == 'image_url':
|
|
||||||
msg.content.remove(me)
|
|
||||||
|
|
||||||
content_list: list[provider_message.ContentElement] = []
|
|
||||||
|
|
||||||
plain_text = ''
|
|
||||||
qoute_msg = query.pipeline_config['trigger'].get('misc', '').get('combine-quote-message')
|
|
||||||
|
|
||||||
for me in query.message_chain:
|
|
||||||
if isinstance(me, platform_message.Plain):
|
|
||||||
content_list.append(provider_message.ContentElement.from_text(me.text))
|
|
||||||
plain_text += me.text
|
|
||||||
elif isinstance(me, platform_message.Image):
|
|
||||||
if selected_runner != 'local-agent' or llm_model.model_entity.abilities.__contains__('vision'):
|
|
||||||
if me.base64 is not None:
|
|
||||||
content_list.append(provider_message.ContentElement.from_image_base64(me.base64))
|
|
||||||
elif isinstance(me, platform_message.File):
|
|
||||||
# if me.url is not None:
|
|
||||||
content_list.append(provider_message.ContentElement.from_file_url(me.url, me.name))
|
|
||||||
elif isinstance(me, platform_message.Quote) and qoute_msg:
|
|
||||||
for msg in me.origin:
|
|
||||||
if isinstance(msg, platform_message.Plain):
|
|
||||||
content_list.append(provider_message.ContentElement.from_text(msg.text))
|
|
||||||
elif isinstance(msg, platform_message.Image):
|
|
||||||
if selected_runner != 'local-agent' or llm_model.model_entity.abilities.__contains__('vision'):
|
|
||||||
if msg.base64 is not None:
|
|
||||||
content_list.append(provider_message.ContentElement.from_image_base64(msg.base64))
|
|
||||||
|
|
||||||
query.variables['user_message_text'] = plain_text
|
|
||||||
|
|
||||||
query.user_message = provider_message.Message(role='user', content=content_list)
|
|
||||||
# =========== 触发事件 PromptPreProcessing
|
|
||||||
|
|
||||||
event = events.PromptPreProcessing(
|
|
||||||
session_name=f'{query.session.launcher_type.value}_{query.session.launcher_id}',
|
|
||||||
default_prompt=query.prompt.messages,
|
|
||||||
prompt=query.messages,
|
|
||||||
query=query,
|
|
||||||
)
|
|
||||||
|
|
||||||
event_ctx = await self.ap.plugin_connector.emit_event(event)
|
|
||||||
|
|
||||||
query.prompt.messages = event_ctx.event.default_prompt
|
|
||||||
query.messages = event_ctx.event.prompt
|
|
||||||
|
|
||||||
return entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query)
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import uuid
|
|
||||||
import typing
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
|
|
||||||
from .. import handler
|
|
||||||
from ... import entities
|
|
||||||
from ....provider import runner as runner_module
|
|
||||||
|
|
||||||
import langbot_plugin.api.entities.events as events
|
|
||||||
from ....utils import importutil
|
|
||||||
from ....provider import runners
|
|
||||||
import langbot_plugin.api.entities.builtin.provider.session as provider_session
|
|
||||||
import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query
|
|
||||||
|
|
||||||
|
|
||||||
importutil.import_modules_in_pkg(runners)
|
|
||||||
|
|
||||||
|
|
||||||
class ChatMessageHandler(handler.MessageHandler):
|
|
||||||
async def handle(
|
|
||||||
self,
|
|
||||||
query: pipeline_query.Query,
|
|
||||||
) -> typing.AsyncGenerator[entities.StageProcessResult, None]:
|
|
||||||
"""处理"""
|
|
||||||
# 调API
|
|
||||||
# 生成器
|
|
||||||
|
|
||||||
# 触发插件事件
|
|
||||||
event_class = (
|
|
||||||
events.PersonNormalMessageReceived
|
|
||||||
if query.launcher_type == provider_session.LauncherTypes.PERSON
|
|
||||||
else events.GroupNormalMessageReceived
|
|
||||||
)
|
|
||||||
|
|
||||||
event = event_class(
|
|
||||||
launcher_type=query.launcher_type.value,
|
|
||||||
launcher_id=query.launcher_id,
|
|
||||||
sender_id=query.sender_id,
|
|
||||||
text_message=str(query.message_chain),
|
|
||||||
query=query,
|
|
||||||
)
|
|
||||||
|
|
||||||
event_ctx = await self.ap.plugin_connector.emit_event(event)
|
|
||||||
|
|
||||||
is_create_card = False # 判断下是否需要创建流式卡片
|
|
||||||
|
|
||||||
if event_ctx.is_prevented_default():
|
|
||||||
if event_ctx.event.reply_message_chain is not None:
|
|
||||||
mc = event_ctx.event.reply_message_chain
|
|
||||||
query.resp_messages.append(mc)
|
|
||||||
|
|
||||||
yield entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query)
|
|
||||||
else:
|
|
||||||
yield entities.StageProcessResult(result_type=entities.ResultType.INTERRUPT, new_query=query)
|
|
||||||
else:
|
|
||||||
if event_ctx.event.user_message_alter is not None:
|
|
||||||
# if isinstance(event_ctx.event, str): # 现在暂时不考虑多模态alter
|
|
||||||
query.user_message.content = event_ctx.event.user_message_alter
|
|
||||||
|
|
||||||
text_length = 0
|
|
||||||
try:
|
|
||||||
is_stream = await query.adapter.is_stream_output_supported()
|
|
||||||
except AttributeError:
|
|
||||||
is_stream = False
|
|
||||||
|
|
||||||
try:
|
|
||||||
for r in runner_module.preregistered_runners:
|
|
||||||
if r.name == query.pipeline_config['ai']['runner']['runner']:
|
|
||||||
runner = r(self.ap, query.pipeline_config)
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
raise ValueError(f'未找到请求运行器: {query.pipeline_config["ai"]["runner"]["runner"]}')
|
|
||||||
if is_stream:
|
|
||||||
resp_message_id = uuid.uuid4()
|
|
||||||
|
|
||||||
async for result in runner.run(query):
|
|
||||||
result.resp_message_id = str(resp_message_id)
|
|
||||||
if query.resp_messages:
|
|
||||||
query.resp_messages.pop()
|
|
||||||
if query.resp_message_chain:
|
|
||||||
query.resp_message_chain.pop()
|
|
||||||
# 此时连接外部 AI 服务正常,创建卡片
|
|
||||||
if not is_create_card: # 只有不是第一次才创建卡片
|
|
||||||
await query.adapter.create_message_card(str(resp_message_id), query.message_event)
|
|
||||||
is_create_card = True
|
|
||||||
query.resp_messages.append(result)
|
|
||||||
self.ap.logger.info(f'对话({query.query_id})流式响应: {self.cut_str(result.readable_str())}')
|
|
||||||
|
|
||||||
if result.content is not None:
|
|
||||||
text_length += len(result.content)
|
|
||||||
|
|
||||||
yield entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query)
|
|
||||||
|
|
||||||
else:
|
|
||||||
async for result in runner.run(query):
|
|
||||||
query.resp_messages.append(result)
|
|
||||||
|
|
||||||
self.ap.logger.info(f'对话({query.query_id})响应: {self.cut_str(result.readable_str())}')
|
|
||||||
|
|
||||||
if result.content is not None:
|
|
||||||
text_length += len(result.content)
|
|
||||||
|
|
||||||
yield entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query)
|
|
||||||
|
|
||||||
query.session.using_conversation.messages.append(query.user_message)
|
|
||||||
|
|
||||||
query.session.using_conversation.messages.extend(query.resp_messages)
|
|
||||||
except Exception as e:
|
|
||||||
self.ap.logger.error(f'对话({query.query_id})请求失败: {type(e).__name__} {str(e)}')
|
|
||||||
traceback.print_exc()
|
|
||||||
|
|
||||||
hide_exception_info = query.pipeline_config['output']['misc']['hide-exception']
|
|
||||||
|
|
||||||
yield entities.StageProcessResult(
|
|
||||||
result_type=entities.ResultType.INTERRUPT,
|
|
||||||
new_query=query,
|
|
||||||
user_notice='请求失败' if hide_exception_info else f'{e}',
|
|
||||||
error_notice=f'{e}',
|
|
||||||
debug_notice=traceback.format_exc(),
|
|
||||||
)
|
|
||||||
finally:
|
|
||||||
# TODO statistics
|
|
||||||
pass
|
|
||||||
@@ -1,286 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import traceback
|
|
||||||
import sqlalchemy
|
|
||||||
|
|
||||||
from ..core import app, entities as core_entities, taskmgr
|
|
||||||
|
|
||||||
from ..discover import engine
|
|
||||||
|
|
||||||
from ..entity.persistence import bot as persistence_bot
|
|
||||||
|
|
||||||
from ..entity.errors import platform as platform_errors
|
|
||||||
|
|
||||||
from .logger import EventLogger
|
|
||||||
|
|
||||||
import langbot_plugin.api.entities.builtin.provider.session as provider_session
|
|
||||||
import langbot_plugin.api.entities.builtin.platform.events as platform_events
|
|
||||||
import langbot_plugin.api.entities.builtin.platform.message as platform_message
|
|
||||||
import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter
|
|
||||||
|
|
||||||
|
|
||||||
class RuntimeBot:
|
|
||||||
"""运行时机器人"""
|
|
||||||
|
|
||||||
ap: app.Application
|
|
||||||
|
|
||||||
bot_entity: persistence_bot.Bot
|
|
||||||
|
|
||||||
enable: bool
|
|
||||||
|
|
||||||
adapter: abstract_platform_adapter.AbstractMessagePlatformAdapter
|
|
||||||
|
|
||||||
task_wrapper: taskmgr.TaskWrapper
|
|
||||||
|
|
||||||
task_context: taskmgr.TaskContext
|
|
||||||
|
|
||||||
logger: EventLogger
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
ap: app.Application,
|
|
||||||
bot_entity: persistence_bot.Bot,
|
|
||||||
adapter: abstract_platform_adapter.AbstractMessagePlatformAdapter,
|
|
||||||
logger: EventLogger,
|
|
||||||
):
|
|
||||||
self.ap = ap
|
|
||||||
self.bot_entity = bot_entity
|
|
||||||
self.enable = bot_entity.enable
|
|
||||||
self.adapter = adapter
|
|
||||||
self.task_context = taskmgr.TaskContext()
|
|
||||||
self.logger = logger
|
|
||||||
|
|
||||||
async def initialize(self):
|
|
||||||
async def on_friend_message(
|
|
||||||
event: platform_events.FriendMessage,
|
|
||||||
adapter: abstract_platform_adapter.AbstractMessagePlatformAdapter,
|
|
||||||
):
|
|
||||||
image_components = [
|
|
||||||
component for component in event.message_chain if isinstance(component, platform_message.Image)
|
|
||||||
]
|
|
||||||
|
|
||||||
await self.logger.info(
|
|
||||||
f'{event.message_chain}',
|
|
||||||
images=image_components,
|
|
||||||
message_session_id=f'person_{event.sender.id}',
|
|
||||||
)
|
|
||||||
|
|
||||||
await self.ap.query_pool.add_query(
|
|
||||||
bot_uuid=self.bot_entity.uuid,
|
|
||||||
launcher_type=provider_session.LauncherTypes.PERSON,
|
|
||||||
launcher_id=event.sender.id,
|
|
||||||
sender_id=event.sender.id,
|
|
||||||
message_event=event,
|
|
||||||
message_chain=event.message_chain,
|
|
||||||
adapter=adapter,
|
|
||||||
pipeline_uuid=self.bot_entity.use_pipeline_uuid,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def on_group_message(
|
|
||||||
event: platform_events.GroupMessage,
|
|
||||||
adapter: abstract_platform_adapter.AbstractMessagePlatformAdapter,
|
|
||||||
):
|
|
||||||
image_components = [
|
|
||||||
component for component in event.message_chain if isinstance(component, platform_message.Image)
|
|
||||||
]
|
|
||||||
|
|
||||||
await self.logger.info(
|
|
||||||
f'{event.message_chain}',
|
|
||||||
images=image_components,
|
|
||||||
message_session_id=f'group_{event.group.id}',
|
|
||||||
)
|
|
||||||
|
|
||||||
await self.ap.query_pool.add_query(
|
|
||||||
bot_uuid=self.bot_entity.uuid,
|
|
||||||
launcher_type=provider_session.LauncherTypes.GROUP,
|
|
||||||
launcher_id=event.group.id,
|
|
||||||
sender_id=event.sender.id,
|
|
||||||
message_event=event,
|
|
||||||
message_chain=event.message_chain,
|
|
||||||
adapter=adapter,
|
|
||||||
pipeline_uuid=self.bot_entity.use_pipeline_uuid,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.adapter.register_listener(platform_events.FriendMessage, on_friend_message)
|
|
||||||
self.adapter.register_listener(platform_events.GroupMessage, on_group_message)
|
|
||||||
|
|
||||||
async def run(self):
|
|
||||||
async def exception_wrapper():
|
|
||||||
try:
|
|
||||||
self.task_context.set_current_action('Running...')
|
|
||||||
await self.adapter.run_async()
|
|
||||||
self.task_context.set_current_action('Exited.')
|
|
||||||
except Exception as e:
|
|
||||||
if isinstance(e, asyncio.CancelledError):
|
|
||||||
self.task_context.set_current_action('Exited.')
|
|
||||||
return
|
|
||||||
|
|
||||||
traceback_str = traceback.format_exc()
|
|
||||||
self.task_context.set_current_action('Exited with error.')
|
|
||||||
await self.logger.error(f'平台适配器运行出错:\n{e}\n{traceback_str}')
|
|
||||||
|
|
||||||
self.task_wrapper = self.ap.task_mgr.create_task(
|
|
||||||
exception_wrapper(),
|
|
||||||
kind='platform-adapter',
|
|
||||||
name=f'platform-adapter-{self.adapter.__class__.__name__}',
|
|
||||||
context=self.task_context,
|
|
||||||
scopes=[
|
|
||||||
core_entities.LifecycleControlScope.APPLICATION,
|
|
||||||
core_entities.LifecycleControlScope.PLATFORM,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
async def shutdown(self):
|
|
||||||
await self.adapter.kill()
|
|
||||||
|
|
||||||
self.ap.task_mgr.cancel_task(self.task_wrapper.id)
|
|
||||||
|
|
||||||
|
|
||||||
# 控制QQ消息输入输出的类
|
|
||||||
class PlatformManager:
|
|
||||||
# ====== 4.0 ======
|
|
||||||
ap: app.Application = None
|
|
||||||
|
|
||||||
bots: list[RuntimeBot]
|
|
||||||
|
|
||||||
webchat_proxy_bot: RuntimeBot
|
|
||||||
|
|
||||||
adapter_components: list[engine.Component]
|
|
||||||
|
|
||||||
adapter_dict: dict[str, type[abstract_platform_adapter.AbstractMessagePlatformAdapter]]
|
|
||||||
|
|
||||||
def __init__(self, ap: app.Application = None):
|
|
||||||
self.ap = ap
|
|
||||||
self.bots = []
|
|
||||||
self.adapter_components = []
|
|
||||||
self.adapter_dict = {}
|
|
||||||
|
|
||||||
async def initialize(self):
|
|
||||||
self.adapter_components = self.ap.discover.get_components_by_kind('MessagePlatformAdapter')
|
|
||||||
adapter_dict: dict[str, type[abstract_platform_adapter.AbstractMessagePlatformAdapter]] = {}
|
|
||||||
for component in self.adapter_components:
|
|
||||||
adapter_dict[component.metadata.name] = component.get_python_component_class()
|
|
||||||
self.adapter_dict = adapter_dict
|
|
||||||
|
|
||||||
webchat_adapter_class = self.adapter_dict['webchat']
|
|
||||||
|
|
||||||
# initialize webchat adapter
|
|
||||||
webchat_logger = EventLogger(name='webchat-adapter', ap=self.ap)
|
|
||||||
webchat_adapter_inst = webchat_adapter_class(
|
|
||||||
{},
|
|
||||||
webchat_logger,
|
|
||||||
ap=self.ap,
|
|
||||||
is_stream=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.webchat_proxy_bot = RuntimeBot(
|
|
||||||
ap=self.ap,
|
|
||||||
bot_entity=persistence_bot.Bot(
|
|
||||||
uuid='webchat-proxy-bot',
|
|
||||||
name='WebChat',
|
|
||||||
description='',
|
|
||||||
adapter='webchat',
|
|
||||||
adapter_config={},
|
|
||||||
enable=True,
|
|
||||||
),
|
|
||||||
adapter=webchat_adapter_inst,
|
|
||||||
logger=webchat_logger,
|
|
||||||
)
|
|
||||||
await self.webchat_proxy_bot.initialize()
|
|
||||||
|
|
||||||
await self.load_bots_from_db()
|
|
||||||
|
|
||||||
def get_running_adapters(self) -> list[abstract_platform_adapter.AbstractMessagePlatformAdapter]:
|
|
||||||
return [bot.adapter for bot in self.bots if bot.enable]
|
|
||||||
|
|
||||||
async def load_bots_from_db(self):
|
|
||||||
self.ap.logger.info('Loading bots from db...')
|
|
||||||
|
|
||||||
self.bots = []
|
|
||||||
|
|
||||||
result = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_bot.Bot))
|
|
||||||
|
|
||||||
bots = result.all()
|
|
||||||
|
|
||||||
for bot in bots:
|
|
||||||
# load all bots here, enable or disable will be handled in runtime
|
|
||||||
try:
|
|
||||||
await self.load_bot(bot)
|
|
||||||
except platform_errors.AdapterNotFoundError as e:
|
|
||||||
self.ap.logger.warning(f'Adapter {e.adapter_name} not found, skipping bot {bot.uuid}')
|
|
||||||
except Exception as e:
|
|
||||||
self.ap.logger.error(f'Failed to load bot {bot.uuid}: {e}\n{traceback.format_exc()}')
|
|
||||||
|
|
||||||
async def load_bot(
|
|
||||||
self,
|
|
||||||
bot_entity: persistence_bot.Bot | sqlalchemy.Row[persistence_bot.Bot] | dict,
|
|
||||||
) -> RuntimeBot:
|
|
||||||
"""加载机器人"""
|
|
||||||
if isinstance(bot_entity, sqlalchemy.Row):
|
|
||||||
bot_entity = persistence_bot.Bot(**bot_entity._mapping)
|
|
||||||
elif isinstance(bot_entity, dict):
|
|
||||||
bot_entity = persistence_bot.Bot(**bot_entity)
|
|
||||||
|
|
||||||
logger = EventLogger(name=f'platform-adapter-{bot_entity.name}', ap=self.ap)
|
|
||||||
|
|
||||||
if bot_entity.adapter not in self.adapter_dict:
|
|
||||||
raise platform_errors.AdapterNotFoundError(bot_entity.adapter)
|
|
||||||
|
|
||||||
adapter_inst = self.adapter_dict[bot_entity.adapter](
|
|
||||||
bot_entity.adapter_config,
|
|
||||||
logger,
|
|
||||||
)
|
|
||||||
|
|
||||||
runtime_bot = RuntimeBot(ap=self.ap, bot_entity=bot_entity, adapter=adapter_inst, logger=logger)
|
|
||||||
|
|
||||||
await runtime_bot.initialize()
|
|
||||||
|
|
||||||
self.bots.append(runtime_bot)
|
|
||||||
|
|
||||||
return runtime_bot
|
|
||||||
|
|
||||||
async def get_bot_by_uuid(self, bot_uuid: str) -> RuntimeBot | None:
|
|
||||||
for bot in self.bots:
|
|
||||||
if bot.bot_entity.uuid == bot_uuid:
|
|
||||||
return bot
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def remove_bot(self, bot_uuid: str):
|
|
||||||
for bot in self.bots:
|
|
||||||
if bot.bot_entity.uuid == bot_uuid:
|
|
||||||
if bot.enable:
|
|
||||||
await bot.shutdown()
|
|
||||||
self.bots.remove(bot)
|
|
||||||
return
|
|
||||||
|
|
||||||
def get_available_adapters_info(self) -> list[dict]:
|
|
||||||
return [
|
|
||||||
component.to_plain_dict() for component in self.adapter_components if component.metadata.name != 'webchat'
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_available_adapter_info_by_name(self, name: str) -> dict | None:
|
|
||||||
for component in self.adapter_components:
|
|
||||||
if component.metadata.name == name:
|
|
||||||
return component.to_plain_dict()
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_available_adapter_manifest_by_name(self, name: str) -> engine.Component | None:
|
|
||||||
for component in self.adapter_components:
|
|
||||||
if component.metadata.name == name:
|
|
||||||
return component
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def run(self):
|
|
||||||
# This method will only be called when the application launching
|
|
||||||
await self.webchat_proxy_bot.run()
|
|
||||||
|
|
||||||
for bot in self.bots:
|
|
||||||
if bot.enable:
|
|
||||||
await bot.run()
|
|
||||||
|
|
||||||
async def shutdown(self):
|
|
||||||
for bot in self.bots:
|
|
||||||
if bot.enable:
|
|
||||||
await bot.shutdown()
|
|
||||||
self.ap.task_mgr.cancel_by_scope(core_entities.LifecycleControlScope.PLATFORM)
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: MessagePlatformAdapter
|
|
||||||
metadata:
|
|
||||||
name: dingtalk
|
|
||||||
label:
|
|
||||||
en_US: DingTalk
|
|
||||||
zh_Hans: 钉钉
|
|
||||||
description:
|
|
||||||
en_US: DingTalk Adapter
|
|
||||||
zh_Hans: 钉钉适配器,请查看文档了解使用方式
|
|
||||||
icon: dingtalk.svg
|
|
||||||
spec:
|
|
||||||
config:
|
|
||||||
- name: client_id
|
|
||||||
label:
|
|
||||||
en_US: Client ID
|
|
||||||
zh_Hans: 客户端ID
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: ""
|
|
||||||
- name: client_secret
|
|
||||||
label:
|
|
||||||
en_US: Client Secret
|
|
||||||
zh_Hans: 客户端密钥
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: ""
|
|
||||||
- name: robot_code
|
|
||||||
label:
|
|
||||||
en_US: Robot Code
|
|
||||||
zh_Hans: 机器人代码
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: ""
|
|
||||||
- name: robot_name
|
|
||||||
label:
|
|
||||||
en_US: Robot Name
|
|
||||||
zh_Hans: 机器人名称
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: ""
|
|
||||||
- name: markdown_card
|
|
||||||
label:
|
|
||||||
en_US: Markdown Card
|
|
||||||
zh_Hans: 是否使用 Markdown 卡片
|
|
||||||
type: boolean
|
|
||||||
required: false
|
|
||||||
default: true
|
|
||||||
- name: enable-stream-reply
|
|
||||||
label:
|
|
||||||
en_US: Enable Stream Reply Mode
|
|
||||||
zh_Hans: 启用钉钉卡片流式回复模式
|
|
||||||
description:
|
|
||||||
en_US: If enabled, the bot will use the stream of lark reply mode
|
|
||||||
zh_Hans: 如果启用,将使用钉钉卡片流式方式来回复内容
|
|
||||||
type: boolean
|
|
||||||
required: true
|
|
||||||
default: false
|
|
||||||
- name: card_template_id
|
|
||||||
label:
|
|
||||||
en_US: card template id
|
|
||||||
zh_Hans: 卡片模板ID
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: "填写你的卡片template_id"
|
|
||||||
execution:
|
|
||||||
python:
|
|
||||||
path: ./dingtalk.py
|
|
||||||
attr: DingTalkAdapter
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: MessagePlatformAdapter
|
|
||||||
metadata:
|
|
||||||
name: discord
|
|
||||||
label:
|
|
||||||
en_US: Discord
|
|
||||||
zh_Hans: Discord
|
|
||||||
description:
|
|
||||||
en_US: Discord Adapter
|
|
||||||
zh_Hans: Discord 适配器,请查看文档了解使用方式
|
|
||||||
icon: discord.svg
|
|
||||||
spec:
|
|
||||||
config:
|
|
||||||
- name: client_id
|
|
||||||
label:
|
|
||||||
en_US: Client ID
|
|
||||||
zh_Hans: 客户端ID
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: ""
|
|
||||||
- name: token
|
|
||||||
label:
|
|
||||||
en_US: Token
|
|
||||||
zh_Hans: 令牌
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: ""
|
|
||||||
execution:
|
|
||||||
python:
|
|
||||||
path: ./discord.py
|
|
||||||
attr: DiscordAdapter
|
|
||||||
@@ -1,809 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import lark_oapi
|
|
||||||
from lark_oapi.api.im.v1 import CreateImageRequest, CreateImageRequestBody
|
|
||||||
import traceback
|
|
||||||
import typing
|
|
||||||
import asyncio
|
|
||||||
import re
|
|
||||||
import base64
|
|
||||||
import uuid
|
|
||||||
import json
|
|
||||||
import datetime
|
|
||||||
import hashlib
|
|
||||||
from Crypto.Cipher import AES
|
|
||||||
|
|
||||||
import aiohttp
|
|
||||||
import lark_oapi.ws.exception
|
|
||||||
import quart
|
|
||||||
from lark_oapi.api.im.v1 import *
|
|
||||||
import pydantic
|
|
||||||
from lark_oapi.api.cardkit.v1 import *
|
|
||||||
|
|
||||||
import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter
|
|
||||||
import langbot_plugin.api.entities.builtin.platform.message as platform_message
|
|
||||||
import langbot_plugin.api.entities.builtin.platform.events as platform_events
|
|
||||||
import langbot_plugin.api.entities.builtin.platform.entities as platform_entities
|
|
||||||
import langbot_plugin.api.definition.abstract.platform.event_logger as abstract_platform_logger
|
|
||||||
|
|
||||||
|
|
||||||
class AESCipher(object):
|
|
||||||
def __init__(self, key):
|
|
||||||
self.bs = AES.block_size
|
|
||||||
self.key = hashlib.sha256(AESCipher.str_to_bytes(key)).digest()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def str_to_bytes(data):
|
|
||||||
u_type = type(b''.decode('utf8'))
|
|
||||||
if isinstance(data, u_type):
|
|
||||||
return data.encode('utf8')
|
|
||||||
return data
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _unpad(s):
|
|
||||||
return s[: -ord(s[len(s) - 1 :])]
|
|
||||||
|
|
||||||
def decrypt(self, enc):
|
|
||||||
iv = enc[: AES.block_size]
|
|
||||||
cipher = AES.new(self.key, AES.MODE_CBC, iv)
|
|
||||||
return self._unpad(cipher.decrypt(enc[AES.block_size :]))
|
|
||||||
|
|
||||||
def decrypt_string(self, enc):
|
|
||||||
enc = base64.b64decode(enc)
|
|
||||||
return self.decrypt(enc).decode('utf8')
|
|
||||||
|
|
||||||
|
|
||||||
class LarkMessageConverter(abstract_platform_adapter.AbstractMessageConverter):
|
|
||||||
@staticmethod
|
|
||||||
async def yiri2target(
|
|
||||||
message_chain: platform_message.MessageChain, api_client: lark_oapi.Client
|
|
||||||
) -> typing.Tuple[list]:
|
|
||||||
message_elements = []
|
|
||||||
pending_paragraph = []
|
|
||||||
for msg in message_chain:
|
|
||||||
if isinstance(msg, platform_message.Plain):
|
|
||||||
# Ensure text is valid UTF-8
|
|
||||||
try:
|
|
||||||
text = msg.text.encode('utf-8').decode('utf-8')
|
|
||||||
pending_paragraph.append({'tag': 'md', 'text': text})
|
|
||||||
except UnicodeError:
|
|
||||||
# If text is not valid UTF-8, try to decode with other encodings
|
|
||||||
try:
|
|
||||||
text = msg.text.encode('latin1').decode('utf-8')
|
|
||||||
pending_paragraph.append({'tag': 'md', 'text': text})
|
|
||||||
except UnicodeError:
|
|
||||||
# If still fails, replace invalid characters
|
|
||||||
text = msg.text.encode('utf-8', errors='replace').decode('utf-8')
|
|
||||||
pending_paragraph.append({'tag': 'md', 'text': text})
|
|
||||||
elif isinstance(msg, platform_message.At):
|
|
||||||
pending_paragraph.append({'tag': 'at', 'user_id': msg.target, 'style': []})
|
|
||||||
elif isinstance(msg, platform_message.AtAll):
|
|
||||||
pending_paragraph.append({'tag': 'at', 'user_id': 'all', 'style': []})
|
|
||||||
elif isinstance(msg, platform_message.Image):
|
|
||||||
image_bytes = None
|
|
||||||
|
|
||||||
if msg.base64:
|
|
||||||
try:
|
|
||||||
# Remove data URL prefix if present
|
|
||||||
if msg.base64.startswith('data:'):
|
|
||||||
msg.base64 = msg.base64.split(',', 1)[1]
|
|
||||||
image_bytes = base64.b64decode(msg.base64)
|
|
||||||
except Exception:
|
|
||||||
traceback.print_exc()
|
|
||||||
continue
|
|
||||||
elif msg.url:
|
|
||||||
try:
|
|
||||||
async with aiohttp.ClientSession() as session:
|
|
||||||
async with session.get(msg.url) as response:
|
|
||||||
if response.status == 200:
|
|
||||||
image_bytes = await response.read()
|
|
||||||
else:
|
|
||||||
traceback.print_exc()
|
|
||||||
continue
|
|
||||||
except Exception:
|
|
||||||
traceback.print_exc()
|
|
||||||
continue
|
|
||||||
elif msg.path:
|
|
||||||
try:
|
|
||||||
with open(msg.path, 'rb') as f:
|
|
||||||
image_bytes = f.read()
|
|
||||||
except Exception:
|
|
||||||
traceback.print_exc()
|
|
||||||
continue
|
|
||||||
|
|
||||||
if image_bytes is None:
|
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Create a temporary file to store the image bytes
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(delete=False) as temp_file:
|
|
||||||
temp_file.write(image_bytes)
|
|
||||||
temp_file.flush()
|
|
||||||
|
|
||||||
# Create image request using the temporary file
|
|
||||||
request = (
|
|
||||||
CreateImageRequest.builder()
|
|
||||||
.request_body(
|
|
||||||
CreateImageRequestBody.builder()
|
|
||||||
.image_type('message')
|
|
||||||
.image(open(temp_file.name, 'rb'))
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
|
|
||||||
response = await api_client.im.v1.image.acreate(request)
|
|
||||||
|
|
||||||
if not response.success():
|
|
||||||
raise Exception(
|
|
||||||
f'client.im.v1.image.create failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}'
|
|
||||||
)
|
|
||||||
|
|
||||||
image_key = response.data.image_key
|
|
||||||
|
|
||||||
message_elements.append(pending_paragraph)
|
|
||||||
message_elements.append(
|
|
||||||
[
|
|
||||||
{
|
|
||||||
'tag': 'img',
|
|
||||||
'image_key': image_key,
|
|
||||||
}
|
|
||||||
]
|
|
||||||
)
|
|
||||||
pending_paragraph = []
|
|
||||||
except Exception:
|
|
||||||
traceback.print_exc()
|
|
||||||
continue
|
|
||||||
finally:
|
|
||||||
# Clean up the temporary file
|
|
||||||
import os
|
|
||||||
|
|
||||||
if 'temp_file' in locals():
|
|
||||||
os.unlink(temp_file.name)
|
|
||||||
elif isinstance(msg, platform_message.Forward):
|
|
||||||
for node in msg.node_list:
|
|
||||||
message_elements.extend(await LarkMessageConverter.yiri2target(node.message_chain, api_client))
|
|
||||||
|
|
||||||
if pending_paragraph:
|
|
||||||
message_elements.append(pending_paragraph)
|
|
||||||
|
|
||||||
return message_elements
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def target2yiri(
|
|
||||||
message: lark_oapi.api.im.v1.model.event_message.EventMessage,
|
|
||||||
api_client: lark_oapi.Client,
|
|
||||||
) -> platform_message.MessageChain:
|
|
||||||
message_content = json.loads(message.content)
|
|
||||||
|
|
||||||
lb_msg_list = []
|
|
||||||
|
|
||||||
msg_create_time = datetime.datetime.fromtimestamp(int(message.create_time) / 1000)
|
|
||||||
|
|
||||||
lb_msg_list.append(platform_message.Source(id=message.message_id, time=msg_create_time))
|
|
||||||
|
|
||||||
if message.message_type == 'text':
|
|
||||||
element_list = []
|
|
||||||
|
|
||||||
def text_element_recur(text_ele: dict) -> list[dict]:
|
|
||||||
if text_ele['text'] == '':
|
|
||||||
return []
|
|
||||||
|
|
||||||
at_pattern = re.compile(r'@_user_[\d]+')
|
|
||||||
at_matches = at_pattern.findall(text_ele['text'])
|
|
||||||
|
|
||||||
name_mapping = {}
|
|
||||||
for mathc in at_matches:
|
|
||||||
for mention in message.mentions:
|
|
||||||
if mention.key == mathc:
|
|
||||||
name_mapping[mathc] = mention.name
|
|
||||||
break
|
|
||||||
|
|
||||||
if len(name_mapping.keys()) == 0:
|
|
||||||
return [text_ele]
|
|
||||||
|
|
||||||
# 只处理第一个,剩下的递归处理
|
|
||||||
text_split = text_ele['text'].split(list(name_mapping.keys())[0])
|
|
||||||
|
|
||||||
new_list = []
|
|
||||||
|
|
||||||
left_text = text_split[0]
|
|
||||||
right_text = text_split[1]
|
|
||||||
|
|
||||||
new_list.extend(text_element_recur({'tag': 'text', 'text': left_text, 'style': []}))
|
|
||||||
|
|
||||||
new_list.append(
|
|
||||||
{
|
|
||||||
'tag': 'at',
|
|
||||||
'user_id': list(name_mapping.keys())[0],
|
|
||||||
'user_name': name_mapping[list(name_mapping.keys())[0]],
|
|
||||||
'style': [],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
new_list.extend(text_element_recur({'tag': 'text', 'text': right_text, 'style': []}))
|
|
||||||
|
|
||||||
return new_list
|
|
||||||
|
|
||||||
element_list = text_element_recur({'tag': 'text', 'text': message_content['text'], 'style': []})
|
|
||||||
|
|
||||||
message_content = {'title': '', 'content': element_list}
|
|
||||||
|
|
||||||
elif message.message_type == 'post':
|
|
||||||
new_list = []
|
|
||||||
|
|
||||||
for ele in message_content['content']:
|
|
||||||
if type(ele) is dict:
|
|
||||||
new_list.append(ele)
|
|
||||||
elif type(ele) is list:
|
|
||||||
new_list.extend(ele)
|
|
||||||
|
|
||||||
message_content['content'] = new_list
|
|
||||||
elif message.message_type == 'image':
|
|
||||||
message_content['content'] = [{'tag': 'img', 'image_key': message_content['image_key'], 'style': []}]
|
|
||||||
|
|
||||||
for ele in message_content['content']:
|
|
||||||
if ele['tag'] == 'text':
|
|
||||||
lb_msg_list.append(platform_message.Plain(text=ele['text']))
|
|
||||||
elif ele['tag'] == 'at':
|
|
||||||
lb_msg_list.append(platform_message.At(target=ele['user_name']))
|
|
||||||
elif ele['tag'] == 'img':
|
|
||||||
image_key = ele['image_key']
|
|
||||||
|
|
||||||
request: GetMessageResourceRequest = (
|
|
||||||
GetMessageResourceRequest.builder()
|
|
||||||
.message_id(message.message_id)
|
|
||||||
.file_key(image_key)
|
|
||||||
.type('image')
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
|
|
||||||
response: GetMessageResourceResponse = await api_client.im.v1.message_resource.aget(request)
|
|
||||||
|
|
||||||
if not response.success():
|
|
||||||
raise Exception(
|
|
||||||
f'client.im.v1.message_resource.get failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}'
|
|
||||||
)
|
|
||||||
|
|
||||||
image_bytes = response.file.read()
|
|
||||||
image_base64 = base64.b64encode(image_bytes).decode()
|
|
||||||
|
|
||||||
image_format = response.raw.headers['content-type']
|
|
||||||
|
|
||||||
lb_msg_list.append(platform_message.Image(base64=f'data:{image_format};base64,{image_base64}'))
|
|
||||||
|
|
||||||
return platform_message.MessageChain(lb_msg_list)
|
|
||||||
|
|
||||||
|
|
||||||
class LarkEventConverter(abstract_platform_adapter.AbstractEventConverter):
|
|
||||||
@staticmethod
|
|
||||||
async def yiri2target(
|
|
||||||
event: platform_events.MessageEvent,
|
|
||||||
) -> lark_oapi.im.v1.P2ImMessageReceiveV1:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def target2yiri(
|
|
||||||
event: lark_oapi.im.v1.P2ImMessageReceiveV1, api_client: lark_oapi.Client
|
|
||||||
) -> platform_events.Event:
|
|
||||||
message_chain = await LarkMessageConverter.target2yiri(event.event.message, api_client)
|
|
||||||
|
|
||||||
if event.event.message.chat_type == 'p2p':
|
|
||||||
return platform_events.FriendMessage(
|
|
||||||
sender=platform_entities.Friend(
|
|
||||||
id=event.event.sender.sender_id.open_id,
|
|
||||||
nickname=event.event.sender.sender_id.union_id,
|
|
||||||
remark='',
|
|
||||||
),
|
|
||||||
message_chain=message_chain,
|
|
||||||
time=event.event.message.create_time,
|
|
||||||
)
|
|
||||||
elif event.event.message.chat_type == 'group':
|
|
||||||
return platform_events.GroupMessage(
|
|
||||||
sender=platform_entities.GroupMember(
|
|
||||||
id=event.event.sender.sender_id.open_id,
|
|
||||||
member_name=event.event.sender.sender_id.union_id,
|
|
||||||
permission=platform_entities.Permission.Member,
|
|
||||||
group=platform_entities.Group(
|
|
||||||
id=event.event.message.chat_id,
|
|
||||||
name='',
|
|
||||||
permission=platform_entities.Permission.Member,
|
|
||||||
),
|
|
||||||
special_title='',
|
|
||||||
join_timestamp=0,
|
|
||||||
last_speak_timestamp=0,
|
|
||||||
mute_time_remaining=0,
|
|
||||||
),
|
|
||||||
message_chain=message_chain,
|
|
||||||
time=event.event.message.create_time,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
CARD_ID_CACHE_SIZE = 500
|
|
||||||
CARD_ID_CACHE_MAX_LIFETIME = 20 * 60 # 20分钟
|
|
||||||
|
|
||||||
|
|
||||||
class LarkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|
||||||
bot: lark_oapi.ws.Client = pydantic.Field(exclude=True)
|
|
||||||
api_client: lark_oapi.Client = pydantic.Field(exclude=True)
|
|
||||||
|
|
||||||
bot_account_id: str # 用于在流水线中识别at是否是本bot,直接以bot_name作为标识
|
|
||||||
lark_tenant_key: str = pydantic.Field(exclude=True, default='') # 飞书企业key
|
|
||||||
|
|
||||||
message_converter: LarkMessageConverter = LarkMessageConverter()
|
|
||||||
event_converter: LarkEventConverter = LarkEventConverter()
|
|
||||||
|
|
||||||
listeners: typing.Dict[
|
|
||||||
typing.Type[platform_events.Event],
|
|
||||||
typing.Callable[[platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None],
|
|
||||||
]
|
|
||||||
|
|
||||||
quart_app: quart.Quart = pydantic.Field(exclude=True)
|
|
||||||
|
|
||||||
card_id_dict: dict[str, str] # 消息id到卡片id的映射,便于创建卡片后的发送消息到指定卡片
|
|
||||||
|
|
||||||
seq: int # 用于在发送卡片消息中识别消息顺序,直接以seq作为标识
|
|
||||||
|
|
||||||
def __init__(self, config: dict, logger: abstract_platform_logger.AbstractEventLogger, **kwargs):
|
|
||||||
quart_app = quart.Quart(__name__)
|
|
||||||
|
|
||||||
@quart_app.route('/lark/callback', methods=['POST'])
|
|
||||||
async def lark_callback():
|
|
||||||
try:
|
|
||||||
data = await quart.request.json
|
|
||||||
|
|
||||||
if 'encrypt' in data:
|
|
||||||
cipher = AESCipher(config['encrypt-key'])
|
|
||||||
data = cipher.decrypt_string(data['encrypt'])
|
|
||||||
data = json.loads(data)
|
|
||||||
|
|
||||||
type = data.get('type')
|
|
||||||
if type is None:
|
|
||||||
context = EventContext(data)
|
|
||||||
type = context.header.event_type
|
|
||||||
|
|
||||||
if 'url_verification' == type:
|
|
||||||
# todo 验证verification token
|
|
||||||
return {'challenge': data.get('challenge')}
|
|
||||||
context = EventContext(data)
|
|
||||||
type = context.header.event_type
|
|
||||||
p2v1 = P2ImMessageReceiveV1()
|
|
||||||
p2v1.header = context.header
|
|
||||||
event = P2ImMessageReceiveV1Data()
|
|
||||||
event.message = EventMessage(context.event['message'])
|
|
||||||
event.sender = EventSender(context.event['sender'])
|
|
||||||
p2v1.event = event
|
|
||||||
p2v1.schema = context.schema
|
|
||||||
if 'im.message.receive_v1' == type:
|
|
||||||
try:
|
|
||||||
event = await self.event_converter.target2yiri(p2v1, self.api_client)
|
|
||||||
except Exception:
|
|
||||||
await self.logger.error(f'Error in lark callback: {traceback.format_exc()}')
|
|
||||||
|
|
||||||
if event.__class__ in self.listeners:
|
|
||||||
await self.listeners[event.__class__](event, self)
|
|
||||||
|
|
||||||
return {'code': 200, 'message': 'ok'}
|
|
||||||
except Exception:
|
|
||||||
await self.logger.error(f'Error in lark callback: {traceback.format_exc()}')
|
|
||||||
return {'code': 500, 'message': 'error'}
|
|
||||||
|
|
||||||
async def on_message(event: lark_oapi.im.v1.P2ImMessageReceiveV1):
|
|
||||||
lb_event = await self.event_converter.target2yiri(event, self.api_client)
|
|
||||||
|
|
||||||
await self.listeners[type(lb_event)](lb_event, self)
|
|
||||||
|
|
||||||
def sync_on_message(event: lark_oapi.im.v1.P2ImMessageReceiveV1):
|
|
||||||
asyncio.create_task(on_message(event))
|
|
||||||
|
|
||||||
event_handler = (
|
|
||||||
lark_oapi.EventDispatcherHandler.builder('', '').register_p2_im_message_receive_v1(sync_on_message).build()
|
|
||||||
)
|
|
||||||
|
|
||||||
bot_account_id = config['bot_name']
|
|
||||||
|
|
||||||
bot = lark_oapi.ws.Client(config['app_id'], config['app_secret'], event_handler=event_handler)
|
|
||||||
api_client = lark_oapi.Client.builder().app_id(config['app_id']).app_secret(config['app_secret']).build()
|
|
||||||
|
|
||||||
super().__init__(
|
|
||||||
config=config,
|
|
||||||
logger=logger,
|
|
||||||
lark_tenant_key=config.get('lark_tenant_key', ''),
|
|
||||||
card_id_dict={},
|
|
||||||
seq=1,
|
|
||||||
listeners={},
|
|
||||||
quart_app=quart_app,
|
|
||||||
bot=bot,
|
|
||||||
api_client=api_client,
|
|
||||||
bot_account_id=bot_account_id,
|
|
||||||
**kwargs,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def send_message(self, target_type: str, target_id: str, message: platform_message.MessageChain):
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def is_stream_output_supported(self) -> bool:
|
|
||||||
is_stream = False
|
|
||||||
if self.config.get('enable-stream-reply', None):
|
|
||||||
is_stream = True
|
|
||||||
return is_stream
|
|
||||||
|
|
||||||
async def create_card_id(self, message_id):
|
|
||||||
try:
|
|
||||||
# self.logger.debug('飞书支持stream输出,创建卡片......')
|
|
||||||
|
|
||||||
card_data = {
|
|
||||||
'schema': '2.0',
|
|
||||||
'config': {
|
|
||||||
'update_multi': True,
|
|
||||||
'streaming_mode': True,
|
|
||||||
'streaming_config': {
|
|
||||||
'print_step': {'default': 1},
|
|
||||||
'print_frequency_ms': {'default': 70},
|
|
||||||
'print_strategy': 'fast',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'body': {
|
|
||||||
'direction': 'vertical',
|
|
||||||
'padding': '12px 12px 12px 12px',
|
|
||||||
'elements': [
|
|
||||||
{
|
|
||||||
'tag': 'div',
|
|
||||||
'text': {
|
|
||||||
'tag': 'plain_text',
|
|
||||||
'content': 'LangBot',
|
|
||||||
'text_size': 'normal',
|
|
||||||
'text_align': 'left',
|
|
||||||
'text_color': 'default',
|
|
||||||
},
|
|
||||||
'icon': {
|
|
||||||
'tag': 'custom_icon',
|
|
||||||
'img_key': 'img_v3_02p3_05c65d5d-9bad-440a-a2fb-c89571bfd5bg',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'tag': 'markdown',
|
|
||||||
'content': '',
|
|
||||||
'text_align': 'left',
|
|
||||||
'text_size': 'normal',
|
|
||||||
'margin': '0px 0px 0px 0px',
|
|
||||||
'element_id': 'streaming_txt',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'tag': 'markdown',
|
|
||||||
'content': '',
|
|
||||||
'text_align': 'left',
|
|
||||||
'text_size': 'normal',
|
|
||||||
'margin': '0px 0px 0px 0px',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'tag': 'column_set',
|
|
||||||
'horizontal_spacing': '8px',
|
|
||||||
'horizontal_align': 'left',
|
|
||||||
'columns': [
|
|
||||||
{
|
|
||||||
'tag': 'column',
|
|
||||||
'width': 'weighted',
|
|
||||||
'elements': [
|
|
||||||
{
|
|
||||||
'tag': 'markdown',
|
|
||||||
'content': '',
|
|
||||||
'text_align': 'left',
|
|
||||||
'text_size': 'normal',
|
|
||||||
'margin': '0px 0px 0px 0px',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'tag': 'markdown',
|
|
||||||
'content': '',
|
|
||||||
'text_align': 'left',
|
|
||||||
'text_size': 'normal',
|
|
||||||
'margin': '0px 0px 0px 0px',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'tag': 'markdown',
|
|
||||||
'content': '',
|
|
||||||
'text_align': 'left',
|
|
||||||
'text_size': 'normal',
|
|
||||||
'margin': '0px 0px 0px 0px',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'padding': '0px 0px 0px 0px',
|
|
||||||
'direction': 'vertical',
|
|
||||||
'horizontal_spacing': '8px',
|
|
||||||
'vertical_spacing': '2px',
|
|
||||||
'horizontal_align': 'left',
|
|
||||||
'vertical_align': 'top',
|
|
||||||
'margin': '0px 0px 0px 0px',
|
|
||||||
'weight': 1,
|
|
||||||
}
|
|
||||||
],
|
|
||||||
'margin': '0px 0px 0px 0px',
|
|
||||||
},
|
|
||||||
{'tag': 'hr', 'margin': '0px 0px 0px 0px'},
|
|
||||||
{
|
|
||||||
'tag': 'column_set',
|
|
||||||
'horizontal_spacing': '12px',
|
|
||||||
'horizontal_align': 'right',
|
|
||||||
'columns': [
|
|
||||||
{
|
|
||||||
'tag': 'column',
|
|
||||||
'width': 'weighted',
|
|
||||||
'elements': [
|
|
||||||
{
|
|
||||||
'tag': 'markdown',
|
|
||||||
'content': '<font color="grey-600">以上内容由 AI 生成,仅供参考。更多详细、准确信息可点击引用链接查看</font>',
|
|
||||||
'text_align': 'left',
|
|
||||||
'text_size': 'notation',
|
|
||||||
'margin': '4px 0px 0px 0px',
|
|
||||||
'icon': {
|
|
||||||
'tag': 'standard_icon',
|
|
||||||
'token': 'robot_outlined',
|
|
||||||
'color': 'grey',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
],
|
|
||||||
'padding': '0px 0px 0px 0px',
|
|
||||||
'direction': 'vertical',
|
|
||||||
'horizontal_spacing': '8px',
|
|
||||||
'vertical_spacing': '8px',
|
|
||||||
'horizontal_align': 'left',
|
|
||||||
'vertical_align': 'top',
|
|
||||||
'margin': '0px 0px 0px 0px',
|
|
||||||
'weight': 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'tag': 'column',
|
|
||||||
'width': '20px',
|
|
||||||
'elements': [
|
|
||||||
{
|
|
||||||
'tag': 'button',
|
|
||||||
'text': {'tag': 'plain_text', 'content': ''},
|
|
||||||
'type': 'text',
|
|
||||||
'width': 'fill',
|
|
||||||
'size': 'medium',
|
|
||||||
'icon': {'tag': 'standard_icon', 'token': 'thumbsup_outlined'},
|
|
||||||
'hover_tips': {'tag': 'plain_text', 'content': '有帮助'},
|
|
||||||
'margin': '0px 0px 0px 0px',
|
|
||||||
}
|
|
||||||
],
|
|
||||||
'padding': '0px 0px 0px 0px',
|
|
||||||
'direction': 'vertical',
|
|
||||||
'horizontal_spacing': '8px',
|
|
||||||
'vertical_spacing': '8px',
|
|
||||||
'horizontal_align': 'left',
|
|
||||||
'vertical_align': 'top',
|
|
||||||
'margin': '0px 0px 0px 0px',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'tag': 'column',
|
|
||||||
'width': '30px',
|
|
||||||
'elements': [
|
|
||||||
{
|
|
||||||
'tag': 'button',
|
|
||||||
'text': {'tag': 'plain_text', 'content': ''},
|
|
||||||
'type': 'text',
|
|
||||||
'width': 'default',
|
|
||||||
'size': 'medium',
|
|
||||||
'icon': {'tag': 'standard_icon', 'token': 'thumbdown_outlined'},
|
|
||||||
'hover_tips': {'tag': 'plain_text', 'content': '无帮助'},
|
|
||||||
'margin': '0px 0px 0px 0px',
|
|
||||||
}
|
|
||||||
],
|
|
||||||
'padding': '0px 0px 0px 0px',
|
|
||||||
'vertical_spacing': '8px',
|
|
||||||
'horizontal_align': 'left',
|
|
||||||
'vertical_align': 'top',
|
|
||||||
'margin': '0px 0px 0px 0px',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'margin': '0px 0px 4px 0px',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
# delay / fast 创建卡片模板,delay 延迟打印,fast 实时打印,可以自定义更好看的消息模板
|
|
||||||
|
|
||||||
request: CreateCardRequest = (
|
|
||||||
CreateCardRequest.builder()
|
|
||||||
.request_body(CreateCardRequestBody.builder().type('card_json').data(json.dumps(card_data)).build())
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
|
|
||||||
# 发起请求
|
|
||||||
response: CreateCardResponse = self.api_client.cardkit.v1.card.create(request)
|
|
||||||
|
|
||||||
# 处理失败返回
|
|
||||||
if not response.success():
|
|
||||||
raise Exception(
|
|
||||||
f'client.cardkit.v1.card.create failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}'
|
|
||||||
)
|
|
||||||
|
|
||||||
self.card_id_dict[message_id] = response.data.card_id
|
|
||||||
|
|
||||||
card_id = response.data.card_id
|
|
||||||
return card_id
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
raise e
|
|
||||||
async def create_message_card(self, message_id, event) -> str:
|
|
||||||
"""
|
|
||||||
创建卡片消息。
|
|
||||||
使用卡片消息是因为普通消息更新次数有限制,而大模型流式返回结果可能很多而超过限制,而飞书卡片没有这个限制(api免费次数有限)
|
|
||||||
"""
|
|
||||||
# message_id = event.message_chain.message_id
|
|
||||||
|
|
||||||
card_id = await self.create_card_id(message_id)
|
|
||||||
content = {
|
|
||||||
'type': 'card',
|
|
||||||
'data': {'card_id': card_id, 'template_variable': {'content': 'Thinking...'}},
|
|
||||||
} # 当收到消息时发送消息模板,可添加模板变量,详情查看飞书中接口文档
|
|
||||||
request: ReplyMessageRequest = (
|
|
||||||
ReplyMessageRequest.builder()
|
|
||||||
.message_id(event.message_chain.message_id)
|
|
||||||
.request_body(
|
|
||||||
ReplyMessageRequestBody.builder().content(json.dumps(content)).msg_type('interactive').build()
|
|
||||||
)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
|
|
||||||
# 发起请求
|
|
||||||
response: ReplyMessageResponse = await self.api_client.im.v1.message.areply(request)
|
|
||||||
|
|
||||||
# 处理失败返回
|
|
||||||
if not response.success():
|
|
||||||
raise Exception(
|
|
||||||
f'client.im.v1.message.reply failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}'
|
|
||||||
)
|
|
||||||
return True
|
|
||||||
|
|
||||||
async def reply_message(
|
|
||||||
self,
|
|
||||||
message_source: platform_events.MessageEvent,
|
|
||||||
message: platform_message.MessageChain,
|
|
||||||
quote_origin: bool = False,
|
|
||||||
):
|
|
||||||
# 不再需要了,因为message_id已经被包含到message_chain中
|
|
||||||
# lark_event = await self.event_converter.yiri2target(message_source)
|
|
||||||
lark_message = await self.message_converter.yiri2target(message, self.api_client)
|
|
||||||
|
|
||||||
final_content = {
|
|
||||||
'zh_Hans': {
|
|
||||||
'title': '',
|
|
||||||
'content': lark_message,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
request: ReplyMessageRequest = (
|
|
||||||
ReplyMessageRequest.builder()
|
|
||||||
.message_id(message_source.message_chain.message_id)
|
|
||||||
.request_body(
|
|
||||||
ReplyMessageRequestBody.builder()
|
|
||||||
.content(json.dumps(final_content))
|
|
||||||
.msg_type('post')
|
|
||||||
.reply_in_thread(False)
|
|
||||||
.uuid(str(uuid.uuid4()))
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
|
|
||||||
response: ReplyMessageResponse = await self.api_client.im.v1.message.areply(request)
|
|
||||||
|
|
||||||
if not response.success():
|
|
||||||
raise Exception(
|
|
||||||
f'client.im.v1.message.reply failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}'
|
|
||||||
)
|
|
||||||
|
|
||||||
async def reply_message_chunk(
|
|
||||||
self,
|
|
||||||
message_source: platform_events.MessageEvent,
|
|
||||||
bot_message,
|
|
||||||
message: platform_message.MessageChain,
|
|
||||||
quote_origin: bool = False,
|
|
||||||
is_final: bool = False,
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
回复消息变成更新卡片消息
|
|
||||||
"""
|
|
||||||
# self.seq += 1
|
|
||||||
message_id = bot_message.resp_message_id
|
|
||||||
msg_seq = bot_message.msg_sequence
|
|
||||||
if msg_seq % 8 == 0 or is_final:
|
|
||||||
lark_message = await self.message_converter.yiri2target(message, self.api_client)
|
|
||||||
|
|
||||||
text_message = ''
|
|
||||||
for ele in lark_message[0]:
|
|
||||||
if ele['tag'] == 'text':
|
|
||||||
text_message += ele['text']
|
|
||||||
elif ele['tag'] == 'md':
|
|
||||||
text_message += ele['text']
|
|
||||||
|
|
||||||
# content = {
|
|
||||||
# 'type': 'card_json',
|
|
||||||
# 'data': {'card_id': self.card_id_dict[message_id], 'elements': {'content': text_message}},
|
|
||||||
# }
|
|
||||||
|
|
||||||
request: ContentCardElementRequest = (
|
|
||||||
ContentCardElementRequest.builder()
|
|
||||||
.card_id(self.card_id_dict[message_id])
|
|
||||||
.element_id('streaming_txt')
|
|
||||||
.request_body(
|
|
||||||
ContentCardElementRequestBody.builder()
|
|
||||||
# .uuid("a0d69e20-1dd1-458b-k525-dfeca4015204")
|
|
||||||
.content(text_message)
|
|
||||||
.sequence(msg_seq)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
|
|
||||||
if is_final and bot_message.tool_calls is None:
|
|
||||||
# self.seq = 1 # 消息回复结束之后重置seq
|
|
||||||
self.card_id_dict.pop(message_id) # 清理已经使用过的卡片
|
|
||||||
# 发起请求
|
|
||||||
response: ContentCardElementResponse = self.api_client.cardkit.v1.card_element.content(request)
|
|
||||||
|
|
||||||
# 处理失败返回
|
|
||||||
if not response.success():
|
|
||||||
raise Exception(
|
|
||||||
f'client.im.v1.message.patch failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}'
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
async def is_muted(self, group_id: int) -> bool:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def register_listener(
|
|
||||||
self,
|
|
||||||
event_type: typing.Type[platform_events.Event],
|
|
||||||
callback: typing.Callable[
|
|
||||||
[platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None
|
|
||||||
],
|
|
||||||
):
|
|
||||||
self.listeners[event_type] = callback
|
|
||||||
|
|
||||||
def unregister_listener(
|
|
||||||
self,
|
|
||||||
event_type: typing.Type[platform_events.Event],
|
|
||||||
callback: typing.Callable[
|
|
||||||
[platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None
|
|
||||||
],
|
|
||||||
):
|
|
||||||
self.listeners.pop(event_type)
|
|
||||||
|
|
||||||
async def run_async(self):
|
|
||||||
port = self.config['port']
|
|
||||||
enable_webhook = self.config['enable-webhook']
|
|
||||||
|
|
||||||
if not enable_webhook:
|
|
||||||
try:
|
|
||||||
await self.bot._connect()
|
|
||||||
except lark_oapi.ws.exception.ClientException as e:
|
|
||||||
raise e
|
|
||||||
except Exception as e:
|
|
||||||
await self.bot._disconnect()
|
|
||||||
if self.bot._auto_reconnect:
|
|
||||||
await self.bot._reconnect()
|
|
||||||
else:
|
|
||||||
raise e
|
|
||||||
else:
|
|
||||||
|
|
||||||
async def shutdown_trigger_placeholder():
|
|
||||||
while True:
|
|
||||||
await asyncio.sleep(1)
|
|
||||||
|
|
||||||
await self.quart_app.run_task(
|
|
||||||
host='0.0.0.0',
|
|
||||||
port=port,
|
|
||||||
shutdown_trigger=shutdown_trigger_placeholder,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def kill(self) -> bool:
|
|
||||||
# 需要断开连接,不然旧的连接会继续运行,导致飞书消息来时会随机选择一个连接
|
|
||||||
# 断开时lark.ws.Client的_receive_message_loop会打印error日志: receive message loop exit。然后进行重连,
|
|
||||||
# 所以要设置_auto_reconnect=False,让其不重连。
|
|
||||||
self.bot._auto_reconnect = False
|
|
||||||
await self.bot._disconnect()
|
|
||||||
return False
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: MessagePlatformAdapter
|
|
||||||
metadata:
|
|
||||||
name: lark
|
|
||||||
label:
|
|
||||||
en_US: Lark
|
|
||||||
zh_Hans: 飞书
|
|
||||||
description:
|
|
||||||
en_US: Lark Adapter
|
|
||||||
zh_Hans: 飞书适配器,请查看文档了解使用方式
|
|
||||||
icon: lark.svg
|
|
||||||
spec:
|
|
||||||
config:
|
|
||||||
- name: app_id
|
|
||||||
label:
|
|
||||||
en_US: App ID
|
|
||||||
zh_Hans: 应用ID
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: ""
|
|
||||||
- name: app_secret
|
|
||||||
label:
|
|
||||||
en_US: App Secret
|
|
||||||
zh_Hans: 应用密钥
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: ""
|
|
||||||
- name: bot_name
|
|
||||||
label:
|
|
||||||
en_US: Bot Name
|
|
||||||
zh_Hans: 机器人名称
|
|
||||||
description:
|
|
||||||
en_US: Must be the same as the name of the bot in Lark, otherwise the bot will not be able to receive messages in the group
|
|
||||||
zh_Hans: 必须与飞书机器人名称一致,否则机器人将无法在群内正常接收消息
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: ""
|
|
||||||
- name: enable-webhook
|
|
||||||
label:
|
|
||||||
en_US: Enable Webhook Mode
|
|
||||||
zh_Hans: 启用Webhook模式
|
|
||||||
description:
|
|
||||||
en_US: If enabled, the bot will use webhook mode to receive messages. Otherwise, it will use WS long connection mode
|
|
||||||
zh_Hans: 如果启用,机器人将使用 Webhook 模式接收消息。否则,将使用 WS 长连接模式
|
|
||||||
type: boolean
|
|
||||||
required: true
|
|
||||||
default: false
|
|
||||||
- name: port
|
|
||||||
label:
|
|
||||||
en_US: Webhook Port
|
|
||||||
zh_Hans: Webhook端口
|
|
||||||
description:
|
|
||||||
en_US: Only valid when webhook mode is enabled, please fill in the webhook port
|
|
||||||
zh_Hans: 仅在启用 Webhook 模式时有效,请填写 Webhook 端口
|
|
||||||
type: integer
|
|
||||||
required: true
|
|
||||||
default: 2285
|
|
||||||
- name: encrypt-key
|
|
||||||
label:
|
|
||||||
en_US: Encrypt Key
|
|
||||||
zh_Hans: 加密密钥
|
|
||||||
description:
|
|
||||||
en_US: Only valid when webhook mode is enabled, please fill in the encrypt key
|
|
||||||
zh_Hans: 仅在启用 Webhook 模式时有效,请填写加密密钥
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: ""
|
|
||||||
- name: enable-stream-reply
|
|
||||||
label:
|
|
||||||
en_US: Enable Stream Reply Mode
|
|
||||||
zh_Hans: 启用飞书流式回复模式
|
|
||||||
description:
|
|
||||||
en_US: If enabled, the bot will use the stream of lark reply mode
|
|
||||||
zh_Hans: 如果启用,将使用飞书流式方式来回复内容
|
|
||||||
type: boolean
|
|
||||||
required: true
|
|
||||||
default: false
|
|
||||||
execution:
|
|
||||||
python:
|
|
||||||
path: ./lark.py
|
|
||||||
attr: LarkAdapter
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: MessagePlatformAdapter
|
|
||||||
metadata:
|
|
||||||
name: LINE
|
|
||||||
label:
|
|
||||||
en_US: LINE
|
|
||||||
zh_Hans: LINE
|
|
||||||
description:
|
|
||||||
en_US: LINE Adapter
|
|
||||||
zh_Hans: LINE适配器,请查看文档了解使用方式
|
|
||||||
ja_JP: LINEアダプター、ドキュメントを参照してください
|
|
||||||
zh_Hant: LINE適配器,請查看文檔了解使用方式
|
|
||||||
icon: line.png
|
|
||||||
spec:
|
|
||||||
config:
|
|
||||||
- name: channel_access_token
|
|
||||||
label:
|
|
||||||
en_US: Channel access token
|
|
||||||
zh_Hans: 频道访问令牌
|
|
||||||
ja_JP: チャンネルアクセストークン
|
|
||||||
zh_Hant: 頻道訪問令牌
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: ""
|
|
||||||
- name: port
|
|
||||||
label:
|
|
||||||
en_US: Webhook Port
|
|
||||||
zh_Hans: Webhook端口
|
|
||||||
description:
|
|
||||||
en_US: Only valid when webhook mode is enabled, please fill in the webhook port
|
|
||||||
zh_Hans: 请填写 Webhook 端口
|
|
||||||
ja_JP: Webhookポートを入力してください
|
|
||||||
zh_Hant: 請填寫 Webhook 端口
|
|
||||||
type: integer
|
|
||||||
required: true
|
|
||||||
default: 2287
|
|
||||||
- name: channel_secret
|
|
||||||
label:
|
|
||||||
en_US: Channel secret
|
|
||||||
zh_Hans: 消息密钥
|
|
||||||
ja_JP: チャンネルシークレット
|
|
||||||
zh_Hant: 消息密钥
|
|
||||||
description:
|
|
||||||
en_US: Only valid when webhook mode is enabled, please fill in the encrypt key
|
|
||||||
zh_Hans: 请填写加密密钥
|
|
||||||
ja_JP: Webhookモードが有効な場合にのみ、暗号化キーを入力してください
|
|
||||||
zh_Hant: 請填寫加密密钥
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: ""
|
|
||||||
execution:
|
|
||||||
python:
|
|
||||||
path: ./line.py
|
|
||||||
attr: LINEAdapter
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: MessagePlatformAdapter
|
|
||||||
metadata:
|
|
||||||
name: officialaccount
|
|
||||||
label:
|
|
||||||
en_US: Official Account
|
|
||||||
zh_Hans: 微信公众号
|
|
||||||
description:
|
|
||||||
en_US: Official Account Adapter
|
|
||||||
zh_Hans: 微信公众号适配器,请查看文档了解使用方式
|
|
||||||
icon: officialaccount.png
|
|
||||||
spec:
|
|
||||||
config:
|
|
||||||
- name: token
|
|
||||||
label:
|
|
||||||
en_US: Token
|
|
||||||
zh_Hans: 令牌
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: ""
|
|
||||||
- name: EncodingAESKey
|
|
||||||
label:
|
|
||||||
en_US: EncodingAESKey
|
|
||||||
zh_Hans: 消息加解密密钥
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: ""
|
|
||||||
- name: AppID
|
|
||||||
label:
|
|
||||||
en_US: App ID
|
|
||||||
zh_Hans: 应用ID
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: ""
|
|
||||||
- name: AppSecret
|
|
||||||
label:
|
|
||||||
en_US: App Secret
|
|
||||||
zh_Hans: 应用密钥
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: ""
|
|
||||||
- name: Mode
|
|
||||||
label:
|
|
||||||
en_US: Mode
|
|
||||||
zh_Hans: 接入模式
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: "drop"
|
|
||||||
- name: LoadingMessage
|
|
||||||
label:
|
|
||||||
en_US: Loading Message
|
|
||||||
zh_Hans: 加载消息
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: "AI正在思考中,请发送任意内容获取回复。"
|
|
||||||
- name: host
|
|
||||||
label:
|
|
||||||
en_US: Host
|
|
||||||
zh_Hans: 监听主机
|
|
||||||
description:
|
|
||||||
en_US: The host that Official Account listens on for Webhook connections.
|
|
||||||
zh_Hans: 微信公众号监听的主机,除非你知道自己在做什么,否则请写 0.0.0.0
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: 0.0.0.0
|
|
||||||
- name: port
|
|
||||||
label:
|
|
||||||
en_US: Port
|
|
||||||
zh_Hans: 监听端口
|
|
||||||
type: integer
|
|
||||||
required: true
|
|
||||||
default: 2287
|
|
||||||
execution:
|
|
||||||
python:
|
|
||||||
path: ./officialaccount.py
|
|
||||||
attr: OfficialAccountAdapter
|
|
||||||
@@ -1,250 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
import typing
|
|
||||||
import asyncio
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
import datetime
|
|
||||||
|
|
||||||
import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter
|
|
||||||
import langbot_plugin.api.entities.builtin.platform.message as platform_message
|
|
||||||
import langbot_plugin.api.entities.builtin.platform.events as platform_events
|
|
||||||
import langbot_plugin.api.entities.builtin.platform.entities as platform_entities
|
|
||||||
from langbot_plugin.api.entities.builtin.command import errors as command_errors
|
|
||||||
from libs.qq_official_api.api import QQOfficialClient
|
|
||||||
from libs.qq_official_api.qqofficialevent import QQOfficialEvent
|
|
||||||
from ...utils import image
|
|
||||||
from ..logger import EventLogger
|
|
||||||
|
|
||||||
|
|
||||||
class QQOfficialMessageConverter(abstract_platform_adapter.AbstractMessageConverter):
|
|
||||||
@staticmethod
|
|
||||||
async def yiri2target(message_chain: platform_message.MessageChain):
|
|
||||||
content_list = []
|
|
||||||
# 只实现了发文字
|
|
||||||
for msg in message_chain:
|
|
||||||
if type(msg) is platform_message.Plain:
|
|
||||||
content_list.append(
|
|
||||||
{
|
|
||||||
'type': 'text',
|
|
||||||
'content': msg.text,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return content_list
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def target2yiri(message: str, message_id: str, pic_url: str, content_type):
|
|
||||||
yiri_msg_list = []
|
|
||||||
yiri_msg_list.append(platform_message.Source(id=message_id, time=datetime.datetime.now()))
|
|
||||||
if pic_url is not None:
|
|
||||||
base64_url = await image.get_qq_official_image_base64(pic_url=pic_url, content_type=content_type)
|
|
||||||
yiri_msg_list.append(platform_message.Image(base64=base64_url))
|
|
||||||
|
|
||||||
yiri_msg_list.append(platform_message.Plain(text=message))
|
|
||||||
chain = platform_message.MessageChain(yiri_msg_list)
|
|
||||||
return chain
|
|
||||||
|
|
||||||
|
|
||||||
class QQOfficialEventConverter(abstract_platform_adapter.AbstractEventConverter):
|
|
||||||
@staticmethod
|
|
||||||
async def yiri2target(event: platform_events.MessageEvent) -> QQOfficialEvent:
|
|
||||||
return event.source_platform_object
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def target2yiri(event: QQOfficialEvent):
|
|
||||||
"""
|
|
||||||
QQ官方消息转换为LB对象
|
|
||||||
"""
|
|
||||||
yiri_chain = await QQOfficialMessageConverter.target2yiri(
|
|
||||||
message=event.content,
|
|
||||||
message_id=event.d_id,
|
|
||||||
pic_url=event.attachments,
|
|
||||||
content_type=event.content_type,
|
|
||||||
)
|
|
||||||
|
|
||||||
if event.t == 'C2C_MESSAGE_CREATE':
|
|
||||||
friend = platform_entities.Friend(
|
|
||||||
id=event.user_openid,
|
|
||||||
nickname=event.t,
|
|
||||||
remark='',
|
|
||||||
)
|
|
||||||
return platform_events.FriendMessage(
|
|
||||||
sender=friend,
|
|
||||||
message_chain=yiri_chain,
|
|
||||||
time=int(datetime.datetime.strptime(event.timestamp, '%Y-%m-%dT%H:%M:%S%z').timestamp()),
|
|
||||||
source_platform_object=event,
|
|
||||||
)
|
|
||||||
|
|
||||||
if event.t == 'DIRECT_MESSAGE_CREATE':
|
|
||||||
friend = platform_entities.Friend(
|
|
||||||
id=event.guild_id,
|
|
||||||
nickname=event.t,
|
|
||||||
remark='',
|
|
||||||
)
|
|
||||||
return platform_events.FriendMessage(sender=friend, message_chain=yiri_chain, source_platform_object=event)
|
|
||||||
if event.t == 'GROUP_AT_MESSAGE_CREATE':
|
|
||||||
yiri_chain.insert(0, platform_message.At(target='justbot'))
|
|
||||||
|
|
||||||
sender = platform_entities.GroupMember(
|
|
||||||
id=event.group_openid,
|
|
||||||
member_name=event.t,
|
|
||||||
permission='MEMBER',
|
|
||||||
group=platform_entities.Group(
|
|
||||||
id=event.group_openid,
|
|
||||||
name='MEMBER',
|
|
||||||
permission=platform_entities.Permission.Member,
|
|
||||||
),
|
|
||||||
special_title='',
|
|
||||||
join_timestamp=0,
|
|
||||||
last_speak_timestamp=0,
|
|
||||||
mute_time_remaining=0,
|
|
||||||
)
|
|
||||||
time = int(datetime.datetime.strptime(event.timestamp, '%Y-%m-%dT%H:%M:%S%z').timestamp())
|
|
||||||
return platform_events.GroupMessage(
|
|
||||||
sender=sender,
|
|
||||||
message_chain=yiri_chain,
|
|
||||||
time=time,
|
|
||||||
source_platform_object=event,
|
|
||||||
)
|
|
||||||
if event.t == 'AT_MESSAGE_CREATE':
|
|
||||||
yiri_chain.insert(0, platform_message.At(target='justbot'))
|
|
||||||
sender = platform_entities.GroupMember(
|
|
||||||
id=event.channel_id,
|
|
||||||
member_name=event.t,
|
|
||||||
permission='MEMBER',
|
|
||||||
group=platform_entities.Group(
|
|
||||||
id=event.channel_id,
|
|
||||||
name='MEMBER',
|
|
||||||
permission=platform_entities.Permission.Member,
|
|
||||||
),
|
|
||||||
special_title='',
|
|
||||||
join_timestamp=0,
|
|
||||||
last_speak_timestamp=0,
|
|
||||||
mute_time_remaining=0,
|
|
||||||
)
|
|
||||||
time = int(datetime.datetime.strptime(event.timestamp, '%Y-%m-%dT%H:%M:%S%z').timestamp())
|
|
||||||
return platform_events.GroupMessage(
|
|
||||||
sender=sender,
|
|
||||||
message_chain=yiri_chain,
|
|
||||||
time=time,
|
|
||||||
source_platform_object=event,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class QQOfficialAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|
||||||
bot: QQOfficialClient
|
|
||||||
config: dict
|
|
||||||
bot_account_id: str
|
|
||||||
message_converter: QQOfficialMessageConverter = QQOfficialMessageConverter()
|
|
||||||
event_converter: QQOfficialEventConverter = QQOfficialEventConverter()
|
|
||||||
|
|
||||||
def __init__(self, config: dict, logger: EventLogger):
|
|
||||||
bot = QQOfficialClient(
|
|
||||||
app_id=config['appid'], secret=config['secret'], token=config['token'], logger=logger
|
|
||||||
)
|
|
||||||
|
|
||||||
super().__init__(
|
|
||||||
config=config,
|
|
||||||
logger=logger,
|
|
||||||
bot=bot,
|
|
||||||
bot_account_id=config['appid'],
|
|
||||||
)
|
|
||||||
|
|
||||||
async def reply_message(
|
|
||||||
self,
|
|
||||||
message_source: platform_events.MessageEvent,
|
|
||||||
message: platform_message.MessageChain,
|
|
||||||
quote_origin: bool = False,
|
|
||||||
):
|
|
||||||
qq_official_event = await QQOfficialEventConverter.yiri2target(
|
|
||||||
message_source,
|
|
||||||
)
|
|
||||||
|
|
||||||
content_list = await QQOfficialMessageConverter.yiri2target(message)
|
|
||||||
|
|
||||||
# 私聊消息
|
|
||||||
if qq_official_event.t == 'C2C_MESSAGE_CREATE':
|
|
||||||
for content in content_list:
|
|
||||||
if content['type'] == 'text':
|
|
||||||
await self.bot.send_private_text_msg(
|
|
||||||
qq_official_event.user_openid,
|
|
||||||
content['content'],
|
|
||||||
qq_official_event.d_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
# 群聊消息
|
|
||||||
if qq_official_event.t == 'GROUP_AT_MESSAGE_CREATE':
|
|
||||||
for content in content_list:
|
|
||||||
if content['type'] == 'text':
|
|
||||||
await self.bot.send_group_text_msg(
|
|
||||||
qq_official_event.group_openid,
|
|
||||||
content['content'],
|
|
||||||
qq_official_event.d_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
# 频道群聊
|
|
||||||
if qq_official_event.t == 'AT_MESSAGE_CREATE':
|
|
||||||
for content in content_list:
|
|
||||||
if content['type'] == 'text':
|
|
||||||
await self.bot.send_channle_group_text_msg(
|
|
||||||
qq_official_event.channel_id,
|
|
||||||
content['content'],
|
|
||||||
qq_official_event.d_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
# 频道私聊
|
|
||||||
if qq_official_event.t == 'DIRECT_MESSAGE_CREATE':
|
|
||||||
for content in content_list:
|
|
||||||
if content['type'] == 'text':
|
|
||||||
await self.bot.send_channle_private_text_msg(
|
|
||||||
qq_official_event.guild_id,
|
|
||||||
content['content'],
|
|
||||||
qq_official_event.d_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def send_message(self, target_type: str, target_id: str, message: platform_message.MessageChain):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def register_listener(
|
|
||||||
self,
|
|
||||||
event_type: typing.Type[platform_events.Event],
|
|
||||||
callback: typing.Callable[
|
|
||||||
[platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None
|
|
||||||
],
|
|
||||||
):
|
|
||||||
async def on_message(event: QQOfficialEvent):
|
|
||||||
self.bot_account_id = 'justbot'
|
|
||||||
try:
|
|
||||||
return await callback(await self.event_converter.target2yiri(event), self)
|
|
||||||
except Exception:
|
|
||||||
await self.logger.error(f'Error in qqofficial callback: {traceback.format_exc()}')
|
|
||||||
|
|
||||||
if event_type == platform_events.FriendMessage:
|
|
||||||
self.bot.on_message('DIRECT_MESSAGE_CREATE')(on_message)
|
|
||||||
self.bot.on_message('C2C_MESSAGE_CREATE')(on_message)
|
|
||||||
elif event_type == platform_events.GroupMessage:
|
|
||||||
self.bot.on_message('GROUP_AT_MESSAGE_CREATE')(on_message)
|
|
||||||
self.bot.on_message('AT_MESSAGE_CREATE')(on_message)
|
|
||||||
|
|
||||||
async def run_async(self):
|
|
||||||
async def shutdown_trigger_placeholder():
|
|
||||||
while True:
|
|
||||||
await asyncio.sleep(1)
|
|
||||||
|
|
||||||
await self.bot.run_task(
|
|
||||||
host='0.0.0.0',
|
|
||||||
port=self.config['port'],
|
|
||||||
shutdown_trigger=shutdown_trigger_placeholder,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def kill(self) -> bool:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def unregister_listener(
|
|
||||||
self,
|
|
||||||
event_type: type,
|
|
||||||
callback: typing.Callable[
|
|
||||||
[platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None
|
|
||||||
],
|
|
||||||
):
|
|
||||||
return super().unregister_listener(event_type, callback)
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: MessagePlatformAdapter
|
|
||||||
metadata:
|
|
||||||
name: qqofficial
|
|
||||||
label:
|
|
||||||
en_US: QQ Official API
|
|
||||||
zh_Hans: QQ 官方 API
|
|
||||||
description:
|
|
||||||
en_US: QQ Official API (Webhook)
|
|
||||||
zh_Hans: QQ 官方 API (Webhook),请查看文档了解使用方式
|
|
||||||
icon: qqofficial.svg
|
|
||||||
spec:
|
|
||||||
config:
|
|
||||||
- name: appid
|
|
||||||
label:
|
|
||||||
en_US: App ID
|
|
||||||
zh_Hans: 应用ID
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: ""
|
|
||||||
- name: secret
|
|
||||||
label:
|
|
||||||
en_US: Secret
|
|
||||||
zh_Hans: 密钥
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: ""
|
|
||||||
- name: port
|
|
||||||
label:
|
|
||||||
en_US: Port
|
|
||||||
zh_Hans: 监听端口
|
|
||||||
type: integer
|
|
||||||
required: true
|
|
||||||
default: 2284
|
|
||||||
- name: token
|
|
||||||
label:
|
|
||||||
en_US: Token
|
|
||||||
zh_Hans: 令牌
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: ""
|
|
||||||
execution:
|
|
||||||
python:
|
|
||||||
path: ./qqofficial.py
|
|
||||||
attr: QQOfficialAdapter
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: MessagePlatformAdapter
|
|
||||||
metadata:
|
|
||||||
name: slack
|
|
||||||
label:
|
|
||||||
en_US: Slack
|
|
||||||
zh_Hans: Slack
|
|
||||||
description:
|
|
||||||
en_US: Slack Adapter
|
|
||||||
zh_Hans: Slack 适配器,请查看文档了解使用方式
|
|
||||||
icon: slack.png
|
|
||||||
spec:
|
|
||||||
config:
|
|
||||||
- name: bot_token
|
|
||||||
label:
|
|
||||||
en_US: Bot Token
|
|
||||||
zh_Hans: 机器人令牌
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: ""
|
|
||||||
- name: signing_secret
|
|
||||||
label:
|
|
||||||
en_US: signing_secret
|
|
||||||
zh_Hans: 密钥
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: ""
|
|
||||||
- name: port
|
|
||||||
label:
|
|
||||||
en_US: Port
|
|
||||||
zh_Hans: 监听端口
|
|
||||||
type: int
|
|
||||||
required: true
|
|
||||||
default: 2288
|
|
||||||
execution:
|
|
||||||
python:
|
|
||||||
path: ./slack.py
|
|
||||||
attr: SlackAdapter
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: MessagePlatformAdapter
|
|
||||||
metadata:
|
|
||||||
name: telegram
|
|
||||||
label:
|
|
||||||
en_US: Telegram
|
|
||||||
zh_Hans: 电报
|
|
||||||
description:
|
|
||||||
en_US: Telegram Adapter
|
|
||||||
zh_Hans: 电报适配器,请查看文档了解使用方式
|
|
||||||
icon: telegram.svg
|
|
||||||
spec:
|
|
||||||
config:
|
|
||||||
- name: token
|
|
||||||
label:
|
|
||||||
en_US: Token
|
|
||||||
zh_Hans: 令牌
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: ""
|
|
||||||
- name: markdown_card
|
|
||||||
label:
|
|
||||||
en_US: Markdown Card
|
|
||||||
zh_Hans: 是否使用 Markdown 卡片
|
|
||||||
type: boolean
|
|
||||||
required: false
|
|
||||||
default: true
|
|
||||||
- name: enable-stream-reply
|
|
||||||
label:
|
|
||||||
en_US: Enable Stream Reply Mode
|
|
||||||
zh_Hans: 启用电报流式回复模式
|
|
||||||
description:
|
|
||||||
en_US: If enabled, the bot will use the stream of telegram reply mode
|
|
||||||
zh_Hans: 如果启用,将使用电报流式方式来回复内容
|
|
||||||
type: boolean
|
|
||||||
required: true
|
|
||||||
default: false
|
|
||||||
execution:
|
|
||||||
python:
|
|
||||||
path: ./telegram.py
|
|
||||||
attr: TelegramAdapter
|
|
||||||
@@ -1,304 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
import logging
|
|
||||||
import typing
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
import pydantic
|
|
||||||
|
|
||||||
import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter
|
|
||||||
import langbot_plugin.api.entities.builtin.platform.message as platform_message
|
|
||||||
import langbot_plugin.api.entities.builtin.platform.events as platform_events
|
|
||||||
import langbot_plugin.api.entities.builtin.platform.entities as platform_entities
|
|
||||||
import langbot_plugin.api.definition.abstract.platform.event_logger as abstract_platform_logger
|
|
||||||
from ...core import app
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class WebChatMessage(pydantic.BaseModel):
|
|
||||||
id: int
|
|
||||||
role: str
|
|
||||||
content: str
|
|
||||||
message_chain: list[dict]
|
|
||||||
timestamp: str
|
|
||||||
is_final: bool = False
|
|
||||||
|
|
||||||
|
|
||||||
class WebChatSession:
|
|
||||||
id: str
|
|
||||||
message_lists: dict[str, list[WebChatMessage]] = {}
|
|
||||||
resp_waiters: dict[int, asyncio.Future[WebChatMessage]]
|
|
||||||
resp_queues: dict[int, asyncio.Queue[WebChatMessage]]
|
|
||||||
|
|
||||||
def __init__(self, id: str):
|
|
||||||
self.id = id
|
|
||||||
self.message_lists = {}
|
|
||||||
self.resp_waiters = {}
|
|
||||||
self.resp_queues = {}
|
|
||||||
|
|
||||||
def get_message_list(self, pipeline_uuid: str) -> list[WebChatMessage]:
|
|
||||||
if pipeline_uuid not in self.message_lists:
|
|
||||||
self.message_lists[pipeline_uuid] = []
|
|
||||||
|
|
||||||
return self.message_lists[pipeline_uuid]
|
|
||||||
|
|
||||||
|
|
||||||
class WebChatAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|
||||||
"""WebChat调试适配器,用于流水线调试"""
|
|
||||||
|
|
||||||
webchat_person_session: WebChatSession = pydantic.Field(exclude=True, default_factory=WebChatSession)
|
|
||||||
webchat_group_session: WebChatSession = pydantic.Field(exclude=True, default_factory=WebChatSession)
|
|
||||||
|
|
||||||
listeners: dict[
|
|
||||||
typing.Type[platform_events.Event],
|
|
||||||
typing.Callable[[platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None],
|
|
||||||
] = pydantic.Field(default_factory=dict, exclude=True)
|
|
||||||
|
|
||||||
is_stream: bool = pydantic.Field(exclude=True)
|
|
||||||
debug_messages: dict[str, list[dict]] = pydantic.Field(default_factory=dict, exclude=True)
|
|
||||||
|
|
||||||
ap: app.Application = pydantic.Field(exclude=True)
|
|
||||||
|
|
||||||
def __init__(self, config: dict, logger: abstract_platform_logger.AbstractEventLogger, **kwargs):
|
|
||||||
super().__init__(
|
|
||||||
config=config,
|
|
||||||
logger=logger,
|
|
||||||
**kwargs,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.webchat_person_session = WebChatSession(id='webchatperson')
|
|
||||||
self.webchat_group_session = WebChatSession(id='webchatgroup')
|
|
||||||
|
|
||||||
self.bot_account_id = 'webchatbot'
|
|
||||||
|
|
||||||
self.debug_messages = {}
|
|
||||||
|
|
||||||
async def send_message(
|
|
||||||
self,
|
|
||||||
target_type: str,
|
|
||||||
target_id: str,
|
|
||||||
message: platform_message.MessageChain,
|
|
||||||
) -> dict:
|
|
||||||
"""发送消息到调试会话"""
|
|
||||||
session_key = target_id
|
|
||||||
|
|
||||||
if session_key not in self.debug_messages:
|
|
||||||
self.debug_messages[session_key] = []
|
|
||||||
|
|
||||||
message_data = {
|
|
||||||
'id': len(self.debug_messages[session_key]) + 1,
|
|
||||||
'type': 'bot',
|
|
||||||
'content': str(message),
|
|
||||||
'timestamp': datetime.now().isoformat(),
|
|
||||||
'message_chain': [component.__dict__ for component in message],
|
|
||||||
}
|
|
||||||
|
|
||||||
self.debug_messages[session_key].append(message_data)
|
|
||||||
|
|
||||||
await self.logger.info(f'Send message to {session_key}: {message}')
|
|
||||||
|
|
||||||
return message_data
|
|
||||||
|
|
||||||
async def reply_message(
|
|
||||||
self,
|
|
||||||
message_source: platform_events.MessageEvent,
|
|
||||||
message: platform_message.MessageChain,
|
|
||||||
quote_origin: bool = False,
|
|
||||||
) -> dict:
|
|
||||||
"""回复消息"""
|
|
||||||
message_data = WebChatMessage(
|
|
||||||
id=-1,
|
|
||||||
role='assistant',
|
|
||||||
content=str(message),
|
|
||||||
message_chain=[component.__dict__ for component in message],
|
|
||||||
timestamp=datetime.now().isoformat(),
|
|
||||||
)
|
|
||||||
|
|
||||||
# notify waiter
|
|
||||||
if isinstance(message_source, platform_events.FriendMessage):
|
|
||||||
await self.webchat_person_session.resp_queues[message_source.message_chain.message_id].put(message_data)
|
|
||||||
elif isinstance(message_source, platform_events.GroupMessage):
|
|
||||||
await self.webchat_group_session.resp_queues[message_source.message_chain.message_id].put(message_data)
|
|
||||||
|
|
||||||
return message_data.model_dump()
|
|
||||||
|
|
||||||
async def reply_message_chunk(
|
|
||||||
self,
|
|
||||||
message_source: platform_events.MessageEvent,
|
|
||||||
bot_message,
|
|
||||||
message: platform_message.MessageChain,
|
|
||||||
quote_origin: bool = False,
|
|
||||||
is_final: bool = False,
|
|
||||||
) -> dict:
|
|
||||||
"""回复消息"""
|
|
||||||
message_data = WebChatMessage(
|
|
||||||
id=-1,
|
|
||||||
role='assistant',
|
|
||||||
content=str(message),
|
|
||||||
message_chain=[component.__dict__ for component in message],
|
|
||||||
timestamp=datetime.now().isoformat(),
|
|
||||||
)
|
|
||||||
|
|
||||||
# notify waiter
|
|
||||||
session = (
|
|
||||||
self.webchat_group_session
|
|
||||||
if isinstance(message_source, platform_events.GroupMessage)
|
|
||||||
else self.webchat_person_session
|
|
||||||
)
|
|
||||||
if message_source.message_chain.message_id not in session.resp_waiters:
|
|
||||||
# session.resp_waiters[message_source.message_chain.message_id] = asyncio.Queue()
|
|
||||||
queue = session.resp_queues[message_source.message_chain.message_id]
|
|
||||||
|
|
||||||
# if isinstance(message_source, platform_events.FriendMessage):
|
|
||||||
# queue = self.webchat_person_session.resp_queues[message_source.message_chain.message_id]
|
|
||||||
# elif isinstance(message_source, platform_events.GroupMessage):
|
|
||||||
# queue = self.webchat_group_session.resp_queues[message_source.message_chain.message_id]
|
|
||||||
if is_final and bot_message.tool_calls is None:
|
|
||||||
message_data.is_final = True
|
|
||||||
# print(message_data)
|
|
||||||
await queue.put(message_data)
|
|
||||||
|
|
||||||
return message_data.model_dump()
|
|
||||||
|
|
||||||
async def is_stream_output_supported(self) -> bool:
|
|
||||||
return self.is_stream
|
|
||||||
|
|
||||||
def register_listener(
|
|
||||||
self,
|
|
||||||
event_type: typing.Type[platform_events.Event],
|
|
||||||
func: typing.Callable[
|
|
||||||
[platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], typing.Awaitable[None]
|
|
||||||
],
|
|
||||||
):
|
|
||||||
"""注册事件监听器"""
|
|
||||||
self.listeners[event_type] = func
|
|
||||||
|
|
||||||
def unregister_listener(
|
|
||||||
self,
|
|
||||||
event_type: typing.Type[platform_events.Event],
|
|
||||||
func: typing.Callable[
|
|
||||||
[platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], typing.Awaitable[None]
|
|
||||||
],
|
|
||||||
):
|
|
||||||
"""取消注册事件监听器"""
|
|
||||||
del self.listeners[event_type]
|
|
||||||
|
|
||||||
async def is_muted(self, group_id: int) -> bool:
|
|
||||||
return False
|
|
||||||
|
|
||||||
async def run_async(self):
|
|
||||||
"""运行适配器"""
|
|
||||||
await self.logger.info('WebChat调试适配器已启动')
|
|
||||||
|
|
||||||
try:
|
|
||||||
while True:
|
|
||||||
await asyncio.sleep(1)
|
|
||||||
except asyncio.CancelledError:
|
|
||||||
await self.logger.info('WebChat调试适配器已停止')
|
|
||||||
raise
|
|
||||||
|
|
||||||
async def kill(self):
|
|
||||||
"""停止适配器"""
|
|
||||||
await self.logger.info('WebChat调试适配器正在停止')
|
|
||||||
|
|
||||||
async def send_webchat_message(
|
|
||||||
self,
|
|
||||||
pipeline_uuid: str,
|
|
||||||
session_type: str,
|
|
||||||
message_chain_obj: typing.List[dict],
|
|
||||||
is_stream: bool = False,
|
|
||||||
) -> dict:
|
|
||||||
self.is_stream = is_stream
|
|
||||||
"""发送调试消息到流水线"""
|
|
||||||
if session_type == 'person':
|
|
||||||
use_session = self.webchat_person_session
|
|
||||||
else:
|
|
||||||
use_session = self.webchat_group_session
|
|
||||||
|
|
||||||
message_chain = platform_message.MessageChain.parse_obj(message_chain_obj)
|
|
||||||
|
|
||||||
message_id = len(use_session.get_message_list(pipeline_uuid)) + 1
|
|
||||||
|
|
||||||
use_session.resp_queues[message_id] = asyncio.Queue()
|
|
||||||
logger.debug(f'Initialized queue for message_id: {message_id}')
|
|
||||||
|
|
||||||
use_session.get_message_list(pipeline_uuid).append(
|
|
||||||
WebChatMessage(
|
|
||||||
id=message_id,
|
|
||||||
role='user',
|
|
||||||
content=str(message_chain),
|
|
||||||
message_chain=message_chain_obj,
|
|
||||||
timestamp=datetime.now().isoformat(),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
message_chain.insert(0, platform_message.Source(id=message_id, time=datetime.now().timestamp()))
|
|
||||||
|
|
||||||
if session_type == 'person':
|
|
||||||
sender = platform_entities.Friend(id='webchatperson', nickname='User', remark='User')
|
|
||||||
event = platform_events.FriendMessage(
|
|
||||||
sender=sender, message_chain=message_chain, time=datetime.now().timestamp()
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
group = platform_entities.Group(
|
|
||||||
id='webchatgroup', name='Group', permission=platform_entities.Permission.Member
|
|
||||||
)
|
|
||||||
sender = platform_entities.GroupMember(
|
|
||||||
id='webchatperson',
|
|
||||||
member_name='User',
|
|
||||||
group=group,
|
|
||||||
permission=platform_entities.Permission.Member,
|
|
||||||
)
|
|
||||||
event = platform_events.GroupMessage(
|
|
||||||
sender=sender, message_chain=message_chain, time=datetime.now().timestamp()
|
|
||||||
)
|
|
||||||
|
|
||||||
self.ap.platform_mgr.webchat_proxy_bot.bot_entity.use_pipeline_uuid = pipeline_uuid
|
|
||||||
|
|
||||||
# trigger pipeline
|
|
||||||
if event.__class__ in self.listeners:
|
|
||||||
await self.listeners[event.__class__](event, self)
|
|
||||||
|
|
||||||
if is_stream:
|
|
||||||
queue = use_session.resp_queues[message_id]
|
|
||||||
msg_id = len(use_session.get_message_list(pipeline_uuid)) + 1
|
|
||||||
while True:
|
|
||||||
resp_message = await queue.get()
|
|
||||||
resp_message.id = msg_id
|
|
||||||
if resp_message.is_final:
|
|
||||||
resp_message.id = msg_id
|
|
||||||
use_session.get_message_list(pipeline_uuid).append(resp_message)
|
|
||||||
yield resp_message.model_dump()
|
|
||||||
break
|
|
||||||
yield resp_message.model_dump()
|
|
||||||
use_session.resp_queues.pop(message_id)
|
|
||||||
|
|
||||||
else: # non-stream
|
|
||||||
# set waiter
|
|
||||||
# waiter = asyncio.Future[WebChatMessage]()
|
|
||||||
# use_session.resp_waiters[message_id] = waiter
|
|
||||||
# # waiter.add_done_callback(lambda future: use_session.resp_waiters.pop(message_id))
|
|
||||||
#
|
|
||||||
# resp_message = await waiter
|
|
||||||
#
|
|
||||||
# resp_message.id = len(use_session.get_message_list(pipeline_uuid)) + 1
|
|
||||||
#
|
|
||||||
# use_session.get_message_list(pipeline_uuid).append(resp_message)
|
|
||||||
#
|
|
||||||
# yield resp_message.model_dump()
|
|
||||||
msg_id = len(use_session.get_message_list(pipeline_uuid)) + 1
|
|
||||||
|
|
||||||
queue = use_session.resp_queues[message_id]
|
|
||||||
resp_message = await queue.get()
|
|
||||||
use_session.get_message_list(pipeline_uuid).append(resp_message)
|
|
||||||
resp_message.id = msg_id
|
|
||||||
resp_message.is_final = True
|
|
||||||
|
|
||||||
yield resp_message.model_dump()
|
|
||||||
|
|
||||||
def get_webchat_messages(self, pipeline_uuid: str, session_type: str) -> list[dict]:
|
|
||||||
"""获取调试消息历史"""
|
|
||||||
if session_type == 'person':
|
|
||||||
return [message.model_dump() for message in self.webchat_person_session.get_message_list(pipeline_uuid)]
|
|
||||||
else:
|
|
||||||
return [message.model_dump() for message in self.webchat_group_session.get_message_list(pipeline_uuid)]
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: MessagePlatformAdapter
|
|
||||||
metadata:
|
|
||||||
name: webchat
|
|
||||||
label:
|
|
||||||
en_US: "WebChat Debug"
|
|
||||||
zh_Hans: "网页聊天调试"
|
|
||||||
description:
|
|
||||||
en_US: "WebChat adapter for pipeline debugging"
|
|
||||||
zh_Hans: "用于流水线调试的网页聊天适配器"
|
|
||||||
icon: ""
|
|
||||||
spec:
|
|
||||||
config: []
|
|
||||||
execution:
|
|
||||||
python:
|
|
||||||
path: "webchat.py"
|
|
||||||
attr: "WebChatAdapter"
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: MessagePlatformAdapter
|
|
||||||
metadata:
|
|
||||||
name: wecom
|
|
||||||
label:
|
|
||||||
en_US: WeCom
|
|
||||||
zh_Hans: 企业微信
|
|
||||||
description:
|
|
||||||
en_US: WeCom Adapter
|
|
||||||
zh_Hans: 企业微信适配器,请查看文档了解使用方式
|
|
||||||
icon: wecom.png
|
|
||||||
spec:
|
|
||||||
config:
|
|
||||||
- name: host
|
|
||||||
label:
|
|
||||||
en_US: Host
|
|
||||||
zh_Hans: 监听主机
|
|
||||||
description:
|
|
||||||
en_US: Webhook host, unless you know what you're doing, please write 0.0.0.0
|
|
||||||
zh_Hans: Webhook 监听主机,除非你知道自己在做什么,否则请写 0.0.0.0
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: "0.0.0.0"
|
|
||||||
- name: port
|
|
||||||
label:
|
|
||||||
en_US: Port
|
|
||||||
zh_Hans: 监听端口
|
|
||||||
type: integer
|
|
||||||
required: true
|
|
||||||
default: 2290
|
|
||||||
- name: corpid
|
|
||||||
label:
|
|
||||||
en_US: Corpid
|
|
||||||
zh_Hans: 企业ID
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: ""
|
|
||||||
- name: secret
|
|
||||||
label:
|
|
||||||
en_US: Secret
|
|
||||||
zh_Hans: 密钥
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: ""
|
|
||||||
- name: token
|
|
||||||
label:
|
|
||||||
en_US: Token
|
|
||||||
zh_Hans: 令牌
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: ""
|
|
||||||
- name: EncodingAESKey
|
|
||||||
label:
|
|
||||||
en_US: EncodingAESKey
|
|
||||||
zh_Hans: 消息加解密密钥
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: ""
|
|
||||||
- name: contacts_secret
|
|
||||||
label:
|
|
||||||
en_US: Contacts Secret
|
|
||||||
zh_Hans: 通讯录密钥
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: ""
|
|
||||||
execution:
|
|
||||||
python:
|
|
||||||
path: ./wecom.py
|
|
||||||
attr: WecomAdapter
|
|
||||||
@@ -1,167 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
import typing
|
|
||||||
import asyncio
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
import datetime
|
|
||||||
import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter
|
|
||||||
import langbot_plugin.api.entities.builtin.platform.message as platform_message
|
|
||||||
import langbot_plugin.api.entities.builtin.platform.events as platform_events
|
|
||||||
import langbot_plugin.api.entities.builtin.platform.entities as platform_entities
|
|
||||||
import pydantic
|
|
||||||
from ..logger import EventLogger
|
|
||||||
from libs.wecom_ai_bot_api.wecombotevent import WecomBotEvent
|
|
||||||
from libs.wecom_ai_bot_api.api import WecomBotClient
|
|
||||||
from ...core import app
|
|
||||||
|
|
||||||
class WecomBotMessageConverter(abstract_platform_adapter.AbstractMessageConverter):
|
|
||||||
@staticmethod
|
|
||||||
async def yiri2target(message_chain: platform_message.MessageChain):
|
|
||||||
content = ''
|
|
||||||
for msg in message_chain:
|
|
||||||
if type(msg) is platform_message.Plain:
|
|
||||||
content += msg.text
|
|
||||||
return content
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def target2yiri(event: WecomBotEvent):
|
|
||||||
yiri_msg_list = []
|
|
||||||
if event.type == 'group':
|
|
||||||
yiri_msg_list.append(platform_message.At(target=event.ai_bot_id))
|
|
||||||
yiri_msg_list.append(platform_message.Source(id=event.message_id, time=datetime.datetime.now()))
|
|
||||||
yiri_msg_list.append(platform_message.Plain(text=event.content))
|
|
||||||
if event.picurl != '':
|
|
||||||
yiri_msg_list.append(platform_message.Image(base64=event.picurl))
|
|
||||||
chain = platform_message.MessageChain(yiri_msg_list)
|
|
||||||
|
|
||||||
return chain
|
|
||||||
|
|
||||||
class WecomBotEventConverter(abstract_platform_adapter.AbstractEventConverter):
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def yiri2target(event:platform_events.MessageEvent):
|
|
||||||
return event.source_platform_object
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def target2yiri(event:WecomBotEvent):
|
|
||||||
message_chain = await WecomBotMessageConverter.target2yiri(event)
|
|
||||||
if event.type == 'single':
|
|
||||||
return platform_events.FriendMessage(
|
|
||||||
sender=platform_entities.Friend(
|
|
||||||
id=event.userid,
|
|
||||||
nickname='',
|
|
||||||
remark='',
|
|
||||||
),
|
|
||||||
message_chain=message_chain,
|
|
||||||
time=datetime.datetime.now().timestamp(),
|
|
||||||
source_platform_object=event,
|
|
||||||
)
|
|
||||||
elif event.type == 'group':
|
|
||||||
try:
|
|
||||||
sender = platform_entities.GroupMember(
|
|
||||||
id=event.userid,
|
|
||||||
permission='MEMBER',
|
|
||||||
member_name=event.userid,
|
|
||||||
group=platform_entities.Group(
|
|
||||||
id=str(event.chatid),
|
|
||||||
name='',
|
|
||||||
permission=platform_entities.Permission.Member,
|
|
||||||
),
|
|
||||||
special_title='',
|
|
||||||
join_timestamp=0,
|
|
||||||
last_speak_timestamp=0,
|
|
||||||
mute_time_remaining=0,
|
|
||||||
)
|
|
||||||
time = datetime.datetime.now().timestamp()
|
|
||||||
return platform_events.GroupMessage(
|
|
||||||
sender=sender,
|
|
||||||
message_chain=message_chain,
|
|
||||||
time=time,
|
|
||||||
source_platform_object=event,
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
print(traceback.format_exc())
|
|
||||||
|
|
||||||
class WecomBotAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|
||||||
bot: WecomBotClient
|
|
||||||
bot_account_id: str
|
|
||||||
message_converter: WecomBotMessageConverter = WecomBotMessageConverter()
|
|
||||||
event_converter: WecomBotEventConverter = WecomBotEventConverter()
|
|
||||||
config: dict
|
|
||||||
|
|
||||||
def __init__(self, config: dict, logger: EventLogger):
|
|
||||||
required_keys = ['Token', 'EncodingAESKey', 'Corpid', 'BotId', 'port']
|
|
||||||
missing_keys = [key for key in required_keys if key not in config]
|
|
||||||
if missing_keys:
|
|
||||||
raise Exception(f'WecomBot 缺少配置项: {missing_keys}')
|
|
||||||
|
|
||||||
# 创建运行时 bot 对象
|
|
||||||
bot = WecomBotClient(
|
|
||||||
Token=config['Token'],
|
|
||||||
EnCodingAESKey=config['EncodingAESKey'],
|
|
||||||
Corpid=config['Corpid'],
|
|
||||||
logger=logger,
|
|
||||||
)
|
|
||||||
bot_account_id = config['BotId']
|
|
||||||
|
|
||||||
super().__init__(
|
|
||||||
config=config,
|
|
||||||
logger=logger,
|
|
||||||
bot=bot,
|
|
||||||
bot_account_id=bot_account_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def reply_message(self, message_source:platform_events.MessageEvent, message:platform_message.MessageChain,quote_origin: bool = False):
|
|
||||||
|
|
||||||
content = await self.message_converter.yiri2target(message)
|
|
||||||
await self.bot.set_message(message_source.source_platform_object.message_id, content)
|
|
||||||
|
|
||||||
async def send_message(self, target_type, target_id, message):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def register_listener(
|
|
||||||
self,
|
|
||||||
event_type: typing.Type[platform_events.Event],
|
|
||||||
callback: typing.Callable[[platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None],
|
|
||||||
):
|
|
||||||
async def on_message(event: WecomBotEvent):
|
|
||||||
try:
|
|
||||||
return await callback(await self.event_converter.target2yiri(event), self)
|
|
||||||
except Exception:
|
|
||||||
await self.logger.error(f'Error in wecombot callback: {traceback.format_exc()}')
|
|
||||||
print(traceback.format_exc())
|
|
||||||
try:
|
|
||||||
if event_type == platform_events.FriendMessage:
|
|
||||||
self.bot.on_message('single')(on_message)
|
|
||||||
elif event_type == platform_events.GroupMessage:
|
|
||||||
self.bot.on_message('group')(on_message)
|
|
||||||
except Exception:
|
|
||||||
print(traceback.format_exc())
|
|
||||||
|
|
||||||
|
|
||||||
async def run_async(self):
|
|
||||||
async def shutdown_trigger_placeholder():
|
|
||||||
while True:
|
|
||||||
await asyncio.sleep(1)
|
|
||||||
|
|
||||||
await self.bot.run_task(
|
|
||||||
host='0.0.0.0',
|
|
||||||
port=self.config['port'],
|
|
||||||
shutdown_trigger=shutdown_trigger_placeholder,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def kill(self) -> bool:
|
|
||||||
return False
|
|
||||||
|
|
||||||
async def unregister_listener(
|
|
||||||
self,
|
|
||||||
event_type: type,
|
|
||||||
callback: typing.Callable[[platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None],
|
|
||||||
):
|
|
||||||
return super().unregister_listener(event_type, callback)
|
|
||||||
|
|
||||||
async def is_muted(self, group_id: int) -> bool:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: MessagePlatformAdapter
|
|
||||||
metadata:
|
|
||||||
name: wecombot
|
|
||||||
label:
|
|
||||||
en_US: WeComBot
|
|
||||||
zh_Hans: 企业微信智能机器人
|
|
||||||
description:
|
|
||||||
en_US: WeComBot Adapter
|
|
||||||
zh_Hans: 企业微信智能机器人适配器,请查看文档了解使用方式
|
|
||||||
icon: wecombot.png
|
|
||||||
spec:
|
|
||||||
config:
|
|
||||||
- name: port
|
|
||||||
label:
|
|
||||||
en_US: Port
|
|
||||||
zh_Hans: 监听端口
|
|
||||||
type: integer
|
|
||||||
required: true
|
|
||||||
default: 2291
|
|
||||||
- name: Corpid
|
|
||||||
label:
|
|
||||||
en_US: Corpid
|
|
||||||
zh_Hans: 企业ID
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: ""
|
|
||||||
- name: Token
|
|
||||||
label:
|
|
||||||
en_US: Token
|
|
||||||
zh_Hans: 令牌
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: ""
|
|
||||||
- name: EncodingAESKey
|
|
||||||
label:
|
|
||||||
en_US: EncodingAESKey
|
|
||||||
zh_Hans: 消息加解密密钥
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: ""
|
|
||||||
- name: BotId
|
|
||||||
label:
|
|
||||||
en_US: BotId
|
|
||||||
zh_Hans: 机器人ID
|
|
||||||
type: string
|
|
||||||
required: false
|
|
||||||
default: ""
|
|
||||||
execution:
|
|
||||||
python:
|
|
||||||
path: ./wecombot.py
|
|
||||||
attr: WecomBotAdapter
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: MessagePlatformAdapter
|
|
||||||
metadata:
|
|
||||||
name: wecomcs
|
|
||||||
label:
|
|
||||||
en_US: WeComCustomerService
|
|
||||||
zh_Hans: 企业微信客服
|
|
||||||
description:
|
|
||||||
en_US: WeComCSAdapter
|
|
||||||
zh_Hans: 企业微信客服适配器
|
|
||||||
icon: wecom.png
|
|
||||||
spec:
|
|
||||||
config:
|
|
||||||
- name: port
|
|
||||||
label:
|
|
||||||
en_US: Port
|
|
||||||
zh_Hans: 监听端口
|
|
||||||
type: int
|
|
||||||
required: true
|
|
||||||
default: 2289
|
|
||||||
- name: corpid
|
|
||||||
label:
|
|
||||||
en_US: Corpid
|
|
||||||
zh_Hans: 企业ID
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: ""
|
|
||||||
- name: secret
|
|
||||||
label:
|
|
||||||
en_US: Secret
|
|
||||||
zh_Hans: 密钥
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: ""
|
|
||||||
- name: token
|
|
||||||
label:
|
|
||||||
en_US: Token
|
|
||||||
zh_Hans: 令牌
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: ""
|
|
||||||
- name: EncodingAESKey
|
|
||||||
label:
|
|
||||||
en_US: EncodingAESKey
|
|
||||||
zh_Hans: 消息加解密密钥
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: ""
|
|
||||||
execution:
|
|
||||||
python:
|
|
||||||
path: ./wecomcs.py
|
|
||||||
attr: WecomCSAdapter
|
|
||||||
@@ -1,250 +0,0 @@
|
|||||||
# For connect to plugin runtime.
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
from typing import Any
|
|
||||||
import typing
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from async_lru import alru_cache
|
|
||||||
|
|
||||||
from ..core import app
|
|
||||||
from . import handler
|
|
||||||
from ..utils import platform
|
|
||||||
from langbot_plugin.runtime.io.controllers.stdio import client as stdio_client_controller
|
|
||||||
from langbot_plugin.runtime.io.controllers.ws import client as ws_client_controller
|
|
||||||
from langbot_plugin.api.entities import events
|
|
||||||
from langbot_plugin.api.entities import context
|
|
||||||
import langbot_plugin.runtime.io.connection as base_connection
|
|
||||||
from langbot_plugin.api.definition.components.manifest import ComponentManifest
|
|
||||||
from langbot_plugin.api.entities.builtin.command import context as command_context
|
|
||||||
from langbot_plugin.runtime.plugin.mgr import PluginInstallSource
|
|
||||||
from ..core import taskmgr
|
|
||||||
|
|
||||||
|
|
||||||
class PluginRuntimeConnector:
|
|
||||||
"""Plugin runtime connector"""
|
|
||||||
|
|
||||||
ap: app.Application
|
|
||||||
|
|
||||||
handler: handler.RuntimeConnectionHandler
|
|
||||||
|
|
||||||
handler_task: asyncio.Task
|
|
||||||
|
|
||||||
heartbeat_task: asyncio.Task | None = None
|
|
||||||
|
|
||||||
stdio_client_controller: stdio_client_controller.StdioClientController
|
|
||||||
|
|
||||||
ctrl: stdio_client_controller.StdioClientController | ws_client_controller.WebSocketClientController
|
|
||||||
|
|
||||||
runtime_disconnect_callback: typing.Callable[
|
|
||||||
[PluginRuntimeConnector], typing.Coroutine[typing.Any, typing.Any, None]
|
|
||||||
]
|
|
||||||
|
|
||||||
is_enable_plugin: bool = True
|
|
||||||
"""Mark if the plugin system is enabled"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
ap: app.Application,
|
|
||||||
runtime_disconnect_callback: typing.Callable[
|
|
||||||
[PluginRuntimeConnector], typing.Coroutine[typing.Any, typing.Any, None]
|
|
||||||
],
|
|
||||||
):
|
|
||||||
self.ap = ap
|
|
||||||
self.runtime_disconnect_callback = runtime_disconnect_callback
|
|
||||||
self.is_enable_plugin = self.ap.instance_config.data.get('plugin', {}).get('enable', True)
|
|
||||||
|
|
||||||
async def heartbeat_loop(self):
|
|
||||||
while True:
|
|
||||||
await asyncio.sleep(10)
|
|
||||||
try:
|
|
||||||
await self.ping_plugin_runtime()
|
|
||||||
self.ap.logger.debug('Heartbeat to plugin runtime success.')
|
|
||||||
except Exception as e:
|
|
||||||
self.ap.logger.debug(f'Failed to heartbeat to plugin runtime: {e}')
|
|
||||||
|
|
||||||
async def initialize(self):
|
|
||||||
if not self.is_enable_plugin:
|
|
||||||
self.ap.logger.info('Plugin system is disabled.')
|
|
||||||
return
|
|
||||||
|
|
||||||
async def new_connection_callback(connection: base_connection.Connection):
|
|
||||||
async def disconnect_callback(rchandler: handler.RuntimeConnectionHandler) -> bool:
|
|
||||||
if platform.get_platform() == 'docker' or platform.use_websocket_to_connect_plugin_runtime():
|
|
||||||
self.ap.logger.error('Disconnected from plugin runtime, trying to reconnect...')
|
|
||||||
await self.runtime_disconnect_callback(self)
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
self.ap.logger.error(
|
|
||||||
'Disconnected from plugin runtime, cannot automatically reconnect while LangBot connects to plugin runtime via stdio, please restart LangBot.'
|
|
||||||
)
|
|
||||||
return False
|
|
||||||
|
|
||||||
self.handler = handler.RuntimeConnectionHandler(connection, disconnect_callback, self.ap)
|
|
||||||
|
|
||||||
self.handler_task = asyncio.create_task(self.handler.run())
|
|
||||||
_ = await self.handler.ping()
|
|
||||||
self.ap.logger.info('Connected to plugin runtime.')
|
|
||||||
await self.handler_task
|
|
||||||
|
|
||||||
task: asyncio.Task | None = None
|
|
||||||
|
|
||||||
if platform.get_platform() == 'docker' or platform.use_websocket_to_connect_plugin_runtime(): # use websocket
|
|
||||||
self.ap.logger.info('use websocket to connect to plugin runtime')
|
|
||||||
ws_url = self.ap.instance_config.data.get('plugin', {}).get(
|
|
||||||
'runtime_ws_url', 'ws://langbot_plugin_runtime:5400/control/ws'
|
|
||||||
)
|
|
||||||
|
|
||||||
async def make_connection_failed_callback(
|
|
||||||
ctrl: ws_client_controller.WebSocketClientController, exc: Exception = None
|
|
||||||
) -> None:
|
|
||||||
if exc is not None:
|
|
||||||
self.ap.logger.error(f'Failed to connect to plugin runtime({ws_url}): {exc}')
|
|
||||||
else:
|
|
||||||
self.ap.logger.error(f'Failed to connect to plugin runtime({ws_url}), trying to reconnect...')
|
|
||||||
await self.runtime_disconnect_callback(self)
|
|
||||||
|
|
||||||
self.ctrl = ws_client_controller.WebSocketClientController(
|
|
||||||
ws_url=ws_url,
|
|
||||||
make_connection_failed_callback=make_connection_failed_callback,
|
|
||||||
)
|
|
||||||
task = self.ctrl.run(new_connection_callback)
|
|
||||||
else: # stdio
|
|
||||||
self.ap.logger.info('use stdio to connect to plugin runtime')
|
|
||||||
# cmd: lbp rt -s
|
|
||||||
python_path = sys.executable
|
|
||||||
env = os.environ.copy()
|
|
||||||
self.ctrl = stdio_client_controller.StdioClientController(
|
|
||||||
command=python_path,
|
|
||||||
args=['-m', 'langbot_plugin.cli.__init__', 'rt', '-s'],
|
|
||||||
env=env,
|
|
||||||
)
|
|
||||||
task = self.ctrl.run(new_connection_callback)
|
|
||||||
|
|
||||||
if self.heartbeat_task is None:
|
|
||||||
self.heartbeat_task = asyncio.create_task(self.heartbeat_loop())
|
|
||||||
|
|
||||||
asyncio.create_task(task)
|
|
||||||
|
|
||||||
async def initialize_plugins(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def ping_plugin_runtime(self):
|
|
||||||
if not hasattr(self, 'handler'):
|
|
||||||
raise Exception('Plugin runtime is not connected')
|
|
||||||
|
|
||||||
return await self.handler.ping()
|
|
||||||
|
|
||||||
async def install_plugin(
|
|
||||||
self,
|
|
||||||
install_source: PluginInstallSource,
|
|
||||||
install_info: dict[str, Any],
|
|
||||||
task_context: taskmgr.TaskContext | None = None,
|
|
||||||
):
|
|
||||||
if install_source == PluginInstallSource.LOCAL:
|
|
||||||
# transfer file before install
|
|
||||||
file_bytes = install_info['plugin_file']
|
|
||||||
file_key = await self.handler.send_file(file_bytes, 'lbpkg')
|
|
||||||
install_info['plugin_file_key'] = file_key
|
|
||||||
del install_info['plugin_file']
|
|
||||||
self.ap.logger.info(f'Transfered file {file_key} to plugin runtime')
|
|
||||||
|
|
||||||
async for ret in self.handler.install_plugin(install_source.value, install_info):
|
|
||||||
current_action = ret.get('current_action', None)
|
|
||||||
if current_action is not None:
|
|
||||||
if task_context is not None:
|
|
||||||
task_context.set_current_action(current_action)
|
|
||||||
|
|
||||||
trace = ret.get('trace', None)
|
|
||||||
if trace is not None:
|
|
||||||
if task_context is not None:
|
|
||||||
task_context.trace(trace)
|
|
||||||
|
|
||||||
async def upgrade_plugin(
|
|
||||||
self, plugin_author: str, plugin_name: str, task_context: taskmgr.TaskContext | None = None
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
async for ret in self.handler.upgrade_plugin(plugin_author, plugin_name):
|
|
||||||
current_action = ret.get('current_action', None)
|
|
||||||
if current_action is not None:
|
|
||||||
if task_context is not None:
|
|
||||||
task_context.set_current_action(current_action)
|
|
||||||
|
|
||||||
trace = ret.get('trace', None)
|
|
||||||
if trace is not None:
|
|
||||||
if task_context is not None:
|
|
||||||
task_context.trace(trace)
|
|
||||||
|
|
||||||
async def delete_plugin(
|
|
||||||
self, plugin_author: str, plugin_name: str, task_context: taskmgr.TaskContext | None = None
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
async for ret in self.handler.delete_plugin(plugin_author, plugin_name):
|
|
||||||
current_action = ret.get('current_action', None)
|
|
||||||
if current_action is not None:
|
|
||||||
if task_context is not None:
|
|
||||||
task_context.set_current_action(current_action)
|
|
||||||
|
|
||||||
trace = ret.get('trace', None)
|
|
||||||
if trace is not None:
|
|
||||||
if task_context is not None:
|
|
||||||
task_context.trace(trace)
|
|
||||||
|
|
||||||
async def list_plugins(self) -> list[dict[str, Any]]:
|
|
||||||
return await self.handler.list_plugins()
|
|
||||||
|
|
||||||
async def get_plugin_info(self, author: str, plugin_name: str) -> dict[str, Any]:
|
|
||||||
return await self.handler.get_plugin_info(author, plugin_name)
|
|
||||||
|
|
||||||
async def set_plugin_config(self, plugin_author: str, plugin_name: str, config: dict[str, Any]) -> dict[str, Any]:
|
|
||||||
return await self.handler.set_plugin_config(plugin_author, plugin_name, config)
|
|
||||||
|
|
||||||
@alru_cache(ttl=5 * 60) # 5 minutes
|
|
||||||
async def get_plugin_icon(self, plugin_author: str, plugin_name: str) -> dict[str, Any]:
|
|
||||||
return await self.handler.get_plugin_icon(plugin_author, plugin_name)
|
|
||||||
|
|
||||||
async def emit_event(
|
|
||||||
self,
|
|
||||||
event: events.BaseEventModel,
|
|
||||||
) -> context.EventContext:
|
|
||||||
event_ctx = context.EventContext.from_event(event)
|
|
||||||
|
|
||||||
if not self.is_enable_plugin:
|
|
||||||
return event_ctx
|
|
||||||
event_ctx_result = await self.handler.emit_event(event_ctx.model_dump(serialize_as_any=True))
|
|
||||||
|
|
||||||
event_ctx = context.EventContext.model_validate(event_ctx_result['event_context'])
|
|
||||||
|
|
||||||
return event_ctx
|
|
||||||
|
|
||||||
async def list_tools(self) -> list[ComponentManifest]:
|
|
||||||
list_tools_data = await self.handler.list_tools()
|
|
||||||
|
|
||||||
return [ComponentManifest.model_validate(tool) for tool in list_tools_data]
|
|
||||||
|
|
||||||
async def call_tool(self, tool_name: str, parameters: dict[str, Any]) -> dict[str, Any]:
|
|
||||||
return await self.handler.call_tool(tool_name, parameters)
|
|
||||||
|
|
||||||
async def list_commands(self) -> list[ComponentManifest]:
|
|
||||||
list_commands_data = await self.handler.list_commands()
|
|
||||||
|
|
||||||
return [ComponentManifest.model_validate(command) for command in list_commands_data]
|
|
||||||
|
|
||||||
async def execute_command(
|
|
||||||
self, command_ctx: command_context.ExecuteContext
|
|
||||||
) -> typing.AsyncGenerator[command_context.CommandReturn, None]:
|
|
||||||
gen = self.handler.execute_command(command_ctx.model_dump(serialize_as_any=True))
|
|
||||||
|
|
||||||
async for ret in gen:
|
|
||||||
cmd_ret = command_context.CommandReturn.model_validate(ret)
|
|
||||||
|
|
||||||
yield cmd_ret
|
|
||||||
|
|
||||||
def dispose(self):
|
|
||||||
if self.is_enable_plugin and isinstance(self.ctrl, stdio_client_controller.StdioClientController):
|
|
||||||
self.ap.logger.info('Terminating plugin runtime process...')
|
|
||||||
self.ctrl.process.terminate()
|
|
||||||
|
|
||||||
if self.heartbeat_task is not None:
|
|
||||||
self.heartbeat_task.cancel()
|
|
||||||
self.heartbeat_task = None
|
|
||||||
@@ -1,609 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import typing
|
|
||||||
from typing import Any
|
|
||||||
import base64
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
import sqlalchemy
|
|
||||||
|
|
||||||
from langbot_plugin.runtime.io import handler
|
|
||||||
from langbot_plugin.runtime.io.connection import Connection
|
|
||||||
from langbot_plugin.entities.io.actions.enums import (
|
|
||||||
CommonAction,
|
|
||||||
RuntimeToLangBotAction,
|
|
||||||
LangBotToRuntimeAction,
|
|
||||||
PluginToRuntimeAction,
|
|
||||||
)
|
|
||||||
import langbot_plugin.api.entities.builtin.platform.message as platform_message
|
|
||||||
import langbot_plugin.api.entities.builtin.provider.message as provider_message
|
|
||||||
import langbot_plugin.api.entities.builtin.resource.tool as resource_tool
|
|
||||||
|
|
||||||
from ..entity.persistence import plugin as persistence_plugin
|
|
||||||
from ..entity.persistence import bstorage as persistence_bstorage
|
|
||||||
|
|
||||||
from ..core import app
|
|
||||||
from ..utils import constants
|
|
||||||
|
|
||||||
|
|
||||||
class RuntimeConnectionHandler(handler.Handler):
|
|
||||||
"""Runtime connection handler"""
|
|
||||||
|
|
||||||
ap: app.Application
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
connection: Connection,
|
|
||||||
disconnect_callback: typing.Callable[[], typing.Coroutine[typing.Any, typing.Any, bool]],
|
|
||||||
ap: app.Application,
|
|
||||||
):
|
|
||||||
super().__init__(connection, disconnect_callback)
|
|
||||||
self.ap = ap
|
|
||||||
|
|
||||||
@self.action(RuntimeToLangBotAction.INITIALIZE_PLUGIN_SETTINGS)
|
|
||||||
async def initialize_plugin_settings(data: dict[str, Any]) -> handler.ActionResponse:
|
|
||||||
"""Initialize plugin settings"""
|
|
||||||
# check if exists plugin setting
|
|
||||||
plugin_author = data['plugin_author']
|
|
||||||
plugin_name = data['plugin_name']
|
|
||||||
install_source = data['install_source']
|
|
||||||
install_info = data['install_info']
|
|
||||||
|
|
||||||
try:
|
|
||||||
result = await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.select(persistence_plugin.PluginSetting)
|
|
||||||
.where(persistence_plugin.PluginSetting.plugin_author == plugin_author)
|
|
||||||
.where(persistence_plugin.PluginSetting.plugin_name == plugin_name)
|
|
||||||
)
|
|
||||||
|
|
||||||
if result.first() is not None:
|
|
||||||
# delete plugin setting
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.delete(persistence_plugin.PluginSetting)
|
|
||||||
.where(persistence_plugin.PluginSetting.plugin_author == plugin_author)
|
|
||||||
.where(persistence_plugin.PluginSetting.plugin_name == plugin_name)
|
|
||||||
)
|
|
||||||
|
|
||||||
# create plugin setting
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.insert(persistence_plugin.PluginSetting).values(
|
|
||||||
plugin_author=plugin_author,
|
|
||||||
plugin_name=plugin_name,
|
|
||||||
install_source=install_source,
|
|
||||||
install_info=install_info,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return handler.ActionResponse.success(
|
|
||||||
data={},
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
traceback.print_exc()
|
|
||||||
return handler.ActionResponse.error(
|
|
||||||
message=f'Failed to initialize plugin settings: {e}',
|
|
||||||
)
|
|
||||||
|
|
||||||
@self.action(RuntimeToLangBotAction.GET_PLUGIN_SETTINGS)
|
|
||||||
async def get_plugin_settings(data: dict[str, Any]) -> handler.ActionResponse:
|
|
||||||
"""Get plugin settings"""
|
|
||||||
|
|
||||||
plugin_author = data['plugin_author']
|
|
||||||
plugin_name = data['plugin_name']
|
|
||||||
|
|
||||||
result = await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.select(persistence_plugin.PluginSetting)
|
|
||||||
.where(persistence_plugin.PluginSetting.plugin_author == plugin_author)
|
|
||||||
.where(persistence_plugin.PluginSetting.plugin_name == plugin_name)
|
|
||||||
)
|
|
||||||
|
|
||||||
data = {
|
|
||||||
'enabled': True,
|
|
||||||
'priority': 0,
|
|
||||||
'plugin_config': {},
|
|
||||||
'install_source': 'local',
|
|
||||||
'install_info': {},
|
|
||||||
}
|
|
||||||
|
|
||||||
setting = result.first()
|
|
||||||
|
|
||||||
if setting is not None:
|
|
||||||
data['enabled'] = setting.enabled
|
|
||||||
data['priority'] = setting.priority
|
|
||||||
data['plugin_config'] = setting.config
|
|
||||||
data['install_source'] = setting.install_source
|
|
||||||
data['install_info'] = setting.install_info
|
|
||||||
|
|
||||||
return handler.ActionResponse.success(
|
|
||||||
data=data,
|
|
||||||
)
|
|
||||||
|
|
||||||
@self.action(PluginToRuntimeAction.REPLY_MESSAGE)
|
|
||||||
async def reply_message(data: dict[str, Any]) -> handler.ActionResponse:
|
|
||||||
"""Reply message"""
|
|
||||||
query_id = data['query_id']
|
|
||||||
message_chain = data['message_chain']
|
|
||||||
quote_origin = data['quote_origin']
|
|
||||||
|
|
||||||
if query_id not in self.ap.query_pool.cached_queries:
|
|
||||||
return handler.ActionResponse.error(
|
|
||||||
message=f'Query with query_id {query_id} not found',
|
|
||||||
)
|
|
||||||
|
|
||||||
query = self.ap.query_pool.cached_queries[query_id]
|
|
||||||
|
|
||||||
message_chain_obj = platform_message.MessageChain.model_validate(message_chain)
|
|
||||||
|
|
||||||
await query.adapter.reply_message(
|
|
||||||
query.message_event,
|
|
||||||
message_chain_obj,
|
|
||||||
quote_origin,
|
|
||||||
)
|
|
||||||
|
|
||||||
return handler.ActionResponse.success(
|
|
||||||
data={},
|
|
||||||
)
|
|
||||||
|
|
||||||
@self.action(PluginToRuntimeAction.GET_BOT_UUID)
|
|
||||||
async def get_bot_uuid(data: dict[str, Any]) -> handler.ActionResponse:
|
|
||||||
"""Get bot uuid"""
|
|
||||||
query_id = data['query_id']
|
|
||||||
if query_id not in self.ap.query_pool.cached_queries:
|
|
||||||
return handler.ActionResponse.error(
|
|
||||||
message=f'Query with query_id {query_id} not found',
|
|
||||||
)
|
|
||||||
|
|
||||||
query = self.ap.query_pool.cached_queries[query_id]
|
|
||||||
|
|
||||||
return handler.ActionResponse.success(
|
|
||||||
data={
|
|
||||||
'bot_uuid': query.bot_uuid,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
@self.action(PluginToRuntimeAction.SET_QUERY_VAR)
|
|
||||||
async def set_query_var(data: dict[str, Any]) -> handler.ActionResponse:
|
|
||||||
"""Set query var"""
|
|
||||||
query_id = data['query_id']
|
|
||||||
key = data['key']
|
|
||||||
value = data['value']
|
|
||||||
|
|
||||||
if query_id not in self.ap.query_pool.cached_queries:
|
|
||||||
return handler.ActionResponse.error(
|
|
||||||
message=f'Query with query_id {query_id} not found',
|
|
||||||
)
|
|
||||||
|
|
||||||
query = self.ap.query_pool.cached_queries[query_id]
|
|
||||||
|
|
||||||
query.variables[key] = value
|
|
||||||
|
|
||||||
return handler.ActionResponse.success(
|
|
||||||
data={},
|
|
||||||
)
|
|
||||||
|
|
||||||
@self.action(PluginToRuntimeAction.GET_QUERY_VAR)
|
|
||||||
async def get_query_var(data: dict[str, Any]) -> handler.ActionResponse:
|
|
||||||
"""Get query var"""
|
|
||||||
query_id = data['query_id']
|
|
||||||
key = data['key']
|
|
||||||
|
|
||||||
if query_id not in self.ap.query_pool.cached_queries:
|
|
||||||
return handler.ActionResponse.error(
|
|
||||||
message=f'Query with query_id {query_id} not found',
|
|
||||||
)
|
|
||||||
|
|
||||||
query = self.ap.query_pool.cached_queries[query_id]
|
|
||||||
|
|
||||||
return handler.ActionResponse.success(
|
|
||||||
data={
|
|
||||||
'value': query.variables[key],
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
@self.action(PluginToRuntimeAction.GET_QUERY_VARS)
|
|
||||||
async def get_query_vars(data: dict[str, Any]) -> handler.ActionResponse:
|
|
||||||
"""Get query vars"""
|
|
||||||
query_id = data['query_id']
|
|
||||||
if query_id not in self.ap.query_pool.cached_queries:
|
|
||||||
return handler.ActionResponse.error(
|
|
||||||
message=f'Query with query_id {query_id} not found',
|
|
||||||
)
|
|
||||||
|
|
||||||
query = self.ap.query_pool.cached_queries[query_id]
|
|
||||||
|
|
||||||
return handler.ActionResponse.success(
|
|
||||||
data={
|
|
||||||
'vars': query.variables,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
@self.action(PluginToRuntimeAction.CREATE_NEW_CONVERSATION)
|
|
||||||
async def create_new_conversation(data: dict[str, Any]) -> handler.ActionResponse:
|
|
||||||
"""Create new conversation"""
|
|
||||||
query_id = data['query_id']
|
|
||||||
if query_id not in self.ap.query_pool.cached_queries:
|
|
||||||
return handler.ActionResponse.error(
|
|
||||||
message=f'Query with query_id {query_id} not found',
|
|
||||||
)
|
|
||||||
|
|
||||||
query = self.ap.query_pool.cached_queries[query_id]
|
|
||||||
|
|
||||||
query.session.using_conversation = None
|
|
||||||
|
|
||||||
return handler.ActionResponse.success(
|
|
||||||
data={},
|
|
||||||
)
|
|
||||||
|
|
||||||
@self.action(PluginToRuntimeAction.GET_LANGBOT_VERSION)
|
|
||||||
async def get_langbot_version(data: dict[str, Any]) -> handler.ActionResponse:
|
|
||||||
"""Get langbot version"""
|
|
||||||
return handler.ActionResponse.success(
|
|
||||||
data={
|
|
||||||
'version': constants.semantic_version,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
@self.action(PluginToRuntimeAction.GET_BOTS)
|
|
||||||
async def get_bots(data: dict[str, Any]) -> handler.ActionResponse:
|
|
||||||
"""Get bots"""
|
|
||||||
bots = await self.ap.bot_service.get_bots(include_secret=False)
|
|
||||||
return handler.ActionResponse.success(
|
|
||||||
data={
|
|
||||||
'bots': bots,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
@self.action(PluginToRuntimeAction.GET_BOT_INFO)
|
|
||||||
async def get_bot_info(data: dict[str, Any]) -> handler.ActionResponse:
|
|
||||||
"""Get bot info"""
|
|
||||||
bot_uuid = data['bot_uuid']
|
|
||||||
bot = await self.ap.bot_service.get_runtime_bot_info(bot_uuid, include_secret=False)
|
|
||||||
return handler.ActionResponse.success(
|
|
||||||
data={
|
|
||||||
'bot': bot,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
@self.action(PluginToRuntimeAction.SEND_MESSAGE)
|
|
||||||
async def send_message(data: dict[str, Any]) -> handler.ActionResponse:
|
|
||||||
"""Send message"""
|
|
||||||
bot_uuid = data['bot_uuid']
|
|
||||||
target_type = data['target_type']
|
|
||||||
target_id = data['target_id']
|
|
||||||
message_chain = data['message_chain']
|
|
||||||
|
|
||||||
message_chain_obj = platform_message.MessageChain.model_validate(message_chain)
|
|
||||||
|
|
||||||
bot = await self.ap.platform_mgr.get_bot_by_uuid(bot_uuid)
|
|
||||||
if bot is None:
|
|
||||||
return handler.ActionResponse.error(
|
|
||||||
message=f'Bot with bot_uuid {bot_uuid} not found',
|
|
||||||
)
|
|
||||||
|
|
||||||
await bot.adapter.send_message(
|
|
||||||
target_type,
|
|
||||||
target_id,
|
|
||||||
message_chain_obj,
|
|
||||||
)
|
|
||||||
|
|
||||||
return handler.ActionResponse.success(
|
|
||||||
data={},
|
|
||||||
)
|
|
||||||
|
|
||||||
@self.action(PluginToRuntimeAction.GET_LLM_MODELS)
|
|
||||||
async def get_llm_models(data: dict[str, Any]) -> handler.ActionResponse:
|
|
||||||
"""Get llm models"""
|
|
||||||
llm_models = await self.ap.model_service.get_llm_models(include_secret=False)
|
|
||||||
return handler.ActionResponse.success(
|
|
||||||
data={
|
|
||||||
'llm_models': llm_models,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
@self.action(PluginToRuntimeAction.INVOKE_LLM)
|
|
||||||
async def invoke_llm(data: dict[str, Any]) -> handler.ActionResponse:
|
|
||||||
"""Invoke llm"""
|
|
||||||
llm_model_uuid = data['llm_model_uuid']
|
|
||||||
messages = data['messages']
|
|
||||||
funcs = data.get('funcs', [])
|
|
||||||
extra_args = data.get('extra_args', {})
|
|
||||||
|
|
||||||
llm_model = await self.ap.model_mgr.get_model_by_uuid(llm_model_uuid)
|
|
||||||
if llm_model is None:
|
|
||||||
return handler.ActionResponse.error(
|
|
||||||
message=f'LLM model with llm_model_uuid {llm_model_uuid} not found',
|
|
||||||
)
|
|
||||||
|
|
||||||
messages_obj = [provider_message.Message.model_validate(message) for message in messages]
|
|
||||||
funcs_obj = [resource_tool.LLMTool.model_validate(func) for func in funcs]
|
|
||||||
|
|
||||||
result = await llm_model.requester.invoke_llm(
|
|
||||||
query=None,
|
|
||||||
model=llm_model,
|
|
||||||
messages=messages_obj,
|
|
||||||
funcs=funcs_obj,
|
|
||||||
extra_args=extra_args,
|
|
||||||
)
|
|
||||||
|
|
||||||
return handler.ActionResponse.success(
|
|
||||||
data={
|
|
||||||
'message': result.model_dump(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
@self.action(RuntimeToLangBotAction.SET_BINARY_STORAGE)
|
|
||||||
async def set_binary_storage(data: dict[str, Any]) -> handler.ActionResponse:
|
|
||||||
"""Set binary storage"""
|
|
||||||
key = data['key']
|
|
||||||
owner_type = data['owner_type']
|
|
||||||
owner = data['owner']
|
|
||||||
value = base64.b64decode(data['value_base64'])
|
|
||||||
|
|
||||||
result = await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.select(persistence_bstorage.BinaryStorage)
|
|
||||||
.where(persistence_bstorage.BinaryStorage.key == key)
|
|
||||||
.where(persistence_bstorage.BinaryStorage.owner_type == owner_type)
|
|
||||||
.where(persistence_bstorage.BinaryStorage.owner == owner)
|
|
||||||
)
|
|
||||||
|
|
||||||
if result.first() is not None:
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.update(persistence_bstorage.BinaryStorage)
|
|
||||||
.where(persistence_bstorage.BinaryStorage.key == key)
|
|
||||||
.where(persistence_bstorage.BinaryStorage.owner_type == owner_type)
|
|
||||||
.where(persistence_bstorage.BinaryStorage.owner == owner)
|
|
||||||
.values(value=value)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.insert(persistence_bstorage.BinaryStorage).values(
|
|
||||||
unique_key=f'{owner_type}:{owner}:{key}',
|
|
||||||
key=key,
|
|
||||||
owner_type=owner_type,
|
|
||||||
owner=owner,
|
|
||||||
value=value,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return handler.ActionResponse.success(
|
|
||||||
data={},
|
|
||||||
)
|
|
||||||
|
|
||||||
@self.action(RuntimeToLangBotAction.GET_BINARY_STORAGE)
|
|
||||||
async def get_binary_storage(data: dict[str, Any]) -> handler.ActionResponse:
|
|
||||||
"""Get binary storage"""
|
|
||||||
key = data['key']
|
|
||||||
owner_type = data['owner_type']
|
|
||||||
owner = data['owner']
|
|
||||||
|
|
||||||
result = await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.select(persistence_bstorage.BinaryStorage)
|
|
||||||
.where(persistence_bstorage.BinaryStorage.key == key)
|
|
||||||
.where(persistence_bstorage.BinaryStorage.owner_type == owner_type)
|
|
||||||
.where(persistence_bstorage.BinaryStorage.owner == owner)
|
|
||||||
)
|
|
||||||
|
|
||||||
storage = result.first()
|
|
||||||
if storage is None:
|
|
||||||
return handler.ActionResponse.error(
|
|
||||||
message=f'Storage with key {key} not found',
|
|
||||||
)
|
|
||||||
|
|
||||||
return handler.ActionResponse.success(
|
|
||||||
data={
|
|
||||||
'value_base64': base64.b64encode(storage.value).decode('utf-8'),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
@self.action(RuntimeToLangBotAction.DELETE_BINARY_STORAGE)
|
|
||||||
async def delete_binary_storage(data: dict[str, Any]) -> handler.ActionResponse:
|
|
||||||
"""Delete binary storage"""
|
|
||||||
key = data['key']
|
|
||||||
owner_type = data['owner_type']
|
|
||||||
owner = data['owner']
|
|
||||||
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.delete(persistence_bstorage.BinaryStorage)
|
|
||||||
.where(persistence_bstorage.BinaryStorage.key == key)
|
|
||||||
.where(persistence_bstorage.BinaryStorage.owner_type == owner_type)
|
|
||||||
.where(persistence_bstorage.BinaryStorage.owner == owner)
|
|
||||||
)
|
|
||||||
|
|
||||||
return handler.ActionResponse.success(
|
|
||||||
data={},
|
|
||||||
)
|
|
||||||
|
|
||||||
@self.action(RuntimeToLangBotAction.GET_BINARY_STORAGE_KEYS)
|
|
||||||
async def get_binary_storage_keys(data: dict[str, Any]) -> handler.ActionResponse:
|
|
||||||
"""Get binary storage keys"""
|
|
||||||
owner_type = data['owner_type']
|
|
||||||
owner = data['owner']
|
|
||||||
|
|
||||||
result = await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.select(persistence_bstorage.BinaryStorage.key)
|
|
||||||
.where(persistence_bstorage.BinaryStorage.owner_type == owner_type)
|
|
||||||
.where(persistence_bstorage.BinaryStorage.owner == owner)
|
|
||||||
)
|
|
||||||
|
|
||||||
return handler.ActionResponse.success(
|
|
||||||
data={
|
|
||||||
'keys': result.scalars().all(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
async def ping(self) -> dict[str, Any]:
|
|
||||||
"""Ping the runtime"""
|
|
||||||
return await self.call_action(
|
|
||||||
CommonAction.PING,
|
|
||||||
{},
|
|
||||||
timeout=10,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def install_plugin(
|
|
||||||
self, install_source: str, install_info: dict[str, Any]
|
|
||||||
) -> typing.AsyncGenerator[dict[str, Any], None]:
|
|
||||||
"""Install plugin"""
|
|
||||||
gen = self.call_action_generator(
|
|
||||||
LangBotToRuntimeAction.INSTALL_PLUGIN,
|
|
||||||
{
|
|
||||||
'install_source': install_source,
|
|
||||||
'install_info': install_info,
|
|
||||||
},
|
|
||||||
timeout=120,
|
|
||||||
)
|
|
||||||
|
|
||||||
async for ret in gen:
|
|
||||||
yield ret
|
|
||||||
|
|
||||||
async def upgrade_plugin(self, plugin_author: str, plugin_name: str) -> typing.AsyncGenerator[dict[str, Any], None]:
|
|
||||||
"""Upgrade plugin"""
|
|
||||||
gen = self.call_action_generator(
|
|
||||||
LangBotToRuntimeAction.UPGRADE_PLUGIN,
|
|
||||||
{
|
|
||||||
'plugin_author': plugin_author,
|
|
||||||
'plugin_name': plugin_name,
|
|
||||||
},
|
|
||||||
timeout=120,
|
|
||||||
)
|
|
||||||
|
|
||||||
async for ret in gen:
|
|
||||||
yield ret
|
|
||||||
|
|
||||||
async def delete_plugin(self, plugin_author: str, plugin_name: str) -> typing.AsyncGenerator[dict[str, Any], None]:
|
|
||||||
"""Delete plugin"""
|
|
||||||
gen = self.call_action_generator(
|
|
||||||
LangBotToRuntimeAction.DELETE_PLUGIN,
|
|
||||||
{
|
|
||||||
'plugin_author': plugin_author,
|
|
||||||
'plugin_name': plugin_name,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
async for ret in gen:
|
|
||||||
yield ret
|
|
||||||
|
|
||||||
async def list_plugins(self) -> list[dict[str, Any]]:
|
|
||||||
"""List plugins"""
|
|
||||||
result = await self.call_action(
|
|
||||||
LangBotToRuntimeAction.LIST_PLUGINS,
|
|
||||||
{},
|
|
||||||
timeout=10,
|
|
||||||
)
|
|
||||||
|
|
||||||
return result['plugins']
|
|
||||||
|
|
||||||
async def get_plugin_info(self, author: str, plugin_name: str) -> dict[str, Any]:
|
|
||||||
"""Get plugin"""
|
|
||||||
result = await self.call_action(
|
|
||||||
LangBotToRuntimeAction.GET_PLUGIN_INFO,
|
|
||||||
{
|
|
||||||
'author': author,
|
|
||||||
'plugin_name': plugin_name,
|
|
||||||
},
|
|
||||||
timeout=10,
|
|
||||||
)
|
|
||||||
return result['plugin']
|
|
||||||
|
|
||||||
async def set_plugin_config(self, plugin_author: str, plugin_name: str, config: dict[str, Any]) -> dict[str, Any]:
|
|
||||||
"""Set plugin config"""
|
|
||||||
# update plugin setting
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.update(persistence_plugin.PluginSetting)
|
|
||||||
.where(persistence_plugin.PluginSetting.plugin_author == plugin_author)
|
|
||||||
.where(persistence_plugin.PluginSetting.plugin_name == plugin_name)
|
|
||||||
.values(config=config)
|
|
||||||
)
|
|
||||||
|
|
||||||
# restart plugin
|
|
||||||
gen = self.call_action_generator(
|
|
||||||
LangBotToRuntimeAction.RESTART_PLUGIN,
|
|
||||||
{
|
|
||||||
'plugin_author': plugin_author,
|
|
||||||
'plugin_name': plugin_name,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
async for ret in gen:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return {}
|
|
||||||
|
|
||||||
async def emit_event(
|
|
||||||
self,
|
|
||||||
event_context: dict[str, Any],
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""Emit event"""
|
|
||||||
result = await self.call_action(
|
|
||||||
LangBotToRuntimeAction.EMIT_EVENT,
|
|
||||||
{
|
|
||||||
'event_context': event_context,
|
|
||||||
},
|
|
||||||
timeout=60,
|
|
||||||
)
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
async def list_tools(self) -> list[dict[str, Any]]:
|
|
||||||
"""List tools"""
|
|
||||||
result = await self.call_action(
|
|
||||||
LangBotToRuntimeAction.LIST_TOOLS,
|
|
||||||
{},
|
|
||||||
timeout=20,
|
|
||||||
)
|
|
||||||
|
|
||||||
return result['tools']
|
|
||||||
|
|
||||||
async def get_plugin_icon(self, plugin_author: str, plugin_name: str) -> dict[str, Any]:
|
|
||||||
"""Get plugin icon"""
|
|
||||||
result = await self.call_action(
|
|
||||||
LangBotToRuntimeAction.GET_PLUGIN_ICON,
|
|
||||||
{
|
|
||||||
'plugin_author': plugin_author,
|
|
||||||
'plugin_name': plugin_name,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
plugin_icon_file_key = result['plugin_icon_file_key']
|
|
||||||
mime_type = result['mime_type']
|
|
||||||
|
|
||||||
plugin_icon_bytes = await self.read_local_file(plugin_icon_file_key)
|
|
||||||
|
|
||||||
await self.delete_local_file(plugin_icon_file_key)
|
|
||||||
|
|
||||||
return {
|
|
||||||
'plugin_icon_base64': base64.b64encode(plugin_icon_bytes).decode('utf-8'),
|
|
||||||
'mime_type': mime_type,
|
|
||||||
}
|
|
||||||
|
|
||||||
async def call_tool(self, tool_name: str, parameters: dict[str, Any]) -> dict[str, Any]:
|
|
||||||
"""Call tool"""
|
|
||||||
result = await self.call_action(
|
|
||||||
LangBotToRuntimeAction.CALL_TOOL,
|
|
||||||
{
|
|
||||||
'tool_name': tool_name,
|
|
||||||
'tool_parameters': parameters,
|
|
||||||
},
|
|
||||||
timeout=60,
|
|
||||||
)
|
|
||||||
|
|
||||||
return result['tool_response']
|
|
||||||
|
|
||||||
async def list_commands(self) -> list[dict[str, Any]]:
|
|
||||||
"""List commands"""
|
|
||||||
result = await self.call_action(
|
|
||||||
LangBotToRuntimeAction.LIST_COMMANDS,
|
|
||||||
{},
|
|
||||||
timeout=10,
|
|
||||||
)
|
|
||||||
return result['commands']
|
|
||||||
|
|
||||||
async def execute_command(self, command_context: dict[str, Any]) -> typing.AsyncGenerator[dict[str, Any], None]:
|
|
||||||
"""Execute command"""
|
|
||||||
gen = self.call_action_generator(
|
|
||||||
LangBotToRuntimeAction.EXECUTE_COMMAND,
|
|
||||||
{
|
|
||||||
'command_context': command_context,
|
|
||||||
},
|
|
||||||
timeout=60,
|
|
||||||
)
|
|
||||||
|
|
||||||
async for ret in gen:
|
|
||||||
yield ret
|
|
||||||
@@ -1,192 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import sqlalchemy
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
from . import requester
|
|
||||||
from ...core import app
|
|
||||||
from ...discover import engine
|
|
||||||
from . import token
|
|
||||||
from ...entity.persistence import model as persistence_model
|
|
||||||
from ...entity.errors import provider as provider_errors
|
|
||||||
|
|
||||||
FETCH_MODEL_LIST_URL = 'https://api.qchatgpt.rockchin.top/api/v2/fetch/model_list'
|
|
||||||
|
|
||||||
|
|
||||||
class ModelManager:
|
|
||||||
"""模型管理器"""
|
|
||||||
|
|
||||||
ap: app.Application
|
|
||||||
|
|
||||||
llm_models: list[requester.RuntimeLLMModel]
|
|
||||||
|
|
||||||
embedding_models: list[requester.RuntimeEmbeddingModel]
|
|
||||||
|
|
||||||
requester_components: list[engine.Component]
|
|
||||||
|
|
||||||
requester_dict: dict[str, type[requester.ProviderAPIRequester]] # cache
|
|
||||||
|
|
||||||
def __init__(self, ap: app.Application):
|
|
||||||
self.ap = ap
|
|
||||||
self.llm_models = []
|
|
||||||
self.embedding_models = []
|
|
||||||
self.requester_components = []
|
|
||||||
self.requester_dict = {}
|
|
||||||
|
|
||||||
async def initialize(self):
|
|
||||||
self.requester_components = self.ap.discover.get_components_by_kind('LLMAPIRequester')
|
|
||||||
|
|
||||||
# forge requester class dict
|
|
||||||
requester_dict: dict[str, type[requester.ProviderAPIRequester]] = {}
|
|
||||||
for component in self.requester_components:
|
|
||||||
requester_dict[component.metadata.name] = component.get_python_component_class()
|
|
||||||
|
|
||||||
self.requester_dict = requester_dict
|
|
||||||
|
|
||||||
await self.load_models_from_db()
|
|
||||||
|
|
||||||
async def load_models_from_db(self):
|
|
||||||
"""从数据库加载模型"""
|
|
||||||
self.ap.logger.info('Loading models from db...')
|
|
||||||
|
|
||||||
self.llm_models = []
|
|
||||||
self.embedding_models = []
|
|
||||||
|
|
||||||
# llm models
|
|
||||||
result = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_model.LLMModel))
|
|
||||||
llm_models = result.all()
|
|
||||||
for llm_model in llm_models:
|
|
||||||
try:
|
|
||||||
await self.load_llm_model(llm_model)
|
|
||||||
except provider_errors.RequesterNotFoundError as e:
|
|
||||||
self.ap.logger.warning(f'Requester {e.requester_name} not found, skipping model {llm_model.uuid}')
|
|
||||||
except Exception as e:
|
|
||||||
self.ap.logger.error(f'Failed to load model {llm_model.uuid}: {e}\n{traceback.format_exc()}')
|
|
||||||
|
|
||||||
# embedding models
|
|
||||||
result = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_model.EmbeddingModel))
|
|
||||||
embedding_models = result.all()
|
|
||||||
for embedding_model in embedding_models:
|
|
||||||
await self.load_embedding_model(embedding_model)
|
|
||||||
|
|
||||||
async def init_runtime_llm_model(
|
|
||||||
self,
|
|
||||||
model_info: persistence_model.LLMModel | sqlalchemy.Row[persistence_model.LLMModel] | dict,
|
|
||||||
):
|
|
||||||
"""初始化运行时 LLM 模型"""
|
|
||||||
if isinstance(model_info, sqlalchemy.Row):
|
|
||||||
model_info = persistence_model.LLMModel(**model_info._mapping)
|
|
||||||
elif isinstance(model_info, dict):
|
|
||||||
model_info = persistence_model.LLMModel(**model_info)
|
|
||||||
|
|
||||||
if model_info.requester not in self.requester_dict:
|
|
||||||
raise provider_errors.RequesterNotFoundError(model_info.requester)
|
|
||||||
|
|
||||||
requester_inst = self.requester_dict[model_info.requester](ap=self.ap, config=model_info.requester_config)
|
|
||||||
|
|
||||||
await requester_inst.initialize()
|
|
||||||
|
|
||||||
runtime_llm_model = requester.RuntimeLLMModel(
|
|
||||||
model_entity=model_info,
|
|
||||||
token_mgr=token.TokenManager(
|
|
||||||
name=model_info.uuid,
|
|
||||||
tokens=model_info.api_keys,
|
|
||||||
),
|
|
||||||
requester=requester_inst,
|
|
||||||
)
|
|
||||||
|
|
||||||
return runtime_llm_model
|
|
||||||
|
|
||||||
async def init_runtime_embedding_model(
|
|
||||||
self,
|
|
||||||
model_info: persistence_model.EmbeddingModel | sqlalchemy.Row[persistence_model.EmbeddingModel] | dict,
|
|
||||||
):
|
|
||||||
"""初始化运行时 Embedding 模型"""
|
|
||||||
if isinstance(model_info, sqlalchemy.Row):
|
|
||||||
model_info = persistence_model.EmbeddingModel(**model_info._mapping)
|
|
||||||
elif isinstance(model_info, dict):
|
|
||||||
model_info = persistence_model.EmbeddingModel(**model_info)
|
|
||||||
|
|
||||||
requester_inst = self.requester_dict[model_info.requester](ap=self.ap, config=model_info.requester_config)
|
|
||||||
|
|
||||||
await requester_inst.initialize()
|
|
||||||
|
|
||||||
runtime_embedding_model = requester.RuntimeEmbeddingModel(
|
|
||||||
model_entity=model_info,
|
|
||||||
token_mgr=token.TokenManager(
|
|
||||||
name=model_info.uuid,
|
|
||||||
tokens=model_info.api_keys,
|
|
||||||
),
|
|
||||||
requester=requester_inst,
|
|
||||||
)
|
|
||||||
|
|
||||||
return runtime_embedding_model
|
|
||||||
|
|
||||||
async def load_llm_model(
|
|
||||||
self,
|
|
||||||
model_info: persistence_model.LLMModel | sqlalchemy.Row[persistence_model.LLMModel] | dict,
|
|
||||||
):
|
|
||||||
"""加载 LLM 模型"""
|
|
||||||
runtime_llm_model = await self.init_runtime_llm_model(model_info)
|
|
||||||
self.llm_models.append(runtime_llm_model)
|
|
||||||
|
|
||||||
async def load_embedding_model(
|
|
||||||
self,
|
|
||||||
model_info: persistence_model.EmbeddingModel | sqlalchemy.Row[persistence_model.EmbeddingModel] | dict,
|
|
||||||
):
|
|
||||||
"""加载 Embedding 模型"""
|
|
||||||
runtime_embedding_model = await self.init_runtime_embedding_model(model_info)
|
|
||||||
self.embedding_models.append(runtime_embedding_model)
|
|
||||||
|
|
||||||
async def get_model_by_uuid(self, uuid: str) -> requester.RuntimeLLMModel:
|
|
||||||
"""通过uuid获取 LLM 模型"""
|
|
||||||
for model in self.llm_models:
|
|
||||||
if model.model_entity.uuid == uuid:
|
|
||||||
return model
|
|
||||||
raise ValueError(f'LLM model {uuid} not found')
|
|
||||||
|
|
||||||
async def get_embedding_model_by_uuid(self, uuid: str) -> requester.RuntimeEmbeddingModel:
|
|
||||||
"""通过uuid获取 Embedding 模型"""
|
|
||||||
for model in self.embedding_models:
|
|
||||||
if model.model_entity.uuid == uuid:
|
|
||||||
return model
|
|
||||||
raise ValueError(f'Embedding model {uuid} not found')
|
|
||||||
|
|
||||||
async def remove_llm_model(self, model_uuid: str):
|
|
||||||
"""移除 LLM 模型"""
|
|
||||||
for model in self.llm_models:
|
|
||||||
if model.model_entity.uuid == model_uuid:
|
|
||||||
self.llm_models.remove(model)
|
|
||||||
return
|
|
||||||
|
|
||||||
async def remove_embedding_model(self, model_uuid: str):
|
|
||||||
"""移除 Embedding 模型"""
|
|
||||||
for model in self.embedding_models:
|
|
||||||
if model.model_entity.uuid == model_uuid:
|
|
||||||
self.embedding_models.remove(model)
|
|
||||||
return
|
|
||||||
|
|
||||||
def get_available_requesters_info(self, model_type: str) -> list[dict]:
|
|
||||||
"""获取所有可用的请求器"""
|
|
||||||
if model_type != '':
|
|
||||||
return [
|
|
||||||
component.to_plain_dict()
|
|
||||||
for component in self.requester_components
|
|
||||||
if model_type in component.spec['support_type']
|
|
||||||
]
|
|
||||||
else:
|
|
||||||
return [component.to_plain_dict() for component in self.requester_components]
|
|
||||||
|
|
||||||
def get_available_requester_info_by_name(self, name: str) -> dict | None:
|
|
||||||
"""通过名称获取请求器信息"""
|
|
||||||
for component in self.requester_components:
|
|
||||||
if component.metadata.name == name:
|
|
||||||
return component.to_plain_dict()
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_available_requester_manifest_by_name(self, name: str) -> engine.Component | None:
|
|
||||||
"""通过名称获取请求器清单"""
|
|
||||||
for component in self.requester_components:
|
|
||||||
if component.metadata.name == name:
|
|
||||||
return component
|
|
||||||
return None
|
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import abc
|
|
||||||
import typing
|
|
||||||
|
|
||||||
from ...core import app
|
|
||||||
from ...entity.persistence import model as persistence_model
|
|
||||||
import langbot_plugin.api.entities.builtin.resource.tool as resource_tool
|
|
||||||
from . import token
|
|
||||||
import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query
|
|
||||||
import langbot_plugin.api.entities.builtin.provider.message as provider_message
|
|
||||||
|
|
||||||
|
|
||||||
class RuntimeLLMModel:
|
|
||||||
"""运行时模型"""
|
|
||||||
|
|
||||||
model_entity: persistence_model.LLMModel
|
|
||||||
"""模型数据"""
|
|
||||||
|
|
||||||
token_mgr: token.TokenManager
|
|
||||||
"""api key管理器"""
|
|
||||||
|
|
||||||
requester: ProviderAPIRequester
|
|
||||||
"""请求器实例"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
model_entity: persistence_model.LLMModel,
|
|
||||||
token_mgr: token.TokenManager,
|
|
||||||
requester: ProviderAPIRequester,
|
|
||||||
):
|
|
||||||
self.model_entity = model_entity
|
|
||||||
self.token_mgr = token_mgr
|
|
||||||
self.requester = requester
|
|
||||||
|
|
||||||
|
|
||||||
class RuntimeEmbeddingModel:
|
|
||||||
"""运行时 Embedding 模型"""
|
|
||||||
|
|
||||||
model_entity: persistence_model.EmbeddingModel
|
|
||||||
"""模型数据"""
|
|
||||||
|
|
||||||
token_mgr: token.TokenManager
|
|
||||||
"""api key管理器"""
|
|
||||||
|
|
||||||
requester: ProviderAPIRequester
|
|
||||||
"""请求器实例"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
model_entity: persistence_model.EmbeddingModel,
|
|
||||||
token_mgr: token.TokenManager,
|
|
||||||
requester: ProviderAPIRequester,
|
|
||||||
):
|
|
||||||
self.model_entity = model_entity
|
|
||||||
self.token_mgr = token_mgr
|
|
||||||
self.requester = requester
|
|
||||||
|
|
||||||
|
|
||||||
class ProviderAPIRequester(metaclass=abc.ABCMeta):
|
|
||||||
"""Provider API请求器"""
|
|
||||||
|
|
||||||
name: str = None
|
|
||||||
|
|
||||||
ap: app.Application
|
|
||||||
|
|
||||||
default_config: dict[str, typing.Any] = {}
|
|
||||||
|
|
||||||
requester_cfg: dict[str, typing.Any] = {}
|
|
||||||
|
|
||||||
def __init__(self, ap: app.Application, config: dict[str, typing.Any]):
|
|
||||||
self.ap = ap
|
|
||||||
self.requester_cfg = {**self.default_config}
|
|
||||||
self.requester_cfg.update(config)
|
|
||||||
|
|
||||||
async def initialize(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
async def invoke_llm(
|
|
||||||
self,
|
|
||||||
query: pipeline_query.Query,
|
|
||||||
model: RuntimeLLMModel,
|
|
||||||
messages: typing.List[provider_message.Message],
|
|
||||||
funcs: typing.List[resource_tool.LLMTool] = None,
|
|
||||||
extra_args: dict[str, typing.Any] = {},
|
|
||||||
remove_think: bool = False,
|
|
||||||
) -> provider_message.Message:
|
|
||||||
"""调用API
|
|
||||||
|
|
||||||
Args:
|
|
||||||
model (RuntimeLLMModel): 使用的模型信息
|
|
||||||
messages (typing.List[llm_entities.Message]): 消息对象列表
|
|
||||||
funcs (typing.List[tools_entities.LLMFunction], optional): 使用的工具函数列表. Defaults to None.
|
|
||||||
extra_args (dict[str, typing.Any], optional): 额外的参数. Defaults to {}.
|
|
||||||
remove_think (bool, optional): 是否移思考中的消息. Defaults to False.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
llm_entities.Message: 返回消息对象
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def invoke_llm_stream(
|
|
||||||
self,
|
|
||||||
query: pipeline_query.Query,
|
|
||||||
model: RuntimeLLMModel,
|
|
||||||
messages: typing.List[provider_message.Message],
|
|
||||||
funcs: typing.List[resource_tool.LLMTool] = None,
|
|
||||||
extra_args: dict[str, typing.Any] = {},
|
|
||||||
remove_think: bool = False,
|
|
||||||
) -> provider_message.MessageChunk:
|
|
||||||
"""调用API
|
|
||||||
|
|
||||||
Args:
|
|
||||||
model (RuntimeLLMModel): 使用的模型信息
|
|
||||||
messages (typing.List[provider_message.Message]): 消息对象列表
|
|
||||||
funcs (typing.List[resource_tool.LLMTool], optional): 使用的工具函数列表. Defaults to None.
|
|
||||||
extra_args (dict[str, typing.Any], optional): 额外的参数. Defaults to {}.
|
|
||||||
remove_think (bool, optional): 是否移除思考中的消息. Defaults to False.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
typing.AsyncGenerator[provider_message.MessageChunk]: 返回消息对象
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def invoke_embedding(
|
|
||||||
self,
|
|
||||||
model: RuntimeEmbeddingModel,
|
|
||||||
input_text: typing.List[str],
|
|
||||||
extra_args: dict[str, typing.Any] = {},
|
|
||||||
) -> typing.List[typing.List[float]]:
|
|
||||||
"""调用 Embedding API
|
|
||||||
|
|
||||||
Args:
|
|
||||||
model (RuntimeEmbeddingModel): 使用的模型信息
|
|
||||||
input_text (typing.List[str]): 输入文本
|
|
||||||
extra_args (dict[str, typing.Any], optional): 额外的参数. Defaults to {}.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
typing.List[typing.List[float]]: 返回的 embedding 向量
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: LLMAPIRequester
|
|
||||||
metadata:
|
|
||||||
name: 302-ai-chat-completions
|
|
||||||
label:
|
|
||||||
en_US: 302.AI
|
|
||||||
zh_Hans: 302.AI
|
|
||||||
icon: 302ai.png
|
|
||||||
spec:
|
|
||||||
config:
|
|
||||||
- name: base_url
|
|
||||||
label:
|
|
||||||
en_US: Base URL
|
|
||||||
zh_Hans: 基础 URL
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: "https://api.302.ai/v1"
|
|
||||||
- name: timeout
|
|
||||||
label:
|
|
||||||
en_US: Timeout
|
|
||||||
zh_Hans: 超时时间
|
|
||||||
type: integer
|
|
||||||
required: true
|
|
||||||
default: 120
|
|
||||||
support_type:
|
|
||||||
- llm
|
|
||||||
- text-embedding
|
|
||||||
execution:
|
|
||||||
python:
|
|
||||||
path: ./302aichatcmpl.py
|
|
||||||
attr: AI302ChatCompletions
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: LLMAPIRequester
|
|
||||||
metadata:
|
|
||||||
name: anthropic-messages
|
|
||||||
label:
|
|
||||||
en_US: Anthropic
|
|
||||||
zh_Hans: Anthropic
|
|
||||||
icon: anthropic.svg
|
|
||||||
spec:
|
|
||||||
config:
|
|
||||||
- name: base_url
|
|
||||||
label:
|
|
||||||
en_US: Base URL
|
|
||||||
zh_Hans: 基础 URL
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: "https://api.anthropic.com"
|
|
||||||
- name: timeout
|
|
||||||
label:
|
|
||||||
en_US: Timeout
|
|
||||||
zh_Hans: 超时时间
|
|
||||||
type: integer
|
|
||||||
required: true
|
|
||||||
default: 120
|
|
||||||
support_type:
|
|
||||||
- llm
|
|
||||||
execution:
|
|
||||||
python:
|
|
||||||
path: ./anthropicmsgs.py
|
|
||||||
attr: AnthropicMessages
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: LLMAPIRequester
|
|
||||||
metadata:
|
|
||||||
name: bailian-chat-completions
|
|
||||||
label:
|
|
||||||
en_US: Aliyun Bailian
|
|
||||||
zh_Hans: 阿里云百炼
|
|
||||||
icon: bailian.png
|
|
||||||
spec:
|
|
||||||
config:
|
|
||||||
- name: base_url
|
|
||||||
label:
|
|
||||||
en_US: Base URL
|
|
||||||
zh_Hans: 基础 URL
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
|
||||||
- name: timeout
|
|
||||||
label:
|
|
||||||
en_US: Timeout
|
|
||||||
zh_Hans: 超时时间
|
|
||||||
type: integer
|
|
||||||
required: true
|
|
||||||
default: 120
|
|
||||||
support_type:
|
|
||||||
- llm
|
|
||||||
execution:
|
|
||||||
python:
|
|
||||||
path: ./bailianchatcmpl.py
|
|
||||||
attr: BailianChatCompletions
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: LLMAPIRequester
|
|
||||||
metadata:
|
|
||||||
name: openai-chat-completions
|
|
||||||
label:
|
|
||||||
en_US: OpenAI
|
|
||||||
zh_Hans: OpenAI
|
|
||||||
icon: openai.svg
|
|
||||||
spec:
|
|
||||||
config:
|
|
||||||
- name: base_url
|
|
||||||
label:
|
|
||||||
en_US: Base URL
|
|
||||||
zh_Hans: 基础 URL
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: "https://api.openai.com/v1"
|
|
||||||
- name: timeout
|
|
||||||
label:
|
|
||||||
en_US: Timeout
|
|
||||||
zh_Hans: 超时时间
|
|
||||||
type: integer
|
|
||||||
required: true
|
|
||||||
default: 120
|
|
||||||
support_type:
|
|
||||||
- llm
|
|
||||||
- text-embedding
|
|
||||||
execution:
|
|
||||||
python:
|
|
||||||
path: ./chatcmpl.py
|
|
||||||
attr: OpenAIChatCompletions
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: LLMAPIRequester
|
|
||||||
metadata:
|
|
||||||
name: compshare-chat-completions
|
|
||||||
label:
|
|
||||||
en_US: CompShare
|
|
||||||
zh_Hans: 优云智算
|
|
||||||
icon: compshare.png
|
|
||||||
spec:
|
|
||||||
config:
|
|
||||||
- name: base_url
|
|
||||||
label:
|
|
||||||
en_US: Base URL
|
|
||||||
zh_Hans: 基础 URL
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: "https://api.modelverse.cn/v1"
|
|
||||||
- name: timeout
|
|
||||||
label:
|
|
||||||
en_US: Timeout
|
|
||||||
zh_Hans: 超时时间
|
|
||||||
type: integer
|
|
||||||
required: true
|
|
||||||
default: 120
|
|
||||||
support_type:
|
|
||||||
- llm
|
|
||||||
execution:
|
|
||||||
python:
|
|
||||||
path: ./compsharechatcmpl.py
|
|
||||||
attr: CompShareChatCompletions
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: LLMAPIRequester
|
|
||||||
metadata:
|
|
||||||
name: deepseek-chat-completions
|
|
||||||
label:
|
|
||||||
en_US: DeepSeek
|
|
||||||
zh_Hans: DeepSeek
|
|
||||||
icon: deepseek.svg
|
|
||||||
spec:
|
|
||||||
config:
|
|
||||||
- name: base_url
|
|
||||||
label:
|
|
||||||
en_US: Base URL
|
|
||||||
zh_Hans: 基础 URL
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: "https://api.deepseek.com"
|
|
||||||
- name: timeout
|
|
||||||
label:
|
|
||||||
en_US: Timeout
|
|
||||||
zh_Hans: 超时时间
|
|
||||||
type: integer
|
|
||||||
required: true
|
|
||||||
default: 120
|
|
||||||
support_type:
|
|
||||||
- llm
|
|
||||||
execution:
|
|
||||||
python:
|
|
||||||
path: ./deepseekchatcmpl.py
|
|
||||||
attr: DeepseekChatCompletions
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: LLMAPIRequester
|
|
||||||
metadata:
|
|
||||||
name: gemini-chat-completions
|
|
||||||
label:
|
|
||||||
en_US: Google Gemini
|
|
||||||
zh_Hans: Google Gemini
|
|
||||||
icon: gemini.svg
|
|
||||||
spec:
|
|
||||||
config:
|
|
||||||
- name: base_url
|
|
||||||
label:
|
|
||||||
en_US: Base URL
|
|
||||||
zh_Hans: 基础 URL
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: "https://generativelanguage.googleapis.com/v1beta/openai"
|
|
||||||
- name: timeout
|
|
||||||
label:
|
|
||||||
en_US: Timeout
|
|
||||||
zh_Hans: 超时时间
|
|
||||||
type: integer
|
|
||||||
required: true
|
|
||||||
default: 120
|
|
||||||
support_type:
|
|
||||||
- llm
|
|
||||||
execution:
|
|
||||||
python:
|
|
||||||
path: ./geminichatcmpl.py
|
|
||||||
attr: GeminiChatCompletions
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: LLMAPIRequester
|
|
||||||
metadata:
|
|
||||||
name: gitee-ai-chat-completions
|
|
||||||
label:
|
|
||||||
en_US: Gitee AI
|
|
||||||
zh_Hans: Gitee AI
|
|
||||||
icon: giteeai.svg
|
|
||||||
spec:
|
|
||||||
config:
|
|
||||||
- name: base_url
|
|
||||||
label:
|
|
||||||
en_US: Base URL
|
|
||||||
zh_Hans: 基础 URL
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: "https://ai.gitee.com/v1"
|
|
||||||
- name: timeout
|
|
||||||
label:
|
|
||||||
en_US: Timeout
|
|
||||||
zh_Hans: 超时时间
|
|
||||||
type: integer
|
|
||||||
required: true
|
|
||||||
default: 120
|
|
||||||
support_type:
|
|
||||||
- llm
|
|
||||||
- text-embedding
|
|
||||||
execution:
|
|
||||||
python:
|
|
||||||
path: ./giteeaichatcmpl.py
|
|
||||||
attr: GiteeAIChatCompletions
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: LLMAPIRequester
|
|
||||||
metadata:
|
|
||||||
name: lmstudio-chat-completions
|
|
||||||
label:
|
|
||||||
en_US: LM Studio
|
|
||||||
zh_Hans: LM Studio
|
|
||||||
icon: lmstudio.webp
|
|
||||||
spec:
|
|
||||||
config:
|
|
||||||
- name: base_url
|
|
||||||
label:
|
|
||||||
en_US: Base URL
|
|
||||||
zh_Hans: 基础 URL
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: "http://127.0.0.1:1234/v1"
|
|
||||||
- name: timeout
|
|
||||||
label:
|
|
||||||
en_US: Timeout
|
|
||||||
zh_Hans: 超时时间
|
|
||||||
type: integer
|
|
||||||
required: true
|
|
||||||
default: 120
|
|
||||||
support_type:
|
|
||||||
- llm
|
|
||||||
- text-embedding
|
|
||||||
execution:
|
|
||||||
python:
|
|
||||||
path: ./lmstudiochatcmpl.py
|
|
||||||
attr: LmStudioChatCompletions
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user